@yadimon/ng-smart-images 0.1.3 → 0.2.1

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 CHANGED
@@ -5,7 +5,7 @@
5
5
  ## What It Does
6
6
 
7
7
  - Generates hashed `avif`, `webp`, and original-format outputs.
8
- - Reuses unchanged generated assets when the source image and manifest config have not changed.
8
+ - Reuses unchanged generated assets through a fingerprint cache based on source content, normalized config, and package version.
9
9
  - Writes a source manifest that you can keep in version control and extend over time.
10
10
  - Writes a generated runtime manifest plus helper wrapper for code-driven lookups.
11
11
  - Rewrites static `html` and `css` asset references in `dist/` to hashed URLs.
@@ -68,6 +68,7 @@ Generates hashed files into `generatedAssetsDir` and writes:
68
68
  - `runtimeManifestJsonPath`
69
69
  - `runtimeManifestTsPath`
70
70
  - `runtimeHelperTsPath`
71
+ - a sibling `.ng-smart-images.cache.json` file for build-time reuse decisions
71
72
 
72
73
  ```bash
73
74
  npx ng-smart-images generate-hashed
@@ -141,5 +142,5 @@ Repository-wide release guidance lives in [`RELEASING.md`](../../RELEASING.md).
141
142
 
142
143
  - keep the package on semver
143
144
  - use `npm run publish:dry-run` before a real publish
144
- - bump versions with the root `version:*` scripts
145
+ - bump versions with the root `release:*` scripts
145
146
  - prefer GitHub trusted publishing once the repository is connected on npm
@@ -1,36 +1,54 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { constants } from 'node:fs';
2
- import { access, readFile, stat } from 'node:fs/promises';
3
+ import { access, readFile } from 'node:fs/promises';
3
4
  import path from 'node:path';
4
5
  import { loadProjectConfig, resolveEntryConfig } from './config.js';
5
6
  import { generateImageArtifact } from './optimize.js';
6
7
  import { writeTextFile } from '../utils/fs.js';
8
+ const IMAGE_REUSE_CACHE_FILE_NAME = '.ng-smart-images.cache.json';
9
+ const IMAGE_REUSE_CACHE_SCHEMA_VERSION = 1;
10
+ let installedPackageVersionPromise = null;
7
11
  export async function generateHashedImages(options = {}) {
8
12
  const cwd = path.resolve(options.cwd ?? process.cwd());
9
13
  const { manifestPath, config } = await loadProjectConfig(cwd, options.manifestPath);
10
14
  const generatedAssetsDir = path.join(cwd, config.generatedAssetsDir);
11
15
  const assetsRootAbsolute = path.join(cwd, config.assetsRoot);
12
16
  const runtimeManifestJsonPath = path.join(cwd, config.runtimeManifestJsonPath);
17
+ const runtimeCacheJsonPath = path.join(path.dirname(runtimeManifestJsonPath), IMAGE_REUSE_CACHE_FILE_NAME);
13
18
  const runtimeManifestTsPath = path.join(cwd, config.runtimeManifestTsPath);
14
19
  const runtimeHelperTsPath = path.join(cwd, config.runtimeHelperTsPath);
15
20
  const runtimeManifest = {};
16
21
  const previousRuntimeManifest = await loadRuntimeManifest(runtimeManifestJsonPath);
17
- const runtimeManifestMtimeMs = await getFileModifiedTime(runtimeManifestJsonPath);
18
- const projectManifestMtimeMs = await getFileModifiedTime(manifestPath);
22
+ const previousReuseCache = await loadReuseCache(runtimeCacheJsonPath);
23
+ const packageVersion = await getInstalledPackageVersion();
24
+ const runtimeReuseCache = {
25
+ schemaVersion: IMAGE_REUSE_CACHE_SCHEMA_VERSION,
26
+ packageVersion,
27
+ entries: {},
28
+ };
19
29
  for (const sourceKey of Object.keys(config.images).sort()) {
20
30
  const resolvedEntry = resolveEntryConfig(sourceKey, config, cwd);
21
31
  if (!resolvedEntry) {
22
32
  continue;
23
33
  }
34
+ const fingerprint = await createEntryFingerprint({
35
+ sourceKey,
36
+ sourcePath: resolvedEntry.sourcePath,
37
+ sizes: resolvedEntry.sizes,
38
+ quality: resolvedEntry.quality,
39
+ extensions: resolvedEntry.extensions,
40
+ packageVersion,
41
+ });
24
42
  const reusableEntry = await tryReuseGeneratedEntry({
25
43
  existingEntry: previousRuntimeManifest[sourceKey],
26
- runtimeManifestMtimeMs,
27
- projectManifestMtimeMs,
28
- sourcePath: resolvedEntry.sourcePath,
44
+ cacheEntry: previousReuseCache.entries[sourceKey],
45
+ fingerprint,
29
46
  outputRoot: generatedAssetsDir,
30
47
  publicPath: config.publicPath,
31
48
  });
32
49
  if (reusableEntry) {
33
50
  runtimeManifest[sourceKey] = reusableEntry;
51
+ runtimeReuseCache.entries[sourceKey] = { fingerprint };
34
52
  continue;
35
53
  }
36
54
  const artifact = await generateImageArtifact({
@@ -42,13 +60,16 @@ export async function generateHashedImages(options = {}) {
42
60
  entryConfig: resolvedEntry,
43
61
  });
44
62
  runtimeManifest[sourceKey] = artifact.entry;
63
+ runtimeReuseCache.entries[sourceKey] = { fingerprint };
45
64
  }
46
65
  await writeTextFile(runtimeManifestJsonPath, `${JSON.stringify(runtimeManifest, null, 2)}\n`);
66
+ await writeTextFile(runtimeCacheJsonPath, `${JSON.stringify(runtimeReuseCache, null, 2)}\n`);
47
67
  await writeTextFile(runtimeManifestTsPath, buildRuntimeManifestModule(runtimeManifest));
48
68
  await writeTextFile(runtimeHelperTsPath, buildRuntimeHelperModule(path.basename(runtimeManifestTsPath, '.ts')));
49
69
  return {
50
70
  manifestPath,
51
71
  runtimeManifestJsonPath,
72
+ runtimeCacheJsonPath,
52
73
  runtimeManifestTsPath,
53
74
  runtimeHelperTsPath,
54
75
  generatedAssetsDir,
@@ -62,15 +83,10 @@ function buildRuntimeHelperModule(runtimeManifestBaseName) {
62
83
  return `import manifest from './${runtimeManifestBaseName}.js';\nimport { createSmartImageResolver } from '@yadimon/ng-smart-images/runtime';\n\nconst resolver = createSmartImageResolver(manifest);\n\nexport const hashed = (path: string): string => resolver.hashed(path);\nexport const imageEntry = (path: string) => resolver.imageEntry(path);\nexport const imagePlaceholder = (path: string): string => resolver.imagePlaceholder(path);\nexport const imageSources = (path: string) => resolver.imageSources(path);\nexport const hasImage = (path: string): boolean => resolver.hasImage(path);\n\nexport default resolver;\n`;
63
84
  }
64
85
  async function tryReuseGeneratedEntry(input) {
65
- if (!input.existingEntry || input.runtimeManifestMtimeMs === null) {
86
+ if (!input.existingEntry || !input.cacheEntry) {
66
87
  return null;
67
88
  }
68
- const sourceMtimeMs = await getFileModifiedTime(input.sourcePath);
69
- if (sourceMtimeMs === null) {
70
- return null;
71
- }
72
- const freshnessFloor = Math.max(sourceMtimeMs, input.projectManifestMtimeMs ?? 0);
73
- if (input.runtimeManifestMtimeMs < freshnessFloor) {
89
+ if (input.cacheEntry.fingerprint !== input.fingerprint) {
74
90
  return null;
75
91
  }
76
92
  const generatedFilesExist = await allGeneratedFilesExist(input.existingEntry, input.outputRoot, input.publicPath);
@@ -115,13 +131,27 @@ async function loadRuntimeManifest(filePath) {
115
131
  return {};
116
132
  }
117
133
  }
118
- async function getFileModifiedTime(filePath) {
134
+ async function loadReuseCache(filePath) {
119
135
  try {
120
- const fileStat = await stat(filePath);
121
- return fileStat.mtimeMs;
136
+ const raw = await readFile(filePath, 'utf8');
137
+ if (!raw.trim()) {
138
+ return createEmptyReuseCache();
139
+ }
140
+ const parsed = JSON.parse(raw);
141
+ if (parsed.schemaVersion !== IMAGE_REUSE_CACHE_SCHEMA_VERSION) {
142
+ return createEmptyReuseCache();
143
+ }
144
+ const entries = Object.fromEntries(Object.entries(parsed.entries ?? {}).flatMap(([sourceKey, value]) => value && typeof value.fingerprint === 'string'
145
+ ? [[sourceKey, { fingerprint: value.fingerprint }]]
146
+ : []));
147
+ return {
148
+ schemaVersion: IMAGE_REUSE_CACHE_SCHEMA_VERSION,
149
+ packageVersion: typeof parsed.packageVersion === 'string' ? parsed.packageVersion : '',
150
+ entries,
151
+ };
122
152
  }
123
153
  catch {
124
- return null;
154
+ return createEmptyReuseCache();
125
155
  }
126
156
  }
127
157
  async function fileExists(filePath) {
@@ -133,3 +163,32 @@ async function fileExists(filePath) {
133
163
  return false;
134
164
  }
135
165
  }
166
+ function createEmptyReuseCache() {
167
+ return {
168
+ schemaVersion: IMAGE_REUSE_CACHE_SCHEMA_VERSION,
169
+ packageVersion: '',
170
+ entries: {},
171
+ };
172
+ }
173
+ async function createEntryFingerprint(input) {
174
+ const sourceBytes = await readFile(input.sourcePath);
175
+ const sourceHash = createHash('sha256').update(sourceBytes).digest('hex');
176
+ return createHash('sha256')
177
+ .update(JSON.stringify({
178
+ sourceHash,
179
+ sourceKey: input.sourceKey,
180
+ entryConfig: {
181
+ sizes: input.sizes,
182
+ quality: input.quality,
183
+ extensions: input.extensions,
184
+ },
185
+ packageVersion: input.packageVersion,
186
+ }))
187
+ .digest('hex');
188
+ }
189
+ async function getInstalledPackageVersion() {
190
+ installedPackageVersionPromise ??= readFile(new URL('../../package.json', import.meta.url), 'utf8')
191
+ .then((raw) => JSON.parse(raw))
192
+ .then((pkg) => (typeof pkg.version === 'string' ? pkg.version : '0.0.0'));
193
+ return installedPackageVersionPromise;
194
+ }
@@ -44,11 +44,20 @@ export interface GenerateHashedOptions {
44
44
  export interface GeneratedProjectArtifacts {
45
45
  manifestPath: string;
46
46
  runtimeManifestJsonPath: string;
47
+ runtimeCacheJsonPath: string;
47
48
  runtimeManifestTsPath: string;
48
49
  runtimeHelperTsPath: string;
49
50
  generatedAssetsDir: string;
50
51
  runtimeManifest: SmartImagesManifest;
51
52
  }
53
+ export interface SmartImageReuseCacheEntry {
54
+ fingerprint: string;
55
+ }
56
+ export interface SmartImageReuseCache {
57
+ schemaVersion: number;
58
+ packageVersion: string;
59
+ entries: Record<string, SmartImageReuseCacheEntry>;
60
+ }
52
61
  export interface UpdateBundleOptions {
53
62
  cwd?: string;
54
63
  manifestPath?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yadimon/ng-smart-images",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "CLI-first smart image optimization with hashed asset generation, runtime manifests, and optional Angular helpers.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -66,7 +66,7 @@
66
66
  "dependencies": {
67
67
  "picomatch": "^4.0.3",
68
68
  "sharp": "^0.34.5",
69
- "tinyglobby": "^0.2.15"
69
+ "tinyglobby": "^0.2.16"
70
70
  },
71
71
  "peerDependencies": {
72
72
  "@angular/core": "^20.0.0 || ^21.0.0 || ^22.0.0"
@@ -77,10 +77,10 @@
77
77
  }
78
78
  },
79
79
  "devDependencies": {
80
- "@angular/core": "^21.2.0",
81
- "@types/node": "^25.5.0",
82
- "@types/picomatch": "^4.0.2",
80
+ "@angular/core": "^21.2.9",
81
+ "@types/node": "^25.6.0",
82
+ "@types/picomatch": "^4.0.3",
83
83
  "typescript": "~5.9.3",
84
- "vitest": "^4.0.8"
84
+ "vitest": "^4.1.4"
85
85
  }
86
86
  }