@yadimon/ng-smart-images 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,144 @@
1
+ # @yadimon/ng-smart-images
2
+
3
+ `@yadimon/ng-smart-images` is a CLI-first image optimization tool for Angular and other frontend builds. It generates hashed local image variants, writes a runtime manifest, and can rewrite built HTML and CSS to hashed asset URLs after your normal build finishes.
4
+
5
+ ## What It Does
6
+
7
+ - Generates hashed `avif`, `webp`, and original-format outputs.
8
+ - Writes a source manifest that you can keep in version control and extend over time.
9
+ - Writes a generated runtime manifest plus helper wrapper for code-driven lookups.
10
+ - Rewrites static `html` and `css` asset references in `dist/` to hashed URLs.
11
+ - Keeps defaults lightweight: `sizes`, `quality`, `extension(s)`, and `ignore`.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @yadimon/ng-smart-images
17
+ ```
18
+
19
+ ## Source Manifest
20
+
21
+ Create a `smart-images.manifest.json` file in your app root.
22
+
23
+ ```json
24
+ {
25
+ "assetsRoot": "src/assets",
26
+ "generatedAssetsDir": "src/assets/ng-smart-images",
27
+ "publicPath": "/assets/ng-smart-images",
28
+ "runtimeManifestJsonPath": "src/app/generated/ng-smart-images.manifest.json",
29
+ "runtimeManifestTsPath": "src/app/generated/ng-smart-images.manifest.ts",
30
+ "runtimeHelperTsPath": "src/app/generated/ng-smart-images.runtime.ts",
31
+ "ignore": ["src/assets/archive/**", "src/assets/ng-smart-images/**"],
32
+ "defaults": {
33
+ "extensions": ["avif", "webp", "original"],
34
+ "quality": 76,
35
+ "sizes": []
36
+ },
37
+ "images": {
38
+ "src/assets/landing/hero.webp": {
39
+ "sizes": [640, 960, 1408]
40
+ },
41
+ "src/assets/providers/logo.png": {
42
+ "extensions": ["webp", "original"]
43
+ },
44
+ "src/assets/legacy/old-banner.jpg": {
45
+ "ignore": true
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ ## CLI Commands
52
+
53
+ ### Sync Manifest
54
+
55
+ Scans `assetsRoot` and adds missing image entries without deleting existing config.
56
+
57
+ ```bash
58
+ npx ng-smart-images sync-manifest
59
+ ```
60
+
61
+ If you want a full refresh, delete `smart-images.manifest.json` and run the command again.
62
+
63
+ ### Generate Hashed Assets
64
+
65
+ Generates hashed files into `generatedAssetsDir` and writes:
66
+
67
+ - `runtimeManifestJsonPath`
68
+ - `runtimeManifestTsPath`
69
+ - `runtimeHelperTsPath`
70
+
71
+ ```bash
72
+ npx ng-smart-images generate-hashed
73
+ ```
74
+
75
+ ### Update Bundle
76
+
77
+ Runs after your normal build and rewrites static `html` and `css` references in `dist`.
78
+
79
+ ```bash
80
+ npx ng-smart-images update-bundle --dist dist/app/browser
81
+ ```
82
+
83
+ ## Suggested Angular Scripts
84
+
85
+ Keep Angular on the standard builders and add the image steps around it:
86
+
87
+ ```json
88
+ {
89
+ "scripts": {
90
+ "smart-images:sync-manifest": "npx ng-smart-images sync-manifest",
91
+ "smart-images:generate": "npx ng-smart-images generate-hashed",
92
+ "smart-images:update-bundle": "npx ng-smart-images update-bundle --dist dist/app/browser",
93
+ "build": "npm run smart-images:generate && ng build && npm run smart-images:update-bundle",
94
+ "start": "npm run smart-images:generate && ng serve"
95
+ }
96
+ }
97
+ ```
98
+
99
+ ## Runtime Helper
100
+
101
+ After `generate-hashed`, import the generated helper from your app:
102
+
103
+ ```ts
104
+ import { hashed, imageEntry } from './generated/ng-smart-images.runtime';
105
+
106
+ const heroUrl = hashed('src/assets/landing/hero.webp');
107
+ const heroEntry = imageEntry('src/assets/landing/hero.webp');
108
+ ```
109
+
110
+ Behavior:
111
+
112
+ - known image in the generated manifest: returns the hashed URL
113
+ - missing image or missing manifest entry: falls back to the original `/assets/...` path
114
+
115
+ ## Angular Wrapper
116
+
117
+ The package also exports an optional Angular wrapper layer:
118
+
119
+ ```ts
120
+ import { provideSmartImages } from '@yadimon/ng-smart-images/angular';
121
+ import manifest from './generated/ng-smart-images.manifest';
122
+
123
+ providers: [provideSmartImages(manifest)];
124
+ ```
125
+
126
+ You can then inject `SmartImagesService` if you prefer DI over direct helper functions.
127
+
128
+ ## Development
129
+
130
+ From the repository root:
131
+
132
+ ```bash
133
+ npm install
134
+ npm run verify
135
+ ```
136
+
137
+ ## Release Notes For Maintainers
138
+
139
+ Repository-wide release guidance lives in [`RELEASING.md`](../../RELEASING.md). The short version:
140
+
141
+ - keep the package on semver
142
+ - use `npm run publish:dry-run` before a real publish
143
+ - bump versions with the root `version:*` scripts
144
+ - prefer GitHub trusted publishing once the repository is connected on npm
@@ -0,0 +1,18 @@
1
+ import { InjectionToken, type ModuleWithProviders, type Provider } from '@angular/core';
2
+ import type { SmartImagesManifest } from '../runtime/manifest.js';
3
+ import { type SmartImageFormat, type SmartImageSource } from '../runtime/index.js';
4
+ export declare const SMART_IMAGES_MANIFEST: InjectionToken<SmartImagesManifest>;
5
+ export declare function provideSmartImages(manifest: SmartImagesManifest): Provider[];
6
+ export declare class SmartImagesService {
7
+ private readonly manifest;
8
+ private readonly resolver;
9
+ constructor();
10
+ hasImage(path: string): boolean;
11
+ hashed(path: string): string;
12
+ imageEntry(path: string): import("../runtime/manifest.js").SmartImageEntry | null;
13
+ imagePlaceholder(path: string): string;
14
+ imageSources(path: string, format?: SmartImageFormat): SmartImageSource[];
15
+ }
16
+ export declare class SmartImagesModule {
17
+ static forManifest(manifest: SmartImagesManifest): ModuleWithProviders<SmartImagesModule>;
18
+ }
@@ -0,0 +1,105 @@
1
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
4
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
5
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
6
+ var _, done = false;
7
+ for (var i = decorators.length - 1; i >= 0; i--) {
8
+ var context = {};
9
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
10
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
11
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
12
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
13
+ if (kind === "accessor") {
14
+ if (result === void 0) continue;
15
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
16
+ if (_ = accept(result.get)) descriptor.get = _;
17
+ if (_ = accept(result.set)) descriptor.set = _;
18
+ if (_ = accept(result.init)) initializers.unshift(_);
19
+ }
20
+ else if (_ = accept(result)) {
21
+ if (kind === "field") initializers.unshift(_);
22
+ else descriptor[key] = _;
23
+ }
24
+ }
25
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
26
+ done = true;
27
+ };
28
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
29
+ var useValue = arguments.length > 2;
30
+ for (var i = 0; i < initializers.length; i++) {
31
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
+ }
33
+ return useValue ? value : void 0;
34
+ };
35
+ import { inject, Injectable, InjectionToken, NgModule, } from '@angular/core';
36
+ import { EMPTY_SMART_IMAGES_MANIFEST } from '../runtime/manifest.js';
37
+ import { createSmartImageResolver, } from '../runtime/index.js';
38
+ export const SMART_IMAGES_MANIFEST = new InjectionToken('SMART_IMAGES_MANIFEST', {
39
+ factory: () => EMPTY_SMART_IMAGES_MANIFEST,
40
+ });
41
+ export function provideSmartImages(manifest) {
42
+ return [{ provide: SMART_IMAGES_MANIFEST, useValue: manifest }];
43
+ }
44
+ let SmartImagesService = (() => {
45
+ let _classDecorators = [Injectable({ providedIn: 'root' })];
46
+ let _classDescriptor;
47
+ let _classExtraInitializers = [];
48
+ let _classThis;
49
+ var SmartImagesService = class {
50
+ static { _classThis = this; }
51
+ static {
52
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
53
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
54
+ SmartImagesService = _classThis = _classDescriptor.value;
55
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
56
+ __runInitializers(_classThis, _classExtraInitializers);
57
+ }
58
+ manifest = inject(SMART_IMAGES_MANIFEST);
59
+ resolver;
60
+ constructor() {
61
+ this.resolver = createSmartImageResolver(this.manifest);
62
+ }
63
+ hasImage(path) {
64
+ return this.resolver.hasImage(path);
65
+ }
66
+ hashed(path) {
67
+ return this.resolver.hashed(path);
68
+ }
69
+ imageEntry(path) {
70
+ return this.resolver.imageEntry(path);
71
+ }
72
+ imagePlaceholder(path) {
73
+ return this.resolver.imagePlaceholder(path);
74
+ }
75
+ imageSources(path, format) {
76
+ return this.resolver.imageSources(path, format);
77
+ }
78
+ };
79
+ return SmartImagesService = _classThis;
80
+ })();
81
+ export { SmartImagesService };
82
+ let SmartImagesModule = (() => {
83
+ let _classDecorators = [NgModule({})];
84
+ let _classDescriptor;
85
+ let _classExtraInitializers = [];
86
+ let _classThis;
87
+ var SmartImagesModule = class {
88
+ static { _classThis = this; }
89
+ static {
90
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
91
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
92
+ SmartImagesModule = _classThis = _classDescriptor.value;
93
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
94
+ __runInitializers(_classThis, _classExtraInitializers);
95
+ }
96
+ static forManifest(manifest) {
97
+ return {
98
+ ngModule: SmartImagesModule,
99
+ providers: provideSmartImages(manifest),
100
+ };
101
+ }
102
+ };
103
+ return SmartImagesModule = _classThis;
104
+ })();
105
+ export { SmartImagesModule };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import { generateHashedImages } from './core/generate.js';
4
+ import { syncProjectManifest } from './core/manifest-sync.js';
5
+ import { updateBundle } from './core/update-bundle.js';
6
+ async function main() {
7
+ const parsed = parseArgs(process.argv.slice(2));
8
+ switch (parsed.command) {
9
+ case 'sync-manifest': {
10
+ const result = await syncProjectManifest({
11
+ cwd: parsed.flags.cwd,
12
+ manifestPath: parsed.flags.manifest,
13
+ });
14
+ console.log(`synced ${result.added.length} image entries into ${relativeOrAbsolute(result.manifestPath)}`);
15
+ return;
16
+ }
17
+ case 'generate-hashed': {
18
+ const result = await generateHashedImages({
19
+ cwd: parsed.flags.cwd,
20
+ manifestPath: parsed.flags.manifest,
21
+ });
22
+ console.log(`generated ${Object.keys(result.runtimeManifest).length} hashed image entries into ${relativeOrAbsolute(result.generatedAssetsDir)}`);
23
+ return;
24
+ }
25
+ case 'update-bundle': {
26
+ const distPath = parsed.flags.dist;
27
+ if (!distPath) {
28
+ throw new Error('Missing required --dist argument for update-bundle.');
29
+ }
30
+ const result = await updateBundle({
31
+ cwd: parsed.flags.cwd,
32
+ manifestPath: parsed.flags.manifest,
33
+ distPath,
34
+ });
35
+ console.log(`updated ${result.updatedFiles.length} bundle files and generated ${result.generated.length} additional images`);
36
+ return;
37
+ }
38
+ default:
39
+ printHelp();
40
+ process.exitCode = parsed.command ? 1 : 0;
41
+ }
42
+ }
43
+ function parseArgs(args) {
44
+ const [command = '', ...rest] = args;
45
+ const flags = {};
46
+ for (let index = 0; index < rest.length; index += 1) {
47
+ const value = rest[index];
48
+ if (!value.startsWith('--')) {
49
+ continue;
50
+ }
51
+ const key = value.slice('--'.length);
52
+ const nextValue = rest[index + 1];
53
+ if (nextValue && !nextValue.startsWith('--')) {
54
+ flags[key] = nextValue;
55
+ index += 1;
56
+ }
57
+ else {
58
+ flags[key] = 'true';
59
+ }
60
+ }
61
+ return {
62
+ command,
63
+ flags,
64
+ };
65
+ }
66
+ function relativeOrAbsolute(value) {
67
+ const cwd = process.cwd();
68
+ const relative = path.relative(cwd, value);
69
+ return relative && !relative.startsWith('..') ? relative : value;
70
+ }
71
+ function printHelp() {
72
+ console.log(`ng-smart-images
73
+
74
+ Usage:
75
+ ng-smart-images sync-manifest [--manifest smart-images.manifest.json] [--cwd path]
76
+ ng-smart-images generate-hashed [--manifest smart-images.manifest.json] [--cwd path]
77
+ ng-smart-images update-bundle --dist dist/app/browser [--manifest smart-images.manifest.json] [--cwd path]
78
+ `);
79
+ }
80
+ void main().catch((error) => {
81
+ const message = error instanceof Error ? error.message : String(error);
82
+ console.error(message);
83
+ process.exitCode = 1;
84
+ });
@@ -0,0 +1,9 @@
1
+ import type { SmartImageProjectManifest, SmartImageResolvedConfig, SmartImageResolvedEntry } from './types.js';
2
+ export interface LoadedConfig {
3
+ manifestPath: string;
4
+ config: SmartImageResolvedConfig;
5
+ }
6
+ export declare function loadProjectConfig(cwd: string, manifestPathInput?: string): Promise<LoadedConfig>;
7
+ export declare function saveProjectManifest(manifestPath: string, config: SmartImageProjectManifest): Promise<void>;
8
+ export declare function resolveEntryConfig(sourceKey: string, config: SmartImageResolvedConfig, cwd: string): SmartImageResolvedEntry | null;
9
+ export declare function createDefaultProjectManifest(): SmartImageProjectManifest;
@@ -0,0 +1,130 @@
1
+ import { access, readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { constants } from 'node:fs';
4
+ import { normalizeSourceKey } from './discovery.js';
5
+ import { toPosixPath, writeTextFile } from '../utils/fs.js';
6
+ const DEFAULT_MANIFEST_FILE = 'smart-images.manifest.json';
7
+ const DEFAULT_PUBLIC_PATH = '/assets/ng-smart-images';
8
+ const DEFAULT_ASSETS_ROOT = 'src/assets';
9
+ const DEFAULT_GENERATED_ASSETS_DIR = 'src/assets/ng-smart-images';
10
+ const DEFAULT_RUNTIME_MANIFEST_JSON = 'src/app/generated/ng-smart-images.manifest.json';
11
+ const DEFAULT_RUNTIME_MANIFEST_TS = 'src/app/generated/ng-smart-images.manifest.ts';
12
+ const DEFAULT_RUNTIME_HELPER_TS = 'src/app/generated/ng-smart-images.runtime.ts';
13
+ const DEFAULT_QUALITY = 76;
14
+ const DEFAULT_EXTENSIONS = ['avif', 'webp', 'original'];
15
+ export async function loadProjectConfig(cwd, manifestPathInput) {
16
+ const manifestPath = resolveManifestPath(cwd, manifestPathInput);
17
+ const raw = await readProjectManifest(manifestPath);
18
+ const config = {
19
+ assetsRoot: normalizeProjectPath(raw.assetsRoot ?? DEFAULT_ASSETS_ROOT),
20
+ generatedAssetsDir: normalizeProjectPath(raw.generatedAssetsDir ?? DEFAULT_GENERATED_ASSETS_DIR),
21
+ publicPath: normalizePublicPath(raw.publicPath ?? DEFAULT_PUBLIC_PATH),
22
+ runtimeManifestJsonPath: normalizeProjectPath(raw.runtimeManifestJsonPath ?? DEFAULT_RUNTIME_MANIFEST_JSON),
23
+ runtimeManifestTsPath: normalizeProjectPath(raw.runtimeManifestTsPath ?? DEFAULT_RUNTIME_MANIFEST_TS),
24
+ runtimeHelperTsPath: normalizeProjectPath(raw.runtimeHelperTsPath ?? DEFAULT_RUNTIME_HELPER_TS),
25
+ ignore: normalizeIgnore(raw.ignore ?? [], raw.generatedAssetsDir ?? DEFAULT_GENERATED_ASSETS_DIR),
26
+ defaults: {
27
+ ignore: false,
28
+ sizes: normalizeSizes(raw.defaults?.sizes),
29
+ quality: normalizeQuality(raw.defaults?.quality),
30
+ extensions: normalizeExtensions(raw.defaults),
31
+ },
32
+ images: normalizeImageConfigMap(raw.images ?? {}, cwd),
33
+ };
34
+ return {
35
+ manifestPath,
36
+ config,
37
+ };
38
+ }
39
+ export async function saveProjectManifest(manifestPath, config) {
40
+ await writeTextFile(manifestPath, `${JSON.stringify(config, null, 2)}\n`);
41
+ }
42
+ export function resolveEntryConfig(sourceKey, config, cwd) {
43
+ const normalizedKey = normalizeSourceKey(sourceKey, cwd);
44
+ const entry = config.images[normalizedKey];
45
+ if (entry?.ignore) {
46
+ return null;
47
+ }
48
+ return {
49
+ sourceKey: normalizedKey,
50
+ sourcePath: path.join(cwd, normalizedKey),
51
+ sizes: normalizeSizes(entry?.sizes, config.defaults.sizes),
52
+ quality: normalizeQuality(entry?.quality, config.defaults.quality),
53
+ extensions: normalizeExtensions(entry, config.defaults.extensions),
54
+ };
55
+ }
56
+ export function createDefaultProjectManifest() {
57
+ return {
58
+ assetsRoot: DEFAULT_ASSETS_ROOT,
59
+ generatedAssetsDir: DEFAULT_GENERATED_ASSETS_DIR,
60
+ publicPath: DEFAULT_PUBLIC_PATH,
61
+ runtimeManifestJsonPath: DEFAULT_RUNTIME_MANIFEST_JSON,
62
+ runtimeManifestTsPath: DEFAULT_RUNTIME_MANIFEST_TS,
63
+ runtimeHelperTsPath: DEFAULT_RUNTIME_HELPER_TS,
64
+ ignore: ['src/assets/archive/**', 'src/assets/ng-smart-images/**'],
65
+ defaults: {
66
+ quality: DEFAULT_QUALITY,
67
+ extensions: [...DEFAULT_EXTENSIONS],
68
+ sizes: [],
69
+ },
70
+ images: {},
71
+ };
72
+ }
73
+ async function readProjectManifest(manifestPath) {
74
+ try {
75
+ await access(manifestPath, constants.F_OK);
76
+ }
77
+ catch {
78
+ return createDefaultProjectManifest();
79
+ }
80
+ const raw = await readFile(manifestPath, 'utf8');
81
+ if (!raw.trim()) {
82
+ return createDefaultProjectManifest();
83
+ }
84
+ return JSON.parse(raw);
85
+ }
86
+ function resolveManifestPath(cwd, manifestPathInput) {
87
+ return path.resolve(cwd, manifestPathInput ?? DEFAULT_MANIFEST_FILE);
88
+ }
89
+ function normalizeProjectPath(value) {
90
+ return toPosixPath(value).replace(/^\.\/+/, '');
91
+ }
92
+ function normalizePublicPath(value) {
93
+ const normalized = normalizeProjectPath(value);
94
+ return normalized.startsWith('/') ? normalized : `/${normalized}`;
95
+ }
96
+ function normalizeIgnore(values, generatedAssetsDir) {
97
+ return Array.from(new Set([
98
+ ...values,
99
+ normalizeProjectPath(generatedAssetsDir),
100
+ `${normalizeProjectPath(generatedAssetsDir)}/**`,
101
+ ]
102
+ .map((value) => normalizeProjectPath(value))
103
+ .filter((value) => value.length > 0)));
104
+ }
105
+ function normalizeImageConfigMap(input, cwd) {
106
+ return Object.fromEntries(Object.entries(input).map(([key, value]) => [normalizeSourceKey(key, cwd), value]));
107
+ }
108
+ function normalizeSizes(input, fallback = []) {
109
+ if (!Array.isArray(input) || input.length === 0) {
110
+ return [...fallback];
111
+ }
112
+ return Array.from(new Set(input.filter((value) => Number.isFinite(value) && value > 0))).sort((left, right) => left - right);
113
+ }
114
+ function normalizeQuality(input, fallback = DEFAULT_QUALITY) {
115
+ if (typeof input !== 'number' || !Number.isFinite(input)) {
116
+ return fallback;
117
+ }
118
+ return Math.max(1, Math.min(100, Math.round(input)));
119
+ }
120
+ function normalizeExtensions(input, fallback = DEFAULT_EXTENSIONS) {
121
+ const source = Array.isArray(input?.extensions)
122
+ ? input.extensions
123
+ : input?.extension
124
+ ? Array.isArray(input.extension)
125
+ ? input.extension
126
+ : [input.extension]
127
+ : fallback;
128
+ const normalized = source.filter((value) => value === 'avif' || value === 'webp' || value === 'original');
129
+ return normalized.length > 0 ? Array.from(new Set(normalized)) : [...fallback];
130
+ }
@@ -0,0 +1,5 @@
1
+ export declare function isSupportedImagePath(value: string): boolean;
2
+ export declare function normalizeSourceKey(value: string, cwd: string): string;
3
+ export declare function isIgnored(value: string, ignorePatterns: string[]): boolean;
4
+ export declare function resolveAssetUrlToSourceKey(assetUrl: string, assetsRoot: string, cwd: string): string | null;
5
+ export declare function isStaticLocalAssetReference(value: string): boolean;
@@ -0,0 +1,38 @@
1
+ import path from 'node:path';
2
+ import picomatch from 'picomatch';
3
+ import { toPosixPath } from '../utils/fs.js';
4
+ const IMAGE_EXTENSION_PATTERN = /\.(avif|gif|jpe?g|png|webp)$/i;
5
+ export function isSupportedImagePath(value) {
6
+ return IMAGE_EXTENSION_PATTERN.test(value);
7
+ }
8
+ export function normalizeSourceKey(value, cwd) {
9
+ const normalized = toPosixPath(path.isAbsolute(value) ? path.relative(cwd, value) : value);
10
+ return normalized.replace(/^\.\/+/, '');
11
+ }
12
+ export function isIgnored(value, ignorePatterns) {
13
+ const normalized = toPosixPath(value);
14
+ return ignorePatterns.some((pattern) => picomatch(pattern)(normalized));
15
+ }
16
+ export function resolveAssetUrlToSourceKey(assetUrl, assetsRoot, cwd) {
17
+ const normalized = assetUrl.trim();
18
+ if (!isStaticLocalAssetReference(normalized)) {
19
+ return null;
20
+ }
21
+ const relativeAssetPath = normalized
22
+ .replace(/^\/+/, '')
23
+ .replace(/^assets\//, '')
24
+ .replace(/^\/assets\//, '');
25
+ return normalizeSourceKey(path.join(assetsRoot, relativeAssetPath), cwd);
26
+ }
27
+ export function isStaticLocalAssetReference(value) {
28
+ if (!value || value.includes('{{')) {
29
+ return false;
30
+ }
31
+ if (/^(https?:)?\/\//i.test(value)) {
32
+ return false;
33
+ }
34
+ if (/^(data:|blob:|#)/i.test(value)) {
35
+ return false;
36
+ }
37
+ return isSupportedImagePath(value);
38
+ }
@@ -0,0 +1,2 @@
1
+ import type { GenerateHashedOptions, GeneratedProjectArtifacts } from './types.js';
2
+ export declare function generateHashedImages(options?: GenerateHashedOptions): Promise<GeneratedProjectArtifacts>;
@@ -0,0 +1,46 @@
1
+ import path from 'node:path';
2
+ import { loadProjectConfig, resolveEntryConfig } from './config.js';
3
+ import { generateImageArtifact } from './optimize.js';
4
+ import { writeTextFile } from '../utils/fs.js';
5
+ export async function generateHashedImages(options = {}) {
6
+ const cwd = path.resolve(options.cwd ?? process.cwd());
7
+ const { manifestPath, config } = await loadProjectConfig(cwd, options.manifestPath);
8
+ const runtimeManifest = {};
9
+ const generatedAssetsDir = path.join(cwd, config.generatedAssetsDir);
10
+ const assetsRootAbsolute = path.join(cwd, config.assetsRoot);
11
+ for (const sourceKey of Object.keys(config.images).sort()) {
12
+ const resolvedEntry = resolveEntryConfig(sourceKey, config, cwd);
13
+ if (!resolvedEntry) {
14
+ continue;
15
+ }
16
+ const artifact = await generateImageArtifact({
17
+ sourceKey,
18
+ sourcePath: resolvedEntry.sourcePath,
19
+ sourceRootPath: assetsRootAbsolute,
20
+ publicPath: config.publicPath,
21
+ outputRoot: generatedAssetsDir,
22
+ entryConfig: resolvedEntry,
23
+ });
24
+ runtimeManifest[sourceKey] = artifact.entry;
25
+ }
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
+ await writeTextFile(runtimeManifestJsonPath, `${JSON.stringify(runtimeManifest, null, 2)}\n`);
30
+ await writeTextFile(runtimeManifestTsPath, buildRuntimeManifestModule(runtimeManifest));
31
+ await writeTextFile(runtimeHelperTsPath, buildRuntimeHelperModule(path.basename(runtimeManifestTsPath, '.ts')));
32
+ return {
33
+ manifestPath,
34
+ runtimeManifestJsonPath,
35
+ runtimeManifestTsPath,
36
+ runtimeHelperTsPath,
37
+ generatedAssetsDir,
38
+ runtimeManifest,
39
+ };
40
+ }
41
+ function buildRuntimeManifestModule(runtimeManifest) {
42
+ return `import type { SmartImagesManifest } from '@yadimon/ng-smart-images/runtime';\n\nconst manifest: SmartImagesManifest = ${JSON.stringify(runtimeManifest, null, 2)};\n\nexport default manifest;\n`;
43
+ }
44
+ function buildRuntimeHelperModule(runtimeManifestBaseName) {
45
+ 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
+ }
@@ -0,0 +1,5 @@
1
+ import type { GenerateHashedOptions } from './types.js';
2
+ export declare function syncProjectManifest(options?: GenerateHashedOptions): Promise<{
3
+ manifestPath: string;
4
+ added: string[];
5
+ }>;
@@ -0,0 +1,45 @@
1
+ import path from 'node:path';
2
+ import { loadProjectConfig, saveProjectManifest } from './config.js';
3
+ import { isIgnored, isSupportedImagePath, normalizeSourceKey } from './discovery.js';
4
+ import { listFilesRecursive } from '../utils/fs.js';
5
+ export async function syncProjectManifest(options = {}) {
6
+ const cwd = path.resolve(options.cwd ?? process.cwd());
7
+ const { manifestPath, config } = await loadProjectConfig(cwd, options.manifestPath);
8
+ const assetsRootAbsolute = path.join(cwd, config.assetsRoot);
9
+ const assetFiles = await listFilesRecursive(assetsRootAbsolute);
10
+ const existingImages = { ...config.images };
11
+ const added = [];
12
+ for (const assetFile of assetFiles) {
13
+ const normalizedKey = normalizeSourceKey(assetFile, cwd);
14
+ if (!isSupportedImagePath(normalizedKey)) {
15
+ continue;
16
+ }
17
+ if (isIgnored(normalizedKey, config.ignore)) {
18
+ continue;
19
+ }
20
+ if (!(normalizedKey in existingImages)) {
21
+ existingImages[normalizedKey] = {};
22
+ added.push(normalizedKey);
23
+ }
24
+ }
25
+ const sourceManifest = {
26
+ assetsRoot: config.assetsRoot,
27
+ generatedAssetsDir: config.generatedAssetsDir,
28
+ publicPath: config.publicPath,
29
+ runtimeManifestJsonPath: config.runtimeManifestJsonPath,
30
+ runtimeManifestTsPath: config.runtimeManifestTsPath,
31
+ runtimeHelperTsPath: config.runtimeHelperTsPath,
32
+ ignore: [...config.ignore],
33
+ defaults: {
34
+ quality: config.defaults.quality,
35
+ sizes: [...config.defaults.sizes],
36
+ extensions: [...config.defaults.extensions],
37
+ },
38
+ images: Object.fromEntries(Object.entries(existingImages).sort(([left], [right]) => left.localeCompare(right))),
39
+ };
40
+ await saveProjectManifest(manifestPath, sourceManifest);
41
+ return {
42
+ manifestPath,
43
+ added,
44
+ };
45
+ }
@@ -0,0 +1,13 @@
1
+ import type { SmartImagesManifest } from '../runtime/manifest.js';
2
+ import type { SmartImageResolvedEntry } from './types.js';
3
+ export interface GeneratedImageArtifact {
4
+ entry: SmartImagesManifest[string];
5
+ }
6
+ export declare function generateImageArtifact(input: {
7
+ sourceKey: string;
8
+ sourcePath: string;
9
+ sourceRootPath: string;
10
+ publicPath: string;
11
+ outputRoot: string;
12
+ entryConfig: SmartImageResolvedEntry;
13
+ }): Promise<GeneratedImageArtifact>;
@@ -0,0 +1,114 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import sharp from 'sharp';
5
+ import { toPosixPath } from '../utils/fs.js';
6
+ export async function generateImageArtifact(input) {
7
+ const metadata = await sharp(input.sourcePath, { failOn: 'error' }).metadata();
8
+ if (!metadata.width || !metadata.height || !metadata.format) {
9
+ throw new Error(`Cannot determine image metadata for ${input.sourcePath}`);
10
+ }
11
+ const outputWidths = normalizeOutputWidths(input.entryConfig.sizes, metadata.width);
12
+ const relativeSourcePath = toPosixPath(path.relative(input.sourceRootPath, input.sourcePath));
13
+ const relativeDir = path.dirname(relativeSourcePath) === '.' ? '' : path.dirname(relativeSourcePath);
14
+ const baseName = path.parse(relativeSourcePath).name;
15
+ const originalExtension = metadata.format === 'jpeg' ? 'jpg' : metadata.format;
16
+ const sources = [];
17
+ const generatedOutputExtensions = new Set();
18
+ for (const extension of input.entryConfig.extensions) {
19
+ const effectiveExtension = extension === 'original' ? (originalExtension ?? 'png') : extension;
20
+ if (generatedOutputExtensions.has(effectiveExtension)) {
21
+ continue;
22
+ }
23
+ const generatedSources = await buildSourcesForExtension({
24
+ extension,
25
+ widths: outputWidths,
26
+ quality: input.entryConfig.quality,
27
+ sourcePath: input.sourcePath,
28
+ relativeDir,
29
+ baseName,
30
+ outputRoot: input.outputRoot,
31
+ publicPath: input.publicPath,
32
+ });
33
+ sources.push(...generatedSources);
34
+ generatedOutputExtensions.add(effectiveExtension);
35
+ }
36
+ const fallbackSrc = pickFirstSourceByFormat(sources, 'original')?.src ??
37
+ pickFirstSourceByFormat(sources, 'webp')?.src ??
38
+ sources[0]?.src ??
39
+ '';
40
+ return {
41
+ entry: {
42
+ key: input.sourceKey,
43
+ originalPath: input.sourceKey,
44
+ width: metadata.width,
45
+ height: metadata.height,
46
+ fallbackSrc,
47
+ placeholderDataUrl: '',
48
+ sources,
49
+ },
50
+ };
51
+ }
52
+ async function buildSourcesForExtension(input) {
53
+ const metadata = await sharp(input.sourcePath, { failOn: 'error' }).metadata();
54
+ const originalExtension = metadata.format === 'jpeg' ? 'jpg' : (metadata.format ?? 'png');
55
+ const outputs = [];
56
+ for (const width of input.widths) {
57
+ const transformer = sharp(input.sourcePath, { failOn: 'error' }).resize({
58
+ width,
59
+ withoutEnlargement: true,
60
+ });
61
+ let fileExtension = originalExtension;
62
+ let buffer;
63
+ if (input.extension === 'avif') {
64
+ fileExtension = 'avif';
65
+ buffer = await transformer.avif({ quality: input.quality }).toBuffer();
66
+ }
67
+ else if (input.extension === 'webp') {
68
+ fileExtension = 'webp';
69
+ buffer = await transformer.webp({ quality: input.quality }).toBuffer();
70
+ }
71
+ else {
72
+ buffer = await toOriginalFormatBuffer(transformer, fileExtension, input.quality);
73
+ }
74
+ const hash = createHash('sha256').update(buffer).digest('hex').slice(0, 10);
75
+ const widthSuffix = width > 0 ? `-${width}` : '';
76
+ const fileName = `${input.baseName}${widthSuffix}-${hash}.${fileExtension}`;
77
+ const targetPath = path.join(input.outputRoot, input.relativeDir, fileName);
78
+ await mkdir(path.dirname(targetPath), { recursive: true });
79
+ await writeFile(targetPath, buffer);
80
+ outputs.push({
81
+ format: input.extension,
82
+ width,
83
+ src: `${input.publicPath}/${toPosixPath(path.posix.join(input.relativeDir, fileName)).replace(/^\/+/, '')}`,
84
+ });
85
+ }
86
+ return outputs;
87
+ }
88
+ async function toOriginalFormatBuffer(transformer, extension, quality) {
89
+ switch (extension) {
90
+ case 'jpg':
91
+ case 'jpeg':
92
+ return transformer.jpeg({ quality }).toBuffer();
93
+ case 'png':
94
+ return transformer.png({ quality }).toBuffer();
95
+ case 'webp':
96
+ return transformer.webp({ quality }).toBuffer();
97
+ case 'avif':
98
+ return transformer.avif({ quality }).toBuffer();
99
+ default:
100
+ return transformer.toFormat(extension).toBuffer();
101
+ }
102
+ }
103
+ function normalizeOutputWidths(configuredSizes, sourceWidth) {
104
+ const filtered = configuredSizes.filter((value) => value > 0 && value <= sourceWidth);
105
+ if (filtered.length === 0) {
106
+ return [sourceWidth];
107
+ }
108
+ return Array.from(new Set(filtered)).sort((left, right) => left - right);
109
+ }
110
+ function pickFirstSourceByFormat(sources, format) {
111
+ return ([...sources]
112
+ .filter((entry) => entry.format === format)
113
+ .sort((left, right) => right.width - left.width)[0] ?? null);
114
+ }
@@ -0,0 +1,56 @@
1
+ import type { SmartImageFormat, SmartImagesManifest } from '../runtime/manifest.js';
2
+ export interface SmartImageConfigEntry {
3
+ ignore?: boolean;
4
+ sizes?: number[];
5
+ quality?: number;
6
+ extension?: SmartImageFormat | SmartImageFormat[];
7
+ extensions?: SmartImageFormat[];
8
+ }
9
+ export interface SmartImageProjectManifest {
10
+ assetsRoot?: string;
11
+ generatedAssetsDir?: string;
12
+ publicPath?: string;
13
+ runtimeManifestJsonPath?: string;
14
+ runtimeManifestTsPath?: string;
15
+ runtimeHelperTsPath?: string;
16
+ ignore?: string[];
17
+ defaults?: SmartImageConfigEntry;
18
+ images?: Record<string, SmartImageConfigEntry>;
19
+ }
20
+ export interface SmartImageResolvedConfig {
21
+ assetsRoot: string;
22
+ generatedAssetsDir: string;
23
+ publicPath: string;
24
+ runtimeManifestJsonPath: string;
25
+ runtimeManifestTsPath: string;
26
+ runtimeHelperTsPath: string;
27
+ ignore: string[];
28
+ defaults: Required<Pick<SmartImageConfigEntry, 'sizes' | 'quality' | 'extensions'>> & {
29
+ ignore: false;
30
+ };
31
+ images: Record<string, SmartImageConfigEntry>;
32
+ }
33
+ export interface SmartImageResolvedEntry {
34
+ sourceKey: string;
35
+ sourcePath: string;
36
+ sizes: number[];
37
+ quality: number;
38
+ extensions: SmartImageFormat[];
39
+ }
40
+ export interface GenerateHashedOptions {
41
+ cwd?: string;
42
+ manifestPath?: string;
43
+ }
44
+ export interface GeneratedProjectArtifacts {
45
+ manifestPath: string;
46
+ runtimeManifestJsonPath: string;
47
+ runtimeManifestTsPath: string;
48
+ runtimeHelperTsPath: string;
49
+ generatedAssetsDir: string;
50
+ runtimeManifest: SmartImagesManifest;
51
+ }
52
+ export interface UpdateBundleOptions {
53
+ cwd?: string;
54
+ manifestPath?: string;
55
+ distPath: string;
56
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import type { UpdateBundleOptions } from './types.js';
2
+ export declare function updateBundle(options: UpdateBundleOptions): Promise<{
3
+ updatedFiles: string[];
4
+ generated: string[];
5
+ }>;
@@ -0,0 +1,120 @@
1
+ import { access, readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { constants } from 'node:fs';
4
+ import { loadProjectConfig, resolveEntryConfig } from './config.js';
5
+ import { generateImageArtifact } from './optimize.js';
6
+ import { isStaticLocalAssetReference, normalizeSourceKey, resolveAssetUrlToSourceKey, } from './discovery.js';
7
+ import { listFilesRecursive, writeTextFile } from '../utils/fs.js';
8
+ const OUTPUT_FILE_PATTERN = /\.(css|html)$/i;
9
+ export async function updateBundle(options) {
10
+ const cwd = path.resolve(options.cwd ?? process.cwd());
11
+ const distPath = path.resolve(cwd, options.distPath);
12
+ const { config } = await loadProjectConfig(cwd, options.manifestPath);
13
+ const manifestCache = await loadGeneratedRuntimeManifest(cwd, config.runtimeManifestJsonPath);
14
+ const generatedDistFiles = new Set();
15
+ const ensureEntry = async (sourceKey) => {
16
+ const normalizedKey = normalizeSourceKey(sourceKey, cwd);
17
+ const existing = manifestCache[normalizedKey];
18
+ if (existing) {
19
+ return existing;
20
+ }
21
+ const resolvedEntry = resolveEntryConfig(normalizedKey, config, cwd);
22
+ if (!resolvedEntry) {
23
+ return null;
24
+ }
25
+ try {
26
+ await access(resolvedEntry.sourcePath, constants.F_OK);
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ const artifact = await generateImageArtifact({
32
+ sourceKey: normalizedKey,
33
+ sourcePath: resolvedEntry.sourcePath,
34
+ sourceRootPath: path.join(cwd, config.assetsRoot),
35
+ publicPath: config.publicPath,
36
+ outputRoot: path.join(distPath, config.publicPath.replace(/^\/+/, '')),
37
+ entryConfig: resolvedEntry,
38
+ });
39
+ manifestCache[normalizedKey] = artifact.entry;
40
+ generatedDistFiles.add(normalizedKey);
41
+ return artifact.entry;
42
+ };
43
+ const files = await listFilesRecursive(distPath);
44
+ const updatedFiles = [];
45
+ for (const filePath of files) {
46
+ if (!OUTPUT_FILE_PATTERN.test(filePath)) {
47
+ continue;
48
+ }
49
+ const original = await readFile(filePath, 'utf8');
50
+ const rewritten = path.extname(filePath).toLowerCase() === '.css'
51
+ ? await rewriteCssContent(original, ensureEntry, config.assetsRoot, cwd)
52
+ : await rewriteHtmlContent(original, ensureEntry, config.assetsRoot, cwd);
53
+ if (rewritten !== original) {
54
+ await writeTextFile(filePath, rewritten);
55
+ updatedFiles.push(filePath);
56
+ }
57
+ }
58
+ return {
59
+ updatedFiles,
60
+ generated: [...generatedDistFiles].sort(),
61
+ };
62
+ }
63
+ async function rewriteHtmlContent(content, ensureEntry, assetsRoot, cwd) {
64
+ return replaceAsync(content, /(['"])(\/?assets\/[^'"]+\.(?:avif|gif|jpe?g|png|webp))\1/gi, async (fullMatch, quote, assetUrl) => {
65
+ if (!isStaticLocalAssetReference(assetUrl)) {
66
+ return fullMatch;
67
+ }
68
+ const sourceKey = resolveAssetUrlToSourceKey(assetUrl, assetsRoot, cwd);
69
+ if (!sourceKey) {
70
+ return fullMatch;
71
+ }
72
+ const entry = await ensureEntry(sourceKey);
73
+ return entry ? `${quote}${entry.fallbackSrc}${quote}` : fullMatch;
74
+ });
75
+ }
76
+ async function rewriteCssContent(content, ensureEntry, assetsRoot, cwd) {
77
+ return replaceAsync(content, /url\(\s*(?:'([^']+)'|"([^"]+)"|([^'")]+))\s*\)/gi, async (fullMatch, singleQuoted, doubleQuoted, bare) => {
78
+ const assetUrl = singleQuoted ?? doubleQuoted ?? bare ?? '';
79
+ if (!isStaticLocalAssetReference(assetUrl)) {
80
+ return fullMatch;
81
+ }
82
+ const sourceKey = resolveAssetUrlToSourceKey(assetUrl, assetsRoot, cwd);
83
+ if (!sourceKey) {
84
+ return fullMatch;
85
+ }
86
+ const entry = await ensureEntry(sourceKey);
87
+ return entry ? `url("${entry.fallbackSrc}")` : fullMatch;
88
+ });
89
+ }
90
+ async function replaceAsync(input, pattern, replacer) {
91
+ const matches = [...input.matchAll(pattern)];
92
+ if (matches.length === 0) {
93
+ return input;
94
+ }
95
+ let output = '';
96
+ let lastIndex = 0;
97
+ for (const match of matches) {
98
+ const fullMatch = match[0];
99
+ const matchIndex = match.index ?? 0;
100
+ output += input.slice(lastIndex, matchIndex);
101
+ output += await replacer(...match);
102
+ lastIndex = matchIndex + fullMatch.length;
103
+ }
104
+ output += input.slice(lastIndex);
105
+ return output;
106
+ }
107
+ async function loadGeneratedRuntimeManifest(cwd, runtimeManifestJsonPath) {
108
+ const filePath = path.join(cwd, runtimeManifestJsonPath);
109
+ try {
110
+ await access(filePath, constants.F_OK);
111
+ }
112
+ catch {
113
+ return {};
114
+ }
115
+ const raw = await readFile(filePath, 'utf8');
116
+ if (!raw.trim()) {
117
+ return {};
118
+ }
119
+ return JSON.parse(raw);
120
+ }
@@ -0,0 +1,8 @@
1
+ export { EMPTY_SMART_IMAGES_MANIFEST, resolveSmartImage, type SmartImageEntry, type SmartImageFormat, type SmartImageSource, type SmartImagesManifest, } from './runtime/manifest.js';
2
+ export { createSmartImageResolver, hashed, imageEntry, imagePlaceholder, imageSources, normalizeImageKey, toPublicAssetPath, type SmartImageResolver, } from './runtime/index.js';
3
+ export { SmartImagesModule, SMART_IMAGES_MANIFEST, SmartImagesService, provideSmartImages, } from './angular/index.js';
4
+ export { createDefaultProjectManifest, loadProjectConfig, resolveEntryConfig, saveProjectManifest, type LoadedConfig, } from './core/config.js';
5
+ export { generateHashedImages } from './core/generate.js';
6
+ export { syncProjectManifest } from './core/manifest-sync.js';
7
+ export { updateBundle } from './core/update-bundle.js';
8
+ export type { GenerateHashedOptions, GeneratedProjectArtifacts, SmartImageConfigEntry, SmartImageProjectManifest, SmartImageResolvedConfig, SmartImageResolvedEntry, UpdateBundleOptions, } from './core/types.js';
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { EMPTY_SMART_IMAGES_MANIFEST, resolveSmartImage, } from './runtime/manifest.js';
2
+ export { createSmartImageResolver, hashed, imageEntry, imagePlaceholder, imageSources, normalizeImageKey, toPublicAssetPath, } from './runtime/index.js';
3
+ export { SmartImagesModule, SMART_IMAGES_MANIFEST, SmartImagesService, provideSmartImages, } from './angular/index.js';
4
+ export { createDefaultProjectManifest, loadProjectConfig, resolveEntryConfig, saveProjectManifest, } from './core/config.js';
5
+ export { generateHashedImages } from './core/generate.js';
6
+ export { syncProjectManifest } from './core/manifest-sync.js';
7
+ export { updateBundle } from './core/update-bundle.js';
@@ -0,0 +1,2 @@
1
+ export { EMPTY_SMART_IMAGES_MANIFEST, resolveSmartImage, type SmartImageEntry, type SmartImageFormat, type SmartImageSource, type SmartImagesManifest, } from './manifest.js';
2
+ export { createSmartImageResolver, hashed, imageEntry, imagePlaceholder, imageSources, normalizeImageKey, toPublicAssetPath, type SmartImageResolver, } from './resolver.js';
@@ -0,0 +1,2 @@
1
+ export { EMPTY_SMART_IMAGES_MANIFEST, resolveSmartImage, } from './manifest.js';
2
+ export { createSmartImageResolver, hashed, imageEntry, imagePlaceholder, imageSources, normalizeImageKey, toPublicAssetPath, } from './resolver.js';
@@ -0,0 +1,19 @@
1
+ export type SmartImageFormat = 'avif' | 'webp' | 'original';
2
+ export interface SmartImageSource {
3
+ format: SmartImageFormat;
4
+ src: string;
5
+ width: number;
6
+ }
7
+ export interface SmartImageEntry {
8
+ key: string;
9
+ originalPath: string;
10
+ width: number;
11
+ height: number;
12
+ fallbackSrc: string;
13
+ placeholderDataUrl: string;
14
+ sources: ReadonlyArray<SmartImageSource>;
15
+ }
16
+ export type SmartImagesManifest = Record<string, SmartImageEntry>;
17
+ export declare const EMPTY_SMART_IMAGES_MANIFEST: SmartImagesManifest;
18
+ export declare function resolveSmartImage(key: string, manifest?: SmartImagesManifest): SmartImageEntry | null;
19
+ export default EMPTY_SMART_IMAGES_MANIFEST;
@@ -0,0 +1,5 @@
1
+ export const EMPTY_SMART_IMAGES_MANIFEST = {};
2
+ export function resolveSmartImage(key, manifest = EMPTY_SMART_IMAGES_MANIFEST) {
3
+ return manifest[key] ?? null;
4
+ }
5
+ export default EMPTY_SMART_IMAGES_MANIFEST;
@@ -0,0 +1,15 @@
1
+ import type { SmartImageEntry, SmartImageFormat, SmartImageSource, SmartImagesManifest } from './manifest.js';
2
+ export interface SmartImageResolver {
3
+ hasImage(path: string): boolean;
4
+ hashed(path: string): string;
5
+ imageEntry(path: string): SmartImageEntry | null;
6
+ imagePlaceholder(path: string): string;
7
+ imageSources(path: string, format?: SmartImageFormat): SmartImageSource[];
8
+ }
9
+ export declare function createSmartImageResolver(manifest?: SmartImagesManifest): SmartImageResolver;
10
+ export declare function hashed(path: string, manifest?: SmartImagesManifest): string;
11
+ export declare function imageEntry(path: string, manifest?: SmartImagesManifest): SmartImageEntry | null;
12
+ export declare function imagePlaceholder(path: string, manifest?: SmartImagesManifest): string;
13
+ export declare function imageSources(path: string, manifest?: SmartImagesManifest, format?: SmartImageFormat): SmartImageSource[];
14
+ export declare function normalizeImageKey(path: string): string;
15
+ export declare function toPublicAssetPath(path: string): string;
@@ -0,0 +1,58 @@
1
+ import { EMPTY_SMART_IMAGES_MANIFEST } from './manifest.js';
2
+ export function createSmartImageResolver(manifest = EMPTY_SMART_IMAGES_MANIFEST) {
3
+ return {
4
+ hasImage(path) {
5
+ return imageEntry(path, manifest) !== null;
6
+ },
7
+ hashed(path) {
8
+ return hashed(path, manifest);
9
+ },
10
+ imageEntry(path) {
11
+ return imageEntry(path, manifest);
12
+ },
13
+ imagePlaceholder(path) {
14
+ return imagePlaceholder(path, manifest);
15
+ },
16
+ imageSources(path, format) {
17
+ return imageSources(path, manifest, format);
18
+ },
19
+ };
20
+ }
21
+ export function hashed(path, manifest = EMPTY_SMART_IMAGES_MANIFEST) {
22
+ return imageEntry(path, manifest)?.fallbackSrc ?? toPublicAssetPath(path);
23
+ }
24
+ export function imageEntry(path, manifest = EMPTY_SMART_IMAGES_MANIFEST) {
25
+ return manifest[normalizeImageKey(path)] ?? null;
26
+ }
27
+ export function imagePlaceholder(path, manifest = EMPTY_SMART_IMAGES_MANIFEST) {
28
+ const entry = imageEntry(path, manifest);
29
+ return entry?.placeholderDataUrl || entry?.fallbackSrc || toPublicAssetPath(path);
30
+ }
31
+ export function imageSources(path, manifest = EMPTY_SMART_IMAGES_MANIFEST, format) {
32
+ const entry = imageEntry(path, manifest);
33
+ if (!entry) {
34
+ return [];
35
+ }
36
+ const sources = [...entry.sources].sort((left, right) => left.width - right.width);
37
+ return format ? sources.filter((source) => source.format === format) : sources;
38
+ }
39
+ export function normalizeImageKey(path) {
40
+ const normalized = path.trim().replace(/\\/g, '/');
41
+ if (normalized.startsWith('src/assets/')) {
42
+ return normalized;
43
+ }
44
+ if (normalized.startsWith('/assets/')) {
45
+ return `src${normalized}`;
46
+ }
47
+ if (normalized.startsWith('assets/')) {
48
+ return `src/${normalized}`;
49
+ }
50
+ return normalized;
51
+ }
52
+ export function toPublicAssetPath(path) {
53
+ const normalized = normalizeImageKey(path);
54
+ if (normalized.startsWith('src/')) {
55
+ return `/${normalized.slice('src/'.length)}`;
56
+ }
57
+ return normalized.startsWith('/') ? normalized : `/${normalized}`;
58
+ }
@@ -0,0 +1,4 @@
1
+ export declare function listFilesRecursive(rootDir: string): Promise<string[]>;
2
+ export declare function writeTextFile(targetPath: string, content: string): Promise<void>;
3
+ export declare function copyFileBytes(sourcePath: string, targetPath: string): Promise<void>;
4
+ export declare function toPosixPath(value: string): string;
@@ -0,0 +1,27 @@
1
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ export async function listFilesRecursive(rootDir) {
4
+ const entries = await readdir(rootDir, { withFileTypes: true });
5
+ const files = [];
6
+ for (const entry of entries) {
7
+ const fullPath = path.join(rootDir, entry.name);
8
+ if (entry.isDirectory()) {
9
+ files.push(...(await listFilesRecursive(fullPath)));
10
+ continue;
11
+ }
12
+ files.push(fullPath);
13
+ }
14
+ return files;
15
+ }
16
+ export async function writeTextFile(targetPath, content) {
17
+ await mkdir(path.dirname(targetPath), { recursive: true });
18
+ await writeFile(targetPath, content, 'utf8');
19
+ }
20
+ export async function copyFileBytes(sourcePath, targetPath) {
21
+ const bytes = await readFile(sourcePath);
22
+ await mkdir(path.dirname(targetPath), { recursive: true });
23
+ await writeFile(targetPath, bytes);
24
+ }
25
+ export function toPosixPath(value) {
26
+ return value.replace(/\\/g, '/');
27
+ }
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@yadimon/ng-smart-images",
3
+ "version": "0.1.0",
4
+ "description": "CLI-first smart image optimization with hashed asset generation, runtime manifests, and optional Angular helpers.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/yadimon/ng-smart-images.git"
10
+ },
11
+ "homepage": "https://github.com/yadimon/ng-smart-images#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/yadimon/ng-smart-images/issues"
14
+ },
15
+ "keywords": [
16
+ "angular",
17
+ "cli",
18
+ "images",
19
+ "avif",
20
+ "webp",
21
+ "performance"
22
+ ],
23
+ "main": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "bin": {
26
+ "ng-smart-images": "dist/cli.js"
27
+ },
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "default": "./dist/index.js"
32
+ },
33
+ "./runtime": {
34
+ "types": "./dist/runtime/index.d.ts",
35
+ "default": "./dist/runtime/index.js"
36
+ },
37
+ "./angular": {
38
+ "types": "./dist/angular/index.d.ts",
39
+ "default": "./dist/angular/index.js"
40
+ },
41
+ "./manifest": {
42
+ "types": "./dist/runtime/manifest.d.ts",
43
+ "default": "./dist/runtime/manifest.js"
44
+ },
45
+ "./package.json": "./package.json"
46
+ },
47
+ "files": [
48
+ "dist",
49
+ "README.md"
50
+ ],
51
+ "engines": {
52
+ "node": "^20.19.0 || ^22.14.0 || ^24.0.0"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "scripts": {
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",
60
+ "lint": "eslint src tests scripts",
61
+ "typecheck": "tsc -p tsconfig.json --noEmit",
62
+ "test": "npm run build && vitest run",
63
+ "test:integration": "npm run build && vitest run tests/integration.spec.ts"
64
+ },
65
+ "dependencies": {
66
+ "picomatch": "^4.0.3",
67
+ "sharp": "^0.34.5",
68
+ "tinyglobby": "^0.2.15"
69
+ },
70
+ "peerDependencies": {
71
+ "@angular/core": "^20.0.0 || ^21.0.0 || ^22.0.0"
72
+ },
73
+ "peerDependenciesMeta": {
74
+ "@angular/core": {
75
+ "optional": true
76
+ }
77
+ },
78
+ "devDependencies": {
79
+ "@angular/core": "^21.2.0",
80
+ "@types/node": "^24.6.0",
81
+ "@types/picomatch": "^4.0.2",
82
+ "typescript": "~5.9.3",
83
+ "vitest": "^4.0.8"
84
+ }
85
+ }