ng-image-optimizer 0.0.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 ADDED
@@ -0,0 +1,64 @@
1
+ # NgImageOptimizer
2
+
3
+ This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.2.0.
4
+
5
+ ## Code scaffolding
6
+
7
+ Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
8
+
9
+ ```bash
10
+ ng generate component component-name
11
+ ```
12
+
13
+ For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
14
+
15
+ ```bash
16
+ ng generate --help
17
+ ```
18
+
19
+ ## Building
20
+
21
+ To build the library, run:
22
+
23
+ ```bash
24
+ ng build ng-image-optimizer
25
+ ```
26
+
27
+ This command will compile your project, and the build artifacts will be placed in the `dist/` directory.
28
+
29
+ ### Publishing the Library
30
+
31
+ Once the project is built, you can publish your library by following these steps:
32
+
33
+ 1. Navigate to the `dist` directory:
34
+
35
+ ```bash
36
+ cd dist/ng-image-optimizer
37
+ ```
38
+
39
+ 2. Run the `npm publish` command to publish your library to the npm registry:
40
+ ```bash
41
+ npm publish
42
+ ```
43
+
44
+ ## Running unit tests
45
+
46
+ To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
47
+
48
+ ```bash
49
+ ng test
50
+ ```
51
+
52
+ ## Running end-to-end tests
53
+
54
+ For end-to-end (e2e) testing, run:
55
+
56
+ ```bash
57
+ ng e2e
58
+ ```
59
+
60
+ Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
61
+
62
+ ## Additional Resources
63
+
64
+ For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
@@ -0,0 +1,506 @@
1
+ import fs, { existsSync, mkdirSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { isIP } from 'node:net';
5
+ import { lookup } from 'node:dns/promises';
6
+ import fs$1 from 'node:fs/promises';
7
+ import { LRUCache } from 'lru-cache';
8
+ import sharp from 'sharp';
9
+
10
+ const defaultConfig = {
11
+ deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
12
+ imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
13
+ remotePatterns: [],
14
+ minimumCacheTTL: 14400,
15
+ formats: ['image/webp'],
16
+ dangerouslyAllowSVG: false,
17
+ contentSecurityPolicy: "script-src 'none'; frame-src 'none'; sandbox;",
18
+ contentDispositionType: 'inline',
19
+ maxCacheSize: 50 * 1024 * 1024, // 50MB
20
+ };
21
+
22
+ class ImageError extends Error {
23
+ statusCode;
24
+ constructor(statusCode, message) {
25
+ super(message);
26
+ if (statusCode >= 400) {
27
+ this.statusCode = statusCode;
28
+ }
29
+ else {
30
+ this.statusCode = 500;
31
+ }
32
+ }
33
+ }
34
+ function getHash(items) {
35
+ const hash = createHash('sha256');
36
+ for (let item of items) {
37
+ if (typeof item === 'number')
38
+ hash.update(String(item));
39
+ else {
40
+ hash.update(item);
41
+ }
42
+ }
43
+ return hash.digest('base64url');
44
+ }
45
+ function getImageEtag(image) {
46
+ return getHash([image]);
47
+ }
48
+ function extractEtag(etag, imageBuffer) {
49
+ if (etag) {
50
+ return Buffer.from(etag).toString('base64url');
51
+ }
52
+ return getImageEtag(imageBuffer);
53
+ }
54
+ function parseCacheControl(str) {
55
+ const map = new Map();
56
+ if (!str) {
57
+ return map;
58
+ }
59
+ for (let directive of str.split(',')) {
60
+ let [key, value] = directive.trim().split('=', 2);
61
+ key = key.toLowerCase();
62
+ if (value) {
63
+ value = value.toLowerCase();
64
+ }
65
+ map.set(key, value);
66
+ }
67
+ return map;
68
+ }
69
+ function getMaxAge(str) {
70
+ const map = parseCacheControl(str);
71
+ if (map) {
72
+ let age = map.get('s-maxage') || map.get('max-age') || '';
73
+ if (age.startsWith('"') && age.endsWith('"')) {
74
+ age = age.slice(1, -1);
75
+ }
76
+ const n = parseInt(age, 10);
77
+ if (!isNaN(n)) {
78
+ return n;
79
+ }
80
+ }
81
+ return 0;
82
+ }
83
+ const AVIF = 'image/avif';
84
+ const WEBP = 'image/webp';
85
+ const PNG = 'image/png';
86
+ const JPEG = 'image/jpeg';
87
+ const GIF = 'image/gif';
88
+ const SVG = 'image/svg+xml';
89
+ const ICO = 'image/x-icon';
90
+ async function detectContentType(buffer) {
91
+ if (buffer.byteLength === 0) {
92
+ return null;
93
+ }
94
+ if ([0xff, 0xd8, 0xff].every((b, i) => buffer[i] === b)) {
95
+ return JPEG;
96
+ }
97
+ if ([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every((b, i) => buffer[i] === b)) {
98
+ return PNG;
99
+ }
100
+ if ([0x47, 0x49, 0x46, 0x38].every((b, i) => buffer[i] === b)) {
101
+ return GIF;
102
+ }
103
+ if ([0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50].every((b, i) => !b || buffer[i] === b)) {
104
+ return WEBP;
105
+ }
106
+ if ([0x3c, 0x3f, 0x78, 0x6d, 0x6c].every((b, i) => buffer[i] === b)) {
107
+ return SVG;
108
+ }
109
+ if ([0x3c, 0x73, 0x76, 0x67].every((b, i) => buffer[i] === b)) {
110
+ return SVG;
111
+ }
112
+ if ([0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66].every((b, i) => !b || buffer[i] === b)) {
113
+ return AVIF;
114
+ }
115
+ if ([0x00, 0x00, 0x01, 0x00].every((b, i) => buffer[i] === b)) {
116
+ return ICO;
117
+ }
118
+ return null;
119
+ }
120
+
121
+ function validateParams(req, query, config) {
122
+ const { url, w, q } = query;
123
+ let href;
124
+ if (!url)
125
+ return { errorMessage: '"url" parameter is required' };
126
+ if (Array.isArray(url))
127
+ return { errorMessage: '"url" parameter cannot be an array' };
128
+ if (url.length > 3072)
129
+ return { errorMessage: '"url" parameter is too long' };
130
+ if (url.startsWith('//'))
131
+ return { errorMessage: '"url" parameter cannot be a protocol-relative URL (//)' };
132
+ let isAbsolute;
133
+ if (url.startsWith('/')) {
134
+ href = url;
135
+ isAbsolute = false;
136
+ if (url.includes('/_ng/image')) {
137
+ return { errorMessage: '"url" parameter cannot be recursive' };
138
+ }
139
+ if (config.localPatterns) {
140
+ const localPatternMatch = config.localPatterns.some((pattern) => {
141
+ const { pathname, search } = pattern;
142
+ return url.startsWith(pathname) && (search ? url.includes(search) : true);
143
+ });
144
+ if (localPatternMatch) {
145
+ return { errorMessage: 'url parameter matches a local pattern' };
146
+ }
147
+ }
148
+ }
149
+ else {
150
+ let hrefParsed;
151
+ try {
152
+ hrefParsed = new URL(url);
153
+ href = hrefParsed.toString();
154
+ isAbsolute = true;
155
+ }
156
+ catch {
157
+ return { errorMessage: '"url" parameter is invalid' };
158
+ }
159
+ if (!['http:', 'https:'].includes(hrefParsed.protocol)) {
160
+ return { errorMessage: '"url" parameter is invalid' };
161
+ }
162
+ // remotePatterns check
163
+ const matchesPattern = config.remotePatterns.some((p) => p.hostname === hrefParsed.hostname);
164
+ if (config.remotePatterns.length > 0 && !matchesPattern) {
165
+ return { errorMessage: '"url" parameter is not allowed' };
166
+ }
167
+ }
168
+ if (!w)
169
+ return { errorMessage: '"w" parameter (width) is required' };
170
+ if (Array.isArray(w))
171
+ return { errorMessage: '"w" parameter (width) cannot be an array' };
172
+ if (!/^[0-9]+$/.test(w))
173
+ return { errorMessage: '"w" parameter (width) must be an integer greater than 0' };
174
+ if (!q)
175
+ return { errorMessage: '"q" parameter (quality) is required' };
176
+ if (Array.isArray(q))
177
+ return { errorMessage: '"q" parameter (quality) cannot be an array' };
178
+ if (!/^[0-9]+$/.test(q))
179
+ return { errorMessage: '"q" parameter (quality) must be an integer between 1 and 100' };
180
+ const width = parseInt(w, 10);
181
+ if (width <= 0 || isNaN(width))
182
+ return { errorMessage: '"w" parameter (width) must be an integer greater than 0' };
183
+ const sizes = [...config.deviceSizes, ...config.imageSizes];
184
+ const isValidSize = sizes.includes(width);
185
+ const closestSize = isValidSize
186
+ ? width
187
+ : sizes.reduce((prev, curr) => (Math.abs(curr - width) < Math.abs(prev - width) ? curr : prev));
188
+ const quality = parseInt(q, 10);
189
+ if (isNaN(quality) || quality < 1 || quality > 100) {
190
+ return { errorMessage: '"q" parameter (quality) must be an integer between 1 and 100' };
191
+ }
192
+ if (config.qualities && !config.qualities.includes(quality)) {
193
+ return { errorMessage: `"q" parameter (quality) of ${q} is not allowed` };
194
+ }
195
+ const accept = req.headers['accept'] || '';
196
+ let mimeType = 'image/jpeg';
197
+ if (accept.includes('image/avif') && config.formats.includes('image/avif')) {
198
+ mimeType = 'image/avif';
199
+ }
200
+ else if (accept.includes('image/webp') && config.formats.includes('image/webp')) {
201
+ mimeType = 'image/webp';
202
+ }
203
+ return {
204
+ href,
205
+ isAbsolute,
206
+ isStatic: false,
207
+ width: closestSize,
208
+ quality,
209
+ mimeType,
210
+ minimumCacheTTL: config.minimumCacheTTL,
211
+ };
212
+ }
213
+
214
+ const isPrivateIp = (ip) => {
215
+ return /^(::f{4}:)?10\.\d{1,3}\.\d{1,3}\.\d{1,3}/.test(ip) ||
216
+ /^(::f{4}:)?192\.168\.\d{1,3}\.\d{1,3}/.test(ip) ||
217
+ /^(::f{4}:)?172\.(1[6-9]|2\d|3[0-1])\.\d{1,3}\.\d{1,3}/.test(ip) ||
218
+ /^(::f{4}:)?127\.\d{1,3}\.\d{1,3}\.\d{1,3}/.test(ip) ||
219
+ /^(::f{4}:)?169\.254\.\d{1,3}\.\d{1,3}/.test(ip) ||
220
+ /^f[cd][0-9a-f]{2}:/i.test(ip) ||
221
+ /^fe80:/i.test(ip) ||
222
+ /^::1$/.test(ip) ||
223
+ /^::$/.test(ip);
224
+ };
225
+ async function fetchExternalImage(href, dangerouslyAllowLocalIP, maximumResponseBody, count = 3) {
226
+ if (!dangerouslyAllowLocalIP) {
227
+ const { hostname } = new URL(href);
228
+ let ips = [hostname];
229
+ if (!isIP(hostname)) {
230
+ const records = await lookup(hostname, { family: 0, all: true }).catch(() => [{ address: hostname }]);
231
+ ips = records.map(r => r.address);
232
+ }
233
+ const privateIps = ips.filter(isPrivateIp);
234
+ if (privateIps.length > 0) {
235
+ throw new ImageError(400, '"url" parameter is not allowed (resolved to private IP)');
236
+ }
237
+ }
238
+ const res = await fetch(href, {
239
+ signal: AbortSignal.timeout(7000),
240
+ redirect: 'manual',
241
+ }).catch(err => err);
242
+ if (res instanceof Error) {
243
+ if (res.name === 'TimeoutError')
244
+ throw new ImageError(504, 'upstream image response timed out');
245
+ throw res;
246
+ }
247
+ const locationHeader = res.headers.get('Location');
248
+ if ([301, 302, 303, 307, 308].includes(res.status) && locationHeader) {
249
+ if (count === 0)
250
+ throw new ImageError(508, 'too many redirects');
251
+ const redirect = new URL(locationHeader, href).href;
252
+ return fetchExternalImage(redirect, dangerouslyAllowLocalIP, maximumResponseBody, count - 1);
253
+ }
254
+ if (!res.ok || !res.body) {
255
+ throw new ImageError(res.status || 400, 'upstream image response is invalid');
256
+ }
257
+ const chunks = [];
258
+ let totalSize = 0;
259
+ // Need to process stream chunks manually to cap size
260
+ const reader = res.body.getReader();
261
+ while (true) {
262
+ const { done, value } = await reader.read();
263
+ if (done)
264
+ break;
265
+ totalSize += value.byteLength;
266
+ if (totalSize > maximumResponseBody) {
267
+ throw new ImageError(413, 'upstream response exceeded maximum size');
268
+ }
269
+ chunks.push(value);
270
+ }
271
+ const buffer = Buffer.concat(chunks);
272
+ const etag = extractEtag(res.headers.get('ETag'), buffer);
273
+ return {
274
+ buffer,
275
+ contentType: res.headers.get('Content-Type'),
276
+ cacheControl: res.headers.get('Cache-Control'),
277
+ etag,
278
+ };
279
+ }
280
+ async function fetchInternalImage(url, browserDistFolder) {
281
+ const filePath = path.join(browserDistFolder, url.startsWith('/') ? url.slice(1) : url);
282
+ if (!fs.existsSync(filePath)) {
283
+ throw new ImageError(404, 'Local image not found');
284
+ }
285
+ const buffer = fs.readFileSync(filePath);
286
+ return {
287
+ buffer,
288
+ contentType: undefined,
289
+ cacheControl: undefined,
290
+ etag: extractEtag(null, buffer),
291
+ };
292
+ }
293
+
294
+ const CACHE_DIR = path.join(process.cwd(), '.image-cache');
295
+ if (!existsSync(CACHE_DIR)) {
296
+ mkdirSync(CACHE_DIR, { recursive: true });
297
+ }
298
+ const memoryCache = new LRUCache({
299
+ maxSize: defaultConfig.maxCacheSize || 50 * 1024 * 1024,
300
+ sizeCalculation: (value) => value,
301
+ dispose: async (value, key) => {
302
+ try {
303
+ const dir = path.join(CACHE_DIR, key);
304
+ await fs$1.rm(dir, { recursive: true, force: true });
305
+ }
306
+ catch (e) {
307
+ console.error(`LRU dispose error for ${key}:`, e);
308
+ }
309
+ },
310
+ });
311
+ async function writeToCacheDir(cacheKey, extension, maxAge, expireAt, buffer, etag, upstreamEtag) {
312
+ const dir = path.join(CACHE_DIR, cacheKey);
313
+ const filename = path.join(dir, `${maxAge}.${expireAt}.${etag}.${upstreamEtag}.${extension}`);
314
+ await fs$1.rm(dir, { recursive: true, force: true }).catch(() => { });
315
+ await fs$1.mkdir(dir, { recursive: true });
316
+ await fs$1.writeFile(filename, buffer);
317
+ // Register in memory LRU tracker
318
+ memoryCache.set(cacheKey, buffer.length);
319
+ }
320
+ async function readFromCacheDir(cacheKey) {
321
+ const dir = path.join(CACHE_DIR, cacheKey);
322
+ const files = await fs$1.readdir(dir).catch(() => []);
323
+ const file = files[0];
324
+ if (!file) {
325
+ memoryCache.delete(cacheKey);
326
+ throw new Error(`cache entry "${cacheKey}" not found`);
327
+ }
328
+ const [maxAgeSt, expireAtSt, etag, upstreamEtag, extension] = file.split('.', 5);
329
+ const filePath = path.join(dir, file);
330
+ const stat = await fs$1.stat(filePath);
331
+ const buffer = await fs$1.readFile(filePath);
332
+ // Promote LRU entry tracking since it was successfully accessed
333
+ memoryCache.set(cacheKey, stat.size);
334
+ return {
335
+ maxAge: Number(maxAgeSt),
336
+ expireAt: Number(expireAtSt),
337
+ etag,
338
+ upstreamEtag,
339
+ buffer,
340
+ extension,
341
+ };
342
+ }
343
+ // Resiliency scanning - Pre-populate the LRU tracking queue instantly on startup
344
+ async function setupCacheSync() {
345
+ try {
346
+ const entries = await fs$1.readdir(CACHE_DIR, { withFileTypes: true });
347
+ for (const entry of entries) {
348
+ if (entry.isDirectory()) {
349
+ const cacheKey = entry.name;
350
+ const dir = path.join(CACHE_DIR, cacheKey);
351
+ const files = await fs$1.readdir(dir);
352
+ if (files[0]) {
353
+ const stat = await fs$1.stat(path.join(dir, files[0]));
354
+ memoryCache.set(cacheKey, stat.size);
355
+ }
356
+ }
357
+ }
358
+ }
359
+ catch (err) {
360
+ // Ignore initial failures (like folder not existing)
361
+ }
362
+ }
363
+ // Start synchronization proactively in the background
364
+ setupCacheSync();
365
+
366
+ async function optimizeImage(imageUpstream, params) {
367
+ const { href, quality, width, mimeType } = params;
368
+ const { buffer: upstreamBuffer, etag: upstreamEtag } = imageUpstream;
369
+ const upstreamType = await detectContentType(upstreamBuffer);
370
+ if (!upstreamType || !upstreamType.startsWith('image/')) {
371
+ throw new ImageError(400, "The requested resource isn't a valid image.");
372
+ }
373
+ if (upstreamType === 'image/svg+xml') {
374
+ // Return SVG unmodified as sharp doesn't re-optimize SVGs well
375
+ // SVG is allowed or blocked before this function in index.ts based on dangerouslyAllowSVG
376
+ return {
377
+ buffer: upstreamBuffer,
378
+ contentType: upstreamType,
379
+ etag: upstreamEtag,
380
+ upstreamEtag
381
+ };
382
+ }
383
+ let contentType = mimeType || upstreamType || 'image/jpeg';
384
+ try {
385
+ const transformer = sharp(upstreamBuffer).rotate();
386
+ transformer.resize(width, undefined, { withoutEnlargement: true });
387
+ if (contentType === 'image/avif') {
388
+ transformer.avif({ quality: Math.max(quality - 20, 1), effort: 3 });
389
+ }
390
+ else if (contentType === 'image/webp') {
391
+ transformer.webp({ quality });
392
+ }
393
+ else if (contentType === 'image/png') {
394
+ transformer.png({ quality });
395
+ }
396
+ else {
397
+ contentType = 'image/jpeg';
398
+ transformer.jpeg({ quality, mozjpeg: true });
399
+ }
400
+ const optimizedBuffer = await transformer.toBuffer();
401
+ return {
402
+ buffer: optimizedBuffer,
403
+ contentType,
404
+ etag: getImageEtag(optimizedBuffer),
405
+ upstreamEtag,
406
+ };
407
+ }
408
+ catch (error) {
409
+ console.error('Sharp optimization error:', error);
410
+ // Fallback to upstream image if Sharp fails
411
+ return {
412
+ buffer: upstreamBuffer,
413
+ contentType: upstreamType,
414
+ etag: upstreamEtag,
415
+ upstreamEtag,
416
+ };
417
+ }
418
+ }
419
+
420
+ const imageOptimizerHandler = (browserDistFolder, options) => {
421
+ const config = { ...defaultConfig, ...options };
422
+ const isDev = process.env['NODE_ENV'] !== 'production';
423
+ const publicFolder = isDev || !fs.existsSync(browserDistFolder)
424
+ ? path.join(process.cwd(), 'public')
425
+ : browserDistFolder;
426
+ return async (req, res) => {
427
+ try {
428
+ // 1. Validate Request
429
+ const paramsResult = validateParams(req, req.query, config);
430
+ if ('errorMessage' in paramsResult) {
431
+ throw new ImageError(400, paramsResult.errorMessage);
432
+ }
433
+ const { href, isAbsolute, width, quality, mimeType, minimumCacheTTL } = paramsResult;
434
+ // 2. Cache Key Generation (High Entropy)
435
+ const CACHE_VERSION = 4;
436
+ const cacheKey = getHash([CACHE_VERSION, href, width, quality, mimeType]);
437
+ // 3. Cache Read Check
438
+ try {
439
+ const { maxAge, expireAt, buffer, extension, etag } = await readFromCacheDir(cacheKey);
440
+ // TTL validation
441
+ if (Date.now() <= expireAt) {
442
+ res.setHeader('Vary', 'Accept');
443
+ res.setHeader('Cache-Control', `public, max-age=${maxAge}, must-revalidate`);
444
+ res.setHeader('Content-Type', `image/${extension}`);
445
+ res.setHeader('X-Nextjs-Cache', 'HIT');
446
+ res.setHeader('Content-Disposition', `inline; filename="image.${extension}"`);
447
+ res.setHeader('Content-Security-Policy', config.contentSecurityPolicy);
448
+ res.setHeader('ETag', etag);
449
+ res.status(200).send(buffer);
450
+ return;
451
+ }
452
+ }
453
+ catch (e) {
454
+ // Cache miss
455
+ }
456
+ // 4. Fetch Image (External or Internal)
457
+ const upstream = await (isAbsolute
458
+ ? fetchExternalImage(href, false, 5 * 1024 * 1024) // 5MB limit
459
+ : fetchInternalImage(href, publicFolder));
460
+ const upstreamType = await detectContentType(upstream.buffer);
461
+ if (upstreamType === 'image/svg+xml' && !config.dangerouslyAllowSVG) {
462
+ throw new ImageError(400, '"url" parameter is valid but image type is not allowed (SVG)');
463
+ }
464
+ // 5. Optimize Image
465
+ const optimized = await optimizeImage(upstream, paramsResult);
466
+ const maxAge = Math.max(minimumCacheTTL, getMaxAge(upstream.cacheControl));
467
+ const expireAt = maxAge * 1000 + Date.now();
468
+ const extension = optimized.contentType.split('/')[1] || 'jpeg';
469
+ // 6. Write to File Cache
470
+ await writeToCacheDir(cacheKey, extension, maxAge, expireAt, optimized.buffer, optimized.etag, upstream.etag);
471
+ // 7. Send Response
472
+ res.setHeader('Vary', 'Accept');
473
+ res.setHeader('Cache-Control', `public, max-age=${maxAge}, must-revalidate`);
474
+ res.setHeader('Content-Type', optimized.contentType);
475
+ res.setHeader('X-Nextjs-Cache', 'MISS');
476
+ res.setHeader('Content-Disposition', `inline; filename="image.${extension}"`);
477
+ res.setHeader('Content-Security-Policy', config.contentSecurityPolicy);
478
+ res.setHeader('ETag', optimized.etag);
479
+ res.status(200).send(optimized.buffer);
480
+ return;
481
+ }
482
+ catch (error) {
483
+ if (error instanceof ImageError) {
484
+ console.error('Image Error:', error.message);
485
+ res.status(error.statusCode).send(error.message);
486
+ }
487
+ else {
488
+ console.error('Unexpected Optimizer Error:', error);
489
+ res.status(500).send('Internal Server Error optimizing image');
490
+ }
491
+ return;
492
+ }
493
+ };
494
+ };
495
+
496
+ /**
497
+ * Server-side entry point for ng-image-optimizer
498
+ * Exports all server-side functionality for image optimization
499
+ */
500
+
501
+ /**
502
+ * Generated bundle index. Do not edit.
503
+ */
504
+
505
+ export { ImageError, defaultConfig, imageOptimizerHandler };
506
+ //# sourceMappingURL=ng-image-optimizer-server.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ng-image-optimizer-server.mjs","sources":["../../../projects/ng-image-optimizer/server/config.ts","../../../projects/ng-image-optimizer/server/utils.ts","../../../projects/ng-image-optimizer/server/validator.ts","../../../projects/ng-image-optimizer/server/fetcher.ts","../../../projects/ng-image-optimizer/server/cache.ts","../../../projects/ng-image-optimizer/server/optimizer.ts","../../../projects/ng-image-optimizer/server/handler.ts","../../../projects/ng-image-optimizer/server/index.ts","../../../projects/ng-image-optimizer/server/ng-image-optimizer-server.ts"],"sourcesContent":["export interface RemotePattern {\n protocol?: 'http' | 'https';\n hostname: string;\n port?: string;\n pathname?: string;\n}\n\nexport interface ImageConfig {\n deviceSizes: number[];\n imageSizes: number[];\n remotePatterns: RemotePattern[];\n localPatterns?: { pathname: string; search: string }[];\n minimumCacheTTL: number;\n formats: ('image/avif' | 'image/webp')[];\n dangerouslyAllowSVG: boolean;\n contentSecurityPolicy: string;\n contentDispositionType: 'inline' | 'attachment';\n qualities?: number[];\n maxCacheSize?: number;\n}\n\nexport const defaultConfig: ImageConfig = {\n deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],\n imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],\n remotePatterns: [],\n minimumCacheTTL: 14400,\n formats: ['image/webp'],\n dangerouslyAllowSVG: false,\n contentSecurityPolicy: \"script-src 'none'; frame-src 'none'; sandbox;\",\n contentDispositionType: 'inline',\n maxCacheSize: 50 * 1024 * 1024, // 50MB\n};\n","import { createHash } from 'node:crypto';\n\nexport class ImageError extends Error {\n statusCode: number;\n\n constructor(statusCode: number, message: string) {\n super(message);\n if (statusCode >= 400) {\n this.statusCode = statusCode;\n } else {\n this.statusCode = 500;\n }\n }\n}\n\nexport function getHash(items: (string | number | Buffer)[]) {\n const hash = createHash('sha256');\n for (let item of items) {\n if (typeof item === 'number') hash.update(String(item));\n else {\n hash.update(item);\n }\n }\n return hash.digest('base64url');\n}\n\nexport function getImageEtag(image: Buffer) {\n return getHash([image]);\n}\n\nexport function extractEtag(etag: string | null | undefined, imageBuffer: Buffer) {\n if (etag) {\n return Buffer.from(etag).toString('base64url');\n }\n return getImageEtag(imageBuffer);\n}\n\nfunction parseCacheControl(str: string | null | undefined): Map<string, string> {\n const map = new Map<string, string>();\n if (!str) {\n return map;\n }\n for (let directive of str.split(',')) {\n let [key, value] = directive.trim().split('=', 2);\n key = key.toLowerCase();\n if (value) {\n value = value.toLowerCase();\n }\n map.set(key, value);\n }\n return map;\n}\n\nexport function getMaxAge(str: string | null | undefined): number {\n const map = parseCacheControl(str);\n if (map) {\n let age = map.get('s-maxage') || map.get('max-age') || '';\n if (age.startsWith('\"') && age.endsWith('\"')) {\n age = age.slice(1, -1);\n }\n const n = parseInt(age, 10);\n if (!isNaN(n)) {\n return n;\n }\n }\n return 0;\n}\n\nconst AVIF = 'image/avif';\nconst WEBP = 'image/webp';\nconst PNG = 'image/png';\nconst JPEG = 'image/jpeg';\nconst GIF = 'image/gif';\nconst SVG = 'image/svg+xml';\nconst ICO = 'image/x-icon';\n\nexport async function detectContentType(buffer: Buffer): Promise<string | null> {\n if (buffer.byteLength === 0) {\n return null;\n }\n if ([0xff, 0xd8, 0xff].every((b, i) => buffer[i] === b)) {\n return JPEG;\n }\n if ([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every((b, i) => buffer[i] === b)) {\n return PNG;\n }\n if ([0x47, 0x49, 0x46, 0x38].every((b, i) => buffer[i] === b)) {\n return GIF;\n }\n if (\n [0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50].every(\n (b, i) => !b || buffer[i] === b,\n )\n ) {\n return WEBP;\n }\n if ([0x3c, 0x3f, 0x78, 0x6d, 0x6c].every((b, i) => buffer[i] === b)) {\n return SVG;\n }\n if ([0x3c, 0x73, 0x76, 0x67].every((b, i) => buffer[i] === b)) {\n return SVG;\n }\n if (\n [0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66].every(\n (b, i) => !b || buffer[i] === b,\n )\n ) {\n return AVIF;\n }\n if ([0x00, 0x00, 0x01, 0x00].every((b, i) => buffer[i] === b)) {\n return ICO;\n }\n return null;\n}\n","import type { IncomingMessage } from 'node:http';\nimport { ImageConfig } from './config';\n\nexport interface ImageParamsResult {\n href: string;\n isAbsolute: boolean;\n isStatic: boolean;\n width: number;\n quality: number;\n mimeType: string;\n minimumCacheTTL: number;\n}\n\nexport function validateParams(\n req: IncomingMessage,\n query: Record<string, any>,\n config: ImageConfig,\n): ImageParamsResult | { errorMessage: string } {\n const { url, w, q } = query;\n\n let href: string;\n\n if (!url) return { errorMessage: '\"url\" parameter is required' };\n if (Array.isArray(url)) return { errorMessage: '\"url\" parameter cannot be an array' };\n if (url.length > 3072) return { errorMessage: '\"url\" parameter is too long' };\n if (url.startsWith('//'))\n return { errorMessage: '\"url\" parameter cannot be a protocol-relative URL (//)' };\n\n let isAbsolute: boolean;\n\n if (url.startsWith('/')) {\n href = url;\n isAbsolute = false;\n if (url.includes('/_ng/image')) {\n return { errorMessage: '\"url\" parameter cannot be recursive' };\n }\n if (config.localPatterns) {\n const localPatternMatch = config.localPatterns.some((pattern) => {\n const { pathname, search } = pattern;\n return url.startsWith(pathname) && (search ? url.includes(search) : true);\n });\n if (localPatternMatch) {\n return { errorMessage: 'url parameter matches a local pattern' };\n }\n }\n } else {\n let hrefParsed: URL;\n try {\n hrefParsed = new URL(url);\n\n href = hrefParsed.toString();\n isAbsolute = true;\n } catch {\n return { errorMessage: '\"url\" parameter is invalid' };\n }\n\n if (!['http:', 'https:'].includes(hrefParsed.protocol)) {\n return { errorMessage: '\"url\" parameter is invalid' };\n }\n\n // remotePatterns check\n const matchesPattern = config.remotePatterns.some((p) => p.hostname === hrefParsed.hostname);\n if (config.remotePatterns.length > 0 && !matchesPattern) {\n return { errorMessage: '\"url\" parameter is not allowed' };\n }\n }\n\n if (!w) return { errorMessage: '\"w\" parameter (width) is required' };\n if (Array.isArray(w)) return { errorMessage: '\"w\" parameter (width) cannot be an array' };\n if (!/^[0-9]+$/.test(w))\n return { errorMessage: '\"w\" parameter (width) must be an integer greater than 0' };\n\n if (!q) return { errorMessage: '\"q\" parameter (quality) is required' };\n if (Array.isArray(q)) return { errorMessage: '\"q\" parameter (quality) cannot be an array' };\n if (!/^[0-9]+$/.test(q))\n return { errorMessage: '\"q\" parameter (quality) must be an integer between 1 and 100' };\n\n const width = parseInt(w, 10);\n if (width <= 0 || isNaN(width))\n return { errorMessage: '\"w\" parameter (width) must be an integer greater than 0' };\n\n const sizes = [...config.deviceSizes, ...config.imageSizes];\n const isValidSize = sizes.includes(width);\n const closestSize = isValidSize\n ? width\n : sizes.reduce((prev, curr) => (Math.abs(curr - width) < Math.abs(prev - width) ? curr : prev));\n\n const quality = parseInt(q, 10);\n if (isNaN(quality) || quality < 1 || quality > 100) {\n return { errorMessage: '\"q\" parameter (quality) must be an integer between 1 and 100' };\n }\n\n if (config.qualities && !config.qualities.includes(quality)) {\n return { errorMessage: `\"q\" parameter (quality) of ${q} is not allowed` };\n }\n\n const accept = req.headers['accept'] || '';\n let mimeType = 'image/jpeg';\n if (accept.includes('image/avif') && config.formats.includes('image/avif')) {\n mimeType = 'image/avif';\n } else if (accept.includes('image/webp') && config.formats.includes('image/webp')) {\n mimeType = 'image/webp';\n }\n\n return {\n href,\n isAbsolute,\n isStatic: false,\n width: closestSize,\n quality,\n mimeType,\n minimumCacheTTL: config.minimumCacheTTL,\n };\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport { ImageError, extractEtag } from './utils';\nimport { isIP } from 'node:net';\nimport { lookup } from 'node:dns/promises';\n\nexport interface ImageUpstream {\n buffer: Buffer;\n contentType: string | null | undefined;\n cacheControl: string | null | undefined;\n etag: string;\n}\n\nconst isPrivateIp = (ip: string) => {\n return /^(::f{4}:)?10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}/.test(ip) ||\n /^(::f{4}:)?192\\.168\\.\\d{1,3}\\.\\d{1,3}/.test(ip) ||\n /^(::f{4}:)?172\\.(1[6-9]|2\\d|3[0-1])\\.\\d{1,3}\\.\\d{1,3}/.test(ip) ||\n /^(::f{4}:)?127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}/.test(ip) ||\n /^(::f{4}:)?169\\.254\\.\\d{1,3}\\.\\d{1,3}/.test(ip) ||\n /^f[cd][0-9a-f]{2}:/i.test(ip) ||\n /^fe80:/i.test(ip) ||\n /^::1$/.test(ip) ||\n /^::$/.test(ip);\n};\n\nexport async function fetchExternalImage(\n href: string,\n dangerouslyAllowLocalIP: boolean,\n maximumResponseBody: number,\n count = 3,\n): Promise<ImageUpstream> {\n if (!dangerouslyAllowLocalIP) {\n const { hostname } = new URL(href);\n let ips = [hostname];\n if (!isIP(hostname)) {\n const records = await lookup(hostname, { family: 0, all: true }).catch(() => [{ address: hostname }]);\n ips = records.map(r => r.address);\n }\n const privateIps = ips.filter(isPrivateIp);\n if (privateIps.length > 0) {\n throw new ImageError(400, '\"url\" parameter is not allowed (resolved to private IP)');\n }\n }\n\n const res = await fetch(href, {\n signal: AbortSignal.timeout(7000),\n redirect: 'manual',\n }).catch(err => err as Error);\n\n if (res instanceof Error) {\n if (res.name === 'TimeoutError') throw new ImageError(504, 'upstream image response timed out');\n throw res;\n }\n\n const locationHeader = res.headers.get('Location');\n if ([301, 302, 303, 307, 308].includes(res.status) && locationHeader) {\n if (count === 0) throw new ImageError(508, 'too many redirects');\n const redirect = new URL(locationHeader, href).href;\n return fetchExternalImage(redirect, dangerouslyAllowLocalIP, maximumResponseBody, count - 1);\n }\n\n if (!res.ok || !res.body) {\n throw new ImageError(res.status || 400, 'upstream image response is invalid');\n }\n\n const chunks: Uint8Array[] = [];\n let totalSize = 0;\n \n // Need to process stream chunks manually to cap size\n const reader = res.body.getReader();\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n totalSize += value.byteLength;\n if (totalSize > maximumResponseBody) {\n throw new ImageError(413, 'upstream response exceeded maximum size');\n }\n chunks.push(value);\n }\n\n const buffer = Buffer.concat(chunks);\n const etag = extractEtag(res.headers.get('ETag'), buffer);\n\n return {\n buffer,\n contentType: res.headers.get('Content-Type'),\n cacheControl: res.headers.get('Cache-Control'),\n etag,\n };\n}\n\nexport async function fetchInternalImage(\n url: string,\n browserDistFolder: string,\n): Promise<ImageUpstream> {\n const filePath = path.join(browserDistFolder, url.startsWith('/') ? url.slice(1) : url);\n if (!fs.existsSync(filePath)) {\n throw new ImageError(404, 'Local image not found');\n }\n const buffer = fs.readFileSync(filePath);\n return {\n buffer,\n contentType: undefined,\n cacheControl: undefined,\n etag: extractEtag(null, buffer),\n };\n}\n","import fs from 'node:fs/promises';\nimport { existsSync, mkdirSync } from 'node:fs';\nimport path from 'node:path';\nimport { LRUCache } from 'lru-cache';\nimport { defaultConfig } from './config';\n\nconst CACHE_DIR = path.join(process.cwd(), '.image-cache');\n\nif (!existsSync(CACHE_DIR)) {\n mkdirSync(CACHE_DIR, { recursive: true });\n}\n\nconst memoryCache = new LRUCache<string, number>({\n maxSize: defaultConfig.maxCacheSize || 50 * 1024 * 1024,\n sizeCalculation: (value) => value,\n dispose: async (value, key) => {\n try {\n const dir = path.join(CACHE_DIR, key);\n await fs.rm(dir, { recursive: true, force: true });\n } catch (e) {\n console.error(`LRU dispose error for ${key}:`, e);\n }\n },\n});\n\nexport async function writeToCacheDir(\n cacheKey: string,\n extension: string,\n maxAge: number,\n expireAt: number,\n buffer: Buffer,\n etag: string,\n upstreamEtag: string,\n) {\n const dir = path.join(CACHE_DIR, cacheKey);\n const filename = path.join(dir, `${maxAge}.${expireAt}.${etag}.${upstreamEtag}.${extension}`);\n\n await fs.rm(dir, { recursive: true, force: true }).catch(() => {});\n await fs.mkdir(dir, { recursive: true });\n await fs.writeFile(filename, buffer);\n\n // Register in memory LRU tracker\n memoryCache.set(cacheKey, buffer.length);\n}\n\nexport async function readFromCacheDir(cacheKey: string) {\n const dir = path.join(CACHE_DIR, cacheKey);\n const files = await fs.readdir(dir).catch(() => []);\n const file = files[0];\n if (!file) {\n memoryCache.delete(cacheKey);\n throw new Error(`cache entry \"${cacheKey}\" not found`);\n }\n\n const [maxAgeSt, expireAtSt, etag, upstreamEtag, extension] = file.split('.', 5);\n const filePath = path.join(dir, file);\n const stat = await fs.stat(filePath);\n const buffer = await fs.readFile(filePath);\n\n // Promote LRU entry tracking since it was successfully accessed\n memoryCache.set(cacheKey, stat.size);\n\n return {\n maxAge: Number(maxAgeSt),\n expireAt: Number(expireAtSt),\n etag,\n upstreamEtag,\n buffer,\n extension,\n };\n}\n\n// Resiliency scanning - Pre-populate the LRU tracking queue instantly on startup\nasync function setupCacheSync() {\n try {\n const entries = await fs.readdir(CACHE_DIR, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isDirectory()) {\n const cacheKey = entry.name;\n const dir = path.join(CACHE_DIR, cacheKey);\n const files = await fs.readdir(dir);\n if (files[0]) {\n const stat = await fs.stat(path.join(dir, files[0]));\n memoryCache.set(cacheKey, stat.size);\n }\n }\n }\n } catch (err) {\n // Ignore initial failures (like folder not existing)\n }\n}\n\n// Start synchronization proactively in the background\nsetupCacheSync();\n","import sharp from 'sharp';\r\nimport { detectContentType, ImageError, getImageEtag } from './utils';\r\nimport { ImageParamsResult } from './validator';\r\nimport { ImageUpstream } from './fetcher';\r\n\r\nexport async function optimizeImage(\r\n imageUpstream: ImageUpstream,\r\n params: Pick<ImageParamsResult, 'href' | 'width' | 'quality' | 'mimeType'>,\r\n) {\r\n const { href, quality, width, mimeType } = params;\r\n const { buffer: upstreamBuffer, etag: upstreamEtag } = imageUpstream;\r\n\r\n const upstreamType = await detectContentType(upstreamBuffer);\r\n\r\n if (!upstreamType || !upstreamType.startsWith('image/')) {\r\n throw new ImageError(400, \"The requested resource isn't a valid image.\");\r\n }\r\n if (upstreamType === 'image/svg+xml') {\r\n // Return SVG unmodified as sharp doesn't re-optimize SVGs well\r\n // SVG is allowed or blocked before this function in index.ts based on dangerouslyAllowSVG\r\n return {\r\n buffer: upstreamBuffer,\r\n contentType: upstreamType,\r\n etag: upstreamEtag,\r\n upstreamEtag\r\n };\r\n }\r\n\r\n let contentType = mimeType || upstreamType || 'image/jpeg';\r\n\r\n try {\r\n const transformer = sharp(upstreamBuffer).rotate();\r\n transformer.resize(width, undefined, { withoutEnlargement: true });\r\n\r\n if (contentType === 'image/avif') {\r\n transformer.avif({ quality: Math.max(quality - 20, 1), effort: 3 });\r\n } else if (contentType === 'image/webp') {\r\n transformer.webp({ quality });\r\n } else if (contentType === 'image/png') {\r\n transformer.png({ quality });\r\n } else {\r\n contentType = 'image/jpeg';\r\n transformer.jpeg({ quality, mozjpeg: true });\r\n }\r\n\r\n const optimizedBuffer = await transformer.toBuffer();\r\n\r\n return {\r\n buffer: optimizedBuffer,\r\n contentType,\r\n etag: getImageEtag(optimizedBuffer),\r\n upstreamEtag,\r\n };\r\n } catch (error) {\r\n console.error('Sharp optimization error:', error);\r\n // Fallback to upstream image if Sharp fails\r\n return {\r\n buffer: upstreamBuffer,\r\n contentType: upstreamType,\r\n etag: upstreamEtag,\r\n upstreamEtag,\r\n };\r\n }\r\n}\r\n","import { Request, Response } from 'express';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { defaultConfig, ImageConfig } from './config';\nimport { ImageError, getHash, getMaxAge, detectContentType } from './utils';\nimport { validateParams } from './validator';\nimport { fetchExternalImage, fetchInternalImage } from './fetcher';\nimport { readFromCacheDir, writeToCacheDir } from './cache';\nimport { optimizeImage } from './optimizer';\n\nexport const imageOptimizerHandler = (\n browserDistFolder: string,\n options?: Partial<ImageConfig>,\n) => {\n const config = { ...defaultConfig, ...options };\n const isDev = process.env['NODE_ENV'] !== 'production';\n const publicFolder =\n isDev || !fs.existsSync(browserDistFolder)\n ? path.join(process.cwd(), 'public')\n : browserDistFolder;\n\n return async (req: Request, res: Response) => {\n try {\n // 1. Validate Request\n const paramsResult = validateParams(req, req.query, config);\n if ('errorMessage' in paramsResult) {\n throw new ImageError(400, paramsResult.errorMessage);\n }\n const { href, isAbsolute, width, quality, mimeType, minimumCacheTTL } = paramsResult;\n\n // 2. Cache Key Generation (High Entropy)\n const CACHE_VERSION = 4;\n const cacheKey = getHash([CACHE_VERSION, href, width, quality, mimeType]);\n\n // 3. Cache Read Check\n try {\n const { maxAge, expireAt, buffer, extension, etag } = await readFromCacheDir(cacheKey);\n\n // TTL validation\n if (Date.now() <= expireAt) {\n res.setHeader('Vary', 'Accept');\n res.setHeader('Cache-Control', `public, max-age=${maxAge}, must-revalidate`);\n res.setHeader('Content-Type', `image/${extension}`);\n res.setHeader('X-Nextjs-Cache', 'HIT');\n res.setHeader('Content-Disposition', `inline; filename=\"image.${extension}\"`);\n res.setHeader('Content-Security-Policy', config.contentSecurityPolicy);\n res.setHeader('ETag', etag);\n res.status(200).send(buffer);\n return;\n }\n } catch (e) {\n // Cache miss\n }\n\n // 4. Fetch Image (External or Internal)\n const upstream = await (isAbsolute\n ? fetchExternalImage(href, false, 5 * 1024 * 1024) // 5MB limit\n : fetchInternalImage(href, publicFolder));\n\n const upstreamType = await detectContentType(upstream.buffer);\n if (upstreamType === 'image/svg+xml' && !config.dangerouslyAllowSVG) {\n throw new ImageError(400, '\"url\" parameter is valid but image type is not allowed (SVG)');\n }\n\n // 5. Optimize Image\n const optimized = await optimizeImage(upstream, paramsResult);\n\n const maxAge = Math.max(minimumCacheTTL, getMaxAge(upstream.cacheControl));\n const expireAt = maxAge * 1000 + Date.now();\n const extension = optimized.contentType.split('/')[1] || 'jpeg';\n\n // 6. Write to File Cache\n await writeToCacheDir(\n cacheKey,\n extension,\n maxAge,\n expireAt,\n optimized.buffer,\n optimized.etag,\n upstream.etag,\n );\n\n // 7. Send Response\n res.setHeader('Vary', 'Accept');\n res.setHeader('Cache-Control', `public, max-age=${maxAge}, must-revalidate`);\n res.setHeader('Content-Type', optimized.contentType);\n res.setHeader('X-Nextjs-Cache', 'MISS');\n res.setHeader('Content-Disposition', `inline; filename=\"image.${extension}\"`);\n res.setHeader('Content-Security-Policy', config.contentSecurityPolicy);\n res.setHeader('ETag', optimized.etag);\n res.status(200).send(optimized.buffer);\n return;\n } catch (error) {\n if (error instanceof ImageError) {\n console.error('Image Error:', error.message);\n res.status(error.statusCode).send(error.message);\n } else {\n console.error('Unexpected Optimizer Error:', error);\n res.status(500).send('Internal Server Error optimizing image');\n }\n return;\n }\n };\n};\n","/**\r\n * Server-side entry point for ng-image-optimizer\r\n * Exports all server-side functionality for image optimization\r\n */\r\n\r\nexport { imageOptimizerHandler } from './handler';\r\nexport type { ImageConfig } from './config';\r\nexport { defaultConfig } from './config';\r\nexport { ImageError } from './utils';\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":["fs"],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ import { IMAGE_LOADER } from '@angular/common';
2
+
3
+ function buildOptimizerUrl(routePrefix, config, defaultWidth, defaultQuality) {
4
+ const rawW = config.width ?? defaultWidth;
5
+ const w = rawW;
6
+ const params = config.loaderParams ?? {};
7
+ const rawQ = params['q'] ?? params['quality'];
8
+ const q = rawQ === undefined || rawQ === null
9
+ ? defaultQuality
10
+ : typeof rawQ === 'number'
11
+ ? rawQ
12
+ : Number(rawQ);
13
+ const qualityBase = Number.isFinite(q) ? Math.round(q) : defaultQuality;
14
+ const quality = config.isPlaceholder ? Math.min(qualityBase, 40) : qualityBase;
15
+ const qClamped = Math.min(100, Math.max(1, quality));
16
+ const urlParam = encodeURIComponent(config.src);
17
+ const prefix = routePrefix.startsWith('/') ? routePrefix : `/${routePrefix}`;
18
+ return `${prefix}?url=${urlParam}&w=${w}&q=${qClamped}`;
19
+ }
20
+ /**
21
+ * `ImageLoader` for `NgOptimizedImage` that matches the server's query contract (`url`, `w`, `q`).
22
+ */
23
+ function imageOptimizerLoader(options = {}) {
24
+ const opts = typeof options === 'string' ? { routePrefix: options } : options;
25
+ const routePrefix = opts.routePrefix ?? '/_ng/image';
26
+ const defaultWidth = opts.defaultWidth ?? 1080;
27
+ const defaultQuality = opts.defaultQuality ?? 90;
28
+ return (config) => buildOptimizerUrl(routePrefix, config, defaultWidth, defaultQuality);
29
+ }
30
+ /**
31
+ * Registers {@link imageOptimizerLoader} as `IMAGE_LOADER`.
32
+ */
33
+ function provideImageOptimizerLoader(options = {}) {
34
+ return {
35
+ provide: IMAGE_LOADER,
36
+ useValue: imageOptimizerLoader(options),
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Client-side entry point for ng-image-optimizer
42
+ * Exports all client-side functionality for image optimization
43
+ */
44
+
45
+ /*
46
+ * Public API Surface of ng-image-optimizer
47
+ */
48
+
49
+ /**
50
+ * Generated bundle index. Do not edit.
51
+ */
52
+
53
+ export { imageOptimizerLoader, provideImageOptimizerLoader };
54
+ //# sourceMappingURL=ng-image-optimizer.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ng-image-optimizer.mjs","sources":["../../../projects/ng-image-optimizer/client/image-loader-provider.ts","../../../projects/ng-image-optimizer/client/index.ts","../../../projects/ng-image-optimizer/public-api.ts","../../../projects/ng-image-optimizer/ng-image-optimizer.ts"],"sourcesContent":["import type { ImageLoader, ImageLoaderConfig } from '@angular/common';\r\nimport { IMAGE_LOADER } from '@angular/common';\r\nimport type { Provider } from '@angular/core';\r\n/**\r\n * Matches the server optimizer query shape (`validateParams`):\r\n * `GET <routePrefix>?url=<href>&w=<px>&q=<1-100>`.\r\n *\r\n * **`w`** is snapped to the configured allowlist (defaults match `defaultConfig` / server defaults).\r\n */\r\nexport interface ImageOptimizerLoaderOptions {\r\n /** Mount path of `imageOptimizerMiddleware`. Default `/_ng/image`. */\r\n routePrefix?: string;\r\n /**\r\n * Width when Angular calls the loader with only `src` (primary `img` `src`).\r\n * Snapped with `allowedWidths`. Default `1080`.\r\n */\r\n defaultWidth?: number;\r\n /** Used when `loaderParams` has no `q` / `quality`. Default `90`. */\r\n defaultQuality?: number;\r\n}\r\n\r\nfunction buildOptimizerUrl(\r\n routePrefix: string,\r\n config: ImageLoaderConfig,\r\n defaultWidth: number,\r\n defaultQuality: number,\r\n): string {\r\n const rawW = config.width ?? defaultWidth;\r\n const w = rawW;\r\n\r\n const params = config.loaderParams ?? {};\r\n const rawQ = params['q'] ?? params['quality'];\r\n const q =\r\n rawQ === undefined || rawQ === null\r\n ? defaultQuality\r\n : typeof rawQ === 'number'\r\n ? rawQ\r\n : Number(rawQ);\r\n const qualityBase = Number.isFinite(q) ? Math.round(q) : defaultQuality;\r\n const quality = config.isPlaceholder ? Math.min(qualityBase, 40) : qualityBase;\r\n const qClamped = Math.min(100, Math.max(1, quality));\r\n\r\n const urlParam = encodeURIComponent(config.src);\r\n const prefix = routePrefix.startsWith('/') ? routePrefix : `/${routePrefix}`;\r\n return `${prefix}?url=${urlParam}&w=${w}&q=${qClamped}`;\r\n}\r\n\r\n/**\r\n * `ImageLoader` for `NgOptimizedImage` that matches the server's query contract (`url`, `w`, `q`).\r\n */\r\nexport function imageOptimizerLoader(\r\n options: ImageOptimizerLoaderOptions | string = {},\r\n): ImageLoader {\r\n const opts: ImageOptimizerLoaderOptions =\r\n typeof options === 'string' ? { routePrefix: options } : options;\r\n const routePrefix = opts.routePrefix ?? '/_ng/image';\r\n const defaultWidth = opts.defaultWidth ?? 1080;\r\n const defaultQuality = opts.defaultQuality ?? 90;\r\n\r\n return (config) => buildOptimizerUrl(routePrefix, config, defaultWidth, defaultQuality);\r\n}\r\n\r\n/**\r\n * Registers {@link imageOptimizerLoader} as `IMAGE_LOADER`.\r\n */\r\nexport function provideImageOptimizerLoader(options: ImageOptimizerLoaderOptions = {}): Provider {\r\n return {\r\n provide: IMAGE_LOADER,\r\n useValue: imageOptimizerLoader(options),\r\n };\r\n}\r\n","/**\r\n * Client-side entry point for ng-image-optimizer\r\n * Exports all client-side functionality for image optimization\r\n */\r\n\r\nexport {\r\n imageOptimizerLoader,\r\n provideImageOptimizerLoader,\r\n} from './image-loader-provider';\r\nexport type { ImageOptimizerLoaderOptions } from './image-loader-provider';\r\n","/*\n * Public API Surface of ng-image-optimizer\n */\n\nexport * from './client';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAqBA,SAAS,iBAAiB,CACxB,WAAmB,EACnB,MAAyB,EACzB,YAAoB,EACpB,cAAsB,EAAA;AAEtB,IAAA,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,IAAI,YAAY;IACzC,MAAM,CAAC,GAAG,IAAI;AAEd,IAAA,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY,IAAI,EAAE;IACxC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC;IAC7C,MAAM,CAAC,GACL,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK;AAC7B,UAAE;AACF,UAAE,OAAO,IAAI,KAAK;AAChB,cAAE;AACF,cAAE,MAAM,CAAC,IAAI,CAAC;IACpB,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,cAAc;IACvE,MAAM,OAAO,GAAG,MAAM,CAAC,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC,GAAG,WAAW;AAC9E,IAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAEpD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,MAAM,CAAC,GAAG,CAAC;AAC/C,IAAA,MAAM,MAAM,GAAG,WAAW,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,WAAW,GAAG,CAAA,CAAA,EAAI,WAAW,EAAE;IAC5E,OAAO,CAAA,EAAG,MAAM,CAAA,KAAA,EAAQ,QAAQ,MAAM,CAAC,CAAA,GAAA,EAAM,QAAQ,CAAA,CAAE;AACzD;AAEA;;AAEG;AACG,SAAU,oBAAoB,CAClC,OAAA,GAAgD,EAAE,EAAA;AAElD,IAAA,MAAM,IAAI,GACR,OAAO,OAAO,KAAK,QAAQ,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,OAAO;AAClE,IAAA,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,YAAY;AACpD,IAAA,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,IAAI;AAC9C,IAAA,MAAM,cAAc,GAAG,IAAI,CAAC,cAAc,IAAI,EAAE;AAEhD,IAAA,OAAO,CAAC,MAAM,KAAK,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,cAAc,CAAC;AACzF;AAEA;;AAEG;AACG,SAAU,2BAA2B,CAAC,OAAA,GAAuC,EAAE,EAAA;IACnF,OAAO;AACL,QAAA,OAAO,EAAE,YAAY;AACrB,QAAA,QAAQ,EAAE,oBAAoB,CAAC,OAAO,CAAC;KACxC;AACH;;ACtEA;;;AAGG;;ACHH;;AAEG;;ACFH;;AAEG;;;;"}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "ng-image-optimizer",
3
+ "version": "0.0.1",
4
+ "peerDependencies": {
5
+ "@angular/common": "^21.2.0",
6
+ "@angular/core": "^21.2.0",
7
+ "lru-cache": "^11.2.7",
8
+ "sharp": "^0.34.5"
9
+ },
10
+ "dependencies": {
11
+ "tslib": "^2.3.0"
12
+ },
13
+ "schematics": "./schematics/collection.json",
14
+ "ng-add": {
15
+ "save": "dependencies"
16
+ },
17
+ "sideEffects": false,
18
+ "module": "fesm2022/ng-image-optimizer.mjs",
19
+ "typings": "types/ng-image-optimizer.d.ts",
20
+ "exports": {
21
+ "./package.json": {
22
+ "default": "./package.json"
23
+ },
24
+ ".": {
25
+ "types": "./types/ng-image-optimizer.d.ts",
26
+ "default": "./fesm2022/ng-image-optimizer.mjs"
27
+ },
28
+ "./server": {
29
+ "types": "./types/ng-image-optimizer-server.d.ts",
30
+ "default": "./fesm2022/ng-image-optimizer-server.mjs"
31
+ }
32
+ },
33
+ "type": "module"
34
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema",
3
+ "schematics": {
4
+ "ng-add": {
5
+ "description": "Add my library to the project.",
6
+ "factory": "./ng-add/index#ngAdd",
7
+ "schema": "./ng-add/schema.json"
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,4 @@
1
+ import { Rule } from '@angular-devkit/schematics';
2
+ export declare function ngAdd(options: {
3
+ project: string;
4
+ }): Rule;
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ngAdd = ngAdd;
13
+ const schematics_1 = require("@angular-devkit/schematics");
14
+ const tasks_1 = require("@angular-devkit/schematics/tasks");
15
+ const dependencies_1 = require("@schematics/angular/utility/dependencies");
16
+ const workspace_1 = require("@schematics/angular/utility/workspace");
17
+ const utility_1 = require("@schematics/angular/utility");
18
+ const SHARP_VERSION = '^0.34.5';
19
+ const LRU_CACHE_VERSION = '^11.2.7';
20
+ function ngAdd(options) {
21
+ return (tree) => __awaiter(this, void 0, void 0, function* () {
22
+ var _a;
23
+ const workspace = yield (0, workspace_1.getWorkspace)(tree);
24
+ if (!options.project) {
25
+ options.project = (_a = Array.from(workspace.projects.entries()).find(([_, project]) => project.extensions['projectType'] === 'application')) === null || _a === void 0 ? void 0 : _a[0];
26
+ }
27
+ if (!options.project) {
28
+ options.project = workspace.projects.keys().next().value;
29
+ }
30
+ if (!options.project) {
31
+ throw new schematics_1.SchematicsException('No project found in the workspace.');
32
+ }
33
+ return (0, schematics_1.chain)([
34
+ addDependencies(),
35
+ addProviderToAppConfig(options.project),
36
+ addMiddlewareToServer(options.project),
37
+ ]);
38
+ });
39
+ }
40
+ function addDependencies() {
41
+ return (tree, context) => {
42
+ const dependencies = [
43
+ { name: 'sharp', version: SHARP_VERSION },
44
+ { name: 'lru-cache', version: LRU_CACHE_VERSION },
45
+ ];
46
+ dependencies.forEach((dep) => {
47
+ (0, dependencies_1.addPackageJsonDependency)(tree, {
48
+ type: dependencies_1.NodeDependencyType.Default,
49
+ name: dep.name,
50
+ version: dep.version,
51
+ overwrite: false,
52
+ });
53
+ context.logger.info(`✅ Added ${dep.name} to dependencies`);
54
+ });
55
+ context.addTask(new tasks_1.NodePackageInstallTask());
56
+ };
57
+ }
58
+ function addProviderToAppConfig(projectName) {
59
+ return (tree) => __awaiter(this, void 0, void 0, function* () {
60
+ const workspace = yield (0, workspace_1.getWorkspace)(tree);
61
+ const project = workspace.projects.get(projectName);
62
+ if (!project) {
63
+ throw new schematics_1.SchematicsException(`Project "${projectName}" not found.`);
64
+ }
65
+ return (0, utility_1.addRootProvider)(projectName, ({ code, external }) => {
66
+ return code `${external('provideImageOptimizerLoader', 'ng-image-optimizer')}()`;
67
+ });
68
+ });
69
+ }
70
+ function addMiddlewareToServer(projectName) {
71
+ return (tree, context) => __awaiter(this, void 0, void 0, function* () {
72
+ var _a;
73
+ const workspace = yield (0, workspace_1.getWorkspace)(tree);
74
+ const project = workspace.projects.get(projectName);
75
+ if (!project) {
76
+ return;
77
+ }
78
+ const buildTarget = project.targets.get('build');
79
+ if (!buildTarget || !buildTarget.options) {
80
+ return;
81
+ }
82
+ const ssrEntry = (_a = buildTarget.options.ssr) === null || _a === void 0 ? void 0 : _a.entry;
83
+ if (!ssrEntry || !tree.exists(ssrEntry)) {
84
+ context.logger.warn(`Could not find server entry file (ssr.entry) to add image optimizer middleware.`);
85
+ return;
86
+ }
87
+ let serverContent = tree.readText(ssrEntry);
88
+ if (serverContent.includes('imageOptimizerHandler')) {
89
+ context.logger.info(`✅ Image optimizer middleware already present in ${ssrEntry}`);
90
+ return;
91
+ }
92
+ // Add import
93
+ if (!serverContent.includes("from 'ng-image-optimizer/server'")) {
94
+ const importStatement = `import { imageOptimizerHandler } from 'ng-image-optimizer/server';\n`;
95
+ serverContent = importStatement + serverContent;
96
+ }
97
+ // Ensure join is imported if we need to define browserDistFolder
98
+ if (!serverContent.includes('const browserDistFolder') &&
99
+ !serverContent.includes("import { join } from 'node:path'") &&
100
+ !serverContent.includes('import { join } from "node:path"')) {
101
+ serverContent = `import { join } from 'node:path';\n` + serverContent;
102
+ }
103
+ // In Angular 21, the server.ts usually has `const app = express();`
104
+ // We want to add the middleware after that.
105
+ const expressAppMatch = serverContent.match(/const\s+app\s+=\s+express\(\);/);
106
+ if (expressAppMatch) {
107
+ const insertionPoint = expressAppMatch.index + expressAppMatch[0].length;
108
+ // Check if browserDistFolder is already defined
109
+ if (serverContent.includes('const browserDistFolder')) {
110
+ serverContent = serverContent.replace(/const\s+app\s+=\s+express\(\);/, `const app = express();\napp.use('/_ng/image', imageOptimizerHandler(browserDistFolder, {}));`);
111
+ }
112
+ else {
113
+ const middlewareCode = `\n\nconst browserDistFolder = join(import.meta.dirname, '../browser');\napp.use('/_ng/image', imageOptimizerHandler(browserDistFolder, {}));`;
114
+ serverContent =
115
+ serverContent.slice(0, insertionPoint) +
116
+ middlewareCode +
117
+ serverContent.slice(insertionPoint);
118
+ }
119
+ tree.overwrite(ssrEntry, serverContent);
120
+ context.logger.info(`✅ Added image optimizer middleware to ${ssrEntry}`);
121
+ }
122
+ else {
123
+ context.logger.warn(`Could not find "const app = express();" in ${ssrEntry} to auto-inject middleware.`);
124
+ }
125
+ });
126
+ }
127
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../projects/ng-image-optimizer/schematics/ng-add/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export interface Schema {
2
+ project: string;
3
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../../projects/ng-image-optimizer/schematics/ng-add/schema.ts"],"names":[],"mappings":""}
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema",
3
+ "$id": "ng-add",
4
+ "title": "my-lib ng-add schematic",
5
+ "type": "object",
6
+ "properties": {
7
+ "project": {
8
+ "type": "string",
9
+ "description": "The name of the project.",
10
+ "$default": {
11
+ "$source": "projectName"
12
+ }
13
+ }
14
+ },
15
+ "required": []
16
+ }
@@ -0,0 +1,35 @@
1
+ import { Request, Response } from 'express';
2
+
3
+ interface RemotePattern {
4
+ protocol?: 'http' | 'https';
5
+ hostname: string;
6
+ port?: string;
7
+ pathname?: string;
8
+ }
9
+ interface ImageConfig {
10
+ deviceSizes: number[];
11
+ imageSizes: number[];
12
+ remotePatterns: RemotePattern[];
13
+ localPatterns?: {
14
+ pathname: string;
15
+ search: string;
16
+ }[];
17
+ minimumCacheTTL: number;
18
+ formats: ('image/avif' | 'image/webp')[];
19
+ dangerouslyAllowSVG: boolean;
20
+ contentSecurityPolicy: string;
21
+ contentDispositionType: 'inline' | 'attachment';
22
+ qualities?: number[];
23
+ maxCacheSize?: number;
24
+ }
25
+ declare const defaultConfig: ImageConfig;
26
+
27
+ declare const imageOptimizerHandler: (browserDistFolder: string, options?: Partial<ImageConfig>) => (req: Request, res: Response) => Promise<void>;
28
+
29
+ declare class ImageError extends Error {
30
+ statusCode: number;
31
+ constructor(statusCode: number, message: string);
32
+ }
33
+
34
+ export { ImageError, defaultConfig, imageOptimizerHandler };
35
+ export type { ImageConfig };
@@ -0,0 +1,31 @@
1
+ import { ImageLoader } from '@angular/common';
2
+ import { Provider } from '@angular/core';
3
+
4
+ /**
5
+ * Matches the server optimizer query shape (`validateParams`):
6
+ * `GET <routePrefix>?url=<href>&w=<px>&q=<1-100>`.
7
+ *
8
+ * **`w`** is snapped to the configured allowlist (defaults match `defaultConfig` / server defaults).
9
+ */
10
+ interface ImageOptimizerLoaderOptions {
11
+ /** Mount path of `imageOptimizerMiddleware`. Default `/_ng/image`. */
12
+ routePrefix?: string;
13
+ /**
14
+ * Width when Angular calls the loader with only `src` (primary `img` `src`).
15
+ * Snapped with `allowedWidths`. Default `1080`.
16
+ */
17
+ defaultWidth?: number;
18
+ /** Used when `loaderParams` has no `q` / `quality`. Default `90`. */
19
+ defaultQuality?: number;
20
+ }
21
+ /**
22
+ * `ImageLoader` for `NgOptimizedImage` that matches the server's query contract (`url`, `w`, `q`).
23
+ */
24
+ declare function imageOptimizerLoader(options?: ImageOptimizerLoaderOptions | string): ImageLoader;
25
+ /**
26
+ * Registers {@link imageOptimizerLoader} as `IMAGE_LOADER`.
27
+ */
28
+ declare function provideImageOptimizerLoader(options?: ImageOptimizerLoaderOptions): Provider;
29
+
30
+ export { imageOptimizerLoader, provideImageOptimizerLoader };
31
+ export type { ImageOptimizerLoaderOptions };