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 +64 -0
- package/fesm2022/ng-image-optimizer-server.mjs +506 -0
- package/fesm2022/ng-image-optimizer-server.mjs.map +1 -0
- package/fesm2022/ng-image-optimizer.mjs +54 -0
- package/fesm2022/ng-image-optimizer.mjs.map +1 -0
- package/package.json +34 -0
- package/schematics/collection.json +10 -0
- package/schematics/ng-add/index.d.ts +4 -0
- package/schematics/ng-add/index.js +127 -0
- package/schematics/ng-add/index.js.map +1 -0
- package/schematics/ng-add/schema.d.ts +3 -0
- package/schematics/ng-add/schema.js +3 -0
- package/schematics/ng-add/schema.js.map +1 -0
- package/schematics/ng-add/schema.json +16 -0
- package/types/ng-image-optimizer-server.d.ts +35 -0
- package/types/ng-image-optimizer.d.ts +31 -0
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,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 @@
|
|
|
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 };
|