@yadimon/ng-smart-images 0.1.0 → 0.1.2

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,6 +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
9
  - Writes a source manifest that you can keep in version control and extend over time.
9
10
  - Writes a generated runtime manifest plus helper wrapper for code-driven lookups.
10
11
  - Rewrites static `html` and `css` asset references in `dist/` to hashed URLs.
@@ -1,3 +1,5 @@
1
+ import { constants } from 'node:fs';
2
+ import { access, readFile, stat } from 'node:fs/promises';
1
3
  import path from 'node:path';
2
4
  import { loadProjectConfig, resolveEntryConfig } from './config.js';
3
5
  import { generateImageArtifact } from './optimize.js';
@@ -5,14 +7,32 @@ import { writeTextFile } from '../utils/fs.js';
5
7
  export async function generateHashedImages(options = {}) {
6
8
  const cwd = path.resolve(options.cwd ?? process.cwd());
7
9
  const { manifestPath, config } = await loadProjectConfig(cwd, options.manifestPath);
8
- const runtimeManifest = {};
9
10
  const generatedAssetsDir = path.join(cwd, config.generatedAssetsDir);
10
11
  const assetsRootAbsolute = path.join(cwd, config.assetsRoot);
12
+ const runtimeManifestJsonPath = path.join(cwd, config.runtimeManifestJsonPath);
13
+ const runtimeManifestTsPath = path.join(cwd, config.runtimeManifestTsPath);
14
+ const runtimeHelperTsPath = path.join(cwd, config.runtimeHelperTsPath);
15
+ const runtimeManifest = {};
16
+ const previousRuntimeManifest = await loadRuntimeManifest(runtimeManifestJsonPath);
17
+ const runtimeManifestMtimeMs = await getFileModifiedTime(runtimeManifestJsonPath);
18
+ const projectManifestMtimeMs = await getFileModifiedTime(manifestPath);
11
19
  for (const sourceKey of Object.keys(config.images).sort()) {
12
20
  const resolvedEntry = resolveEntryConfig(sourceKey, config, cwd);
13
21
  if (!resolvedEntry) {
14
22
  continue;
15
23
  }
24
+ const reusableEntry = await tryReuseGeneratedEntry({
25
+ existingEntry: previousRuntimeManifest[sourceKey],
26
+ runtimeManifestMtimeMs,
27
+ projectManifestMtimeMs,
28
+ sourcePath: resolvedEntry.sourcePath,
29
+ outputRoot: generatedAssetsDir,
30
+ publicPath: config.publicPath,
31
+ });
32
+ if (reusableEntry) {
33
+ runtimeManifest[sourceKey] = reusableEntry;
34
+ continue;
35
+ }
16
36
  const artifact = await generateImageArtifact({
17
37
  sourceKey,
18
38
  sourcePath: resolvedEntry.sourcePath,
@@ -23,9 +43,6 @@ export async function generateHashedImages(options = {}) {
23
43
  });
24
44
  runtimeManifest[sourceKey] = artifact.entry;
25
45
  }
26
- const runtimeManifestJsonPath = path.join(cwd, config.runtimeManifestJsonPath);
27
- const runtimeManifestTsPath = path.join(cwd, config.runtimeManifestTsPath);
28
- const runtimeHelperTsPath = path.join(cwd, config.runtimeHelperTsPath);
29
46
  await writeTextFile(runtimeManifestJsonPath, `${JSON.stringify(runtimeManifest, null, 2)}\n`);
30
47
  await writeTextFile(runtimeManifestTsPath, buildRuntimeManifestModule(runtimeManifest));
31
48
  await writeTextFile(runtimeHelperTsPath, buildRuntimeHelperModule(path.basename(runtimeManifestTsPath, '.ts')));
@@ -44,3 +61,75 @@ function buildRuntimeManifestModule(runtimeManifest) {
44
61
  function buildRuntimeHelperModule(runtimeManifestBaseName) {
45
62
  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`;
46
63
  }
64
+ async function tryReuseGeneratedEntry(input) {
65
+ if (!input.existingEntry || input.runtimeManifestMtimeMs === null) {
66
+ return null;
67
+ }
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) {
74
+ return null;
75
+ }
76
+ const generatedFilesExist = await allGeneratedFilesExist(input.existingEntry, input.outputRoot, input.publicPath);
77
+ return generatedFilesExist ? input.existingEntry : null;
78
+ }
79
+ async function allGeneratedFilesExist(entry, outputRoot, publicPath) {
80
+ if (entry.sources.length === 0) {
81
+ return false;
82
+ }
83
+ for (const source of entry.sources) {
84
+ const generatedPath = resolveGeneratedPath(source.src, outputRoot, publicPath);
85
+ if (!generatedPath || !(await fileExists(generatedPath))) {
86
+ return false;
87
+ }
88
+ }
89
+ return true;
90
+ }
91
+ function resolveGeneratedPath(sourceSrc, outputRoot, publicPath) {
92
+ const normalizedPublicPath = normalizePublicPath(publicPath);
93
+ if (sourceSrc !== normalizedPublicPath && !sourceSrc.startsWith(`${normalizedPublicPath}/`)) {
94
+ return null;
95
+ }
96
+ const relativePath = sourceSrc.slice(normalizedPublicPath.length).replace(/^\/+/, '');
97
+ if (!relativePath) {
98
+ return null;
99
+ }
100
+ return path.join(outputRoot, relativePath);
101
+ }
102
+ function normalizePublicPath(publicPath) {
103
+ const trimmed = publicPath.replace(/\/+$/, '');
104
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
105
+ }
106
+ async function loadRuntimeManifest(filePath) {
107
+ try {
108
+ const raw = await readFile(filePath, 'utf8');
109
+ if (!raw.trim()) {
110
+ return {};
111
+ }
112
+ return JSON.parse(raw);
113
+ }
114
+ catch {
115
+ return {};
116
+ }
117
+ }
118
+ async function getFileModifiedTime(filePath) {
119
+ try {
120
+ const fileStat = await stat(filePath);
121
+ return fileStat.mtimeMs;
122
+ }
123
+ catch {
124
+ return null;
125
+ }
126
+ }
127
+ async function fileExists(filePath) {
128
+ try {
129
+ await access(filePath, constants.F_OK);
130
+ return true;
131
+ }
132
+ catch {
133
+ return false;
134
+ }
135
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yadimon/ng-smart-images",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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",
@@ -56,7 +56,8 @@
56
56
  },
57
57
  "scripts": {
58
58
  "build": "node ./scripts/build-package.mjs",
59
- "pack": "node -e \"require('node:fs').mkdirSync('../../.artifacts', { recursive: true })\" && npm run build && npm pack --pack-destination ../../.artifacts",
59
+ "prepack": "npm run build",
60
+ "pack": "node ./scripts/pack-package.mjs",
60
61
  "lint": "eslint src tests scripts",
61
62
  "typecheck": "tsc -p tsconfig.json --noEmit",
62
63
  "test": "npm run build && vitest run",