vite-plugin-sri4 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +393 -160
- package/dist/index.js +393 -160
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -6,29 +6,9 @@ var crypto = require('crypto');
|
|
|
6
6
|
var path = require('path');
|
|
7
7
|
var fetch = require('cross-fetch');
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const DEFAULT_HASH_ALGORITHM = 'sha384';
|
|
13
|
-
const DEFAULT_PLUGIN_NAME = 'vite-plugin-sri4';
|
|
14
|
-
|
|
15
|
-
// Optimized regex patterns for better readability and efficiency
|
|
16
|
-
const HTML_PATTERNS = {
|
|
17
|
-
script: {
|
|
18
|
-
regex: /<script\b[^>]*?\bsrc\s*=\s*["']([^"']+)["'][^>]*><\/script>/g,
|
|
19
|
-
endOffset: 10
|
|
20
|
-
},
|
|
21
|
-
stylesheet: {
|
|
22
|
-
regex: /<link\b[^>]*?\brel\s*=\s*["']stylesheet["'][^>]*?\bhref\s*=\s*["']([^"']+)["'][^>]*>/g,
|
|
23
|
-
endOffset: 1
|
|
24
|
-
},
|
|
25
|
-
modulepreload: {
|
|
26
|
-
regex: /<link\b[^>]*?\brel\s*=\s*["']modulepreload["'][^>]*?\bhref\s*=\s*["']([^"']+)["'][^>]*>/g,
|
|
27
|
-
endOffset: 1
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// Extended caching mechanism with expiration time
|
|
9
|
+
/**
|
|
10
|
+
* Extended caching mechanism with expiration time
|
|
11
|
+
*/
|
|
32
12
|
class ResourceCache {
|
|
33
13
|
constructor(ttl = 3600000) { // Default cache for 1 hour
|
|
34
14
|
this.cache = new Map();
|
|
@@ -64,11 +44,35 @@ class ResourceCache {
|
|
|
64
44
|
}
|
|
65
45
|
}
|
|
66
46
|
|
|
67
|
-
|
|
68
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Cache manager for plugin instances
|
|
49
|
+
*/
|
|
50
|
+
class CacheManager {
|
|
51
|
+
constructor() {
|
|
52
|
+
this.urlSupportCache = new ResourceCache();
|
|
53
|
+
this.resourceCache = new ResourceCache();
|
|
54
|
+
}
|
|
69
55
|
|
|
70
|
-
|
|
71
|
-
|
|
56
|
+
getUrlSupportCache() {
|
|
57
|
+
return this.urlSupportCache
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getResourceCache() {
|
|
61
|
+
return this.resourceCache
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
clearAll() {
|
|
65
|
+
this.urlSupportCache.clear();
|
|
66
|
+
this.resourceCache.clear();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const DEFAULT_TIMEOUT = 5000;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if URL is from a bypass domain
|
|
74
|
+
*/
|
|
75
|
+
function isUrlFromBypassDomain(url, bypassDomains = [], logger = null) {
|
|
72
76
|
if (!url || typeof url !== 'string' || !url.startsWith('http')) return false
|
|
73
77
|
|
|
74
78
|
try {
|
|
@@ -77,13 +81,17 @@ function isUrlFromBypassDomain(url, bypassDomains = []) {
|
|
|
77
81
|
urlObj.hostname === domain || urlObj.hostname.endsWith(`.${domain}`)
|
|
78
82
|
)
|
|
79
83
|
} catch (error) {
|
|
80
|
-
|
|
84
|
+
if (logger) {
|
|
85
|
+
logger.warn(`Invalid URL: ${url}`, error);
|
|
86
|
+
}
|
|
81
87
|
return false
|
|
82
88
|
}
|
|
83
89
|
}
|
|
84
90
|
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Resource check with retry mechanism
|
|
93
|
+
*/
|
|
94
|
+
async function checkResourceSupport(url, urlSupportCache, logger = null, retries = 2) {
|
|
87
95
|
if (urlSupportCache.has(url)) {
|
|
88
96
|
return urlSupportCache.get(url)
|
|
89
97
|
}
|
|
@@ -108,7 +116,9 @@ async function checkResourceSupport(url, retries = 2) {
|
|
|
108
116
|
} catch (error) {
|
|
109
117
|
lastError = error;
|
|
110
118
|
if (error.name === 'AbortError') {
|
|
111
|
-
|
|
119
|
+
if (logger) {
|
|
120
|
+
logger.warn(`Resource check timed out: ${url}`);
|
|
121
|
+
}
|
|
112
122
|
break // Don't retry timeouts
|
|
113
123
|
}
|
|
114
124
|
|
|
@@ -119,13 +129,17 @@ async function checkResourceSupport(url, retries = 2) {
|
|
|
119
129
|
}
|
|
120
130
|
}
|
|
121
131
|
|
|
122
|
-
|
|
132
|
+
if (logger) {
|
|
133
|
+
logger.warn(`Failed to check resource support: ${url}`, lastError);
|
|
134
|
+
}
|
|
123
135
|
urlSupportCache.set(url, false);
|
|
124
136
|
return false
|
|
125
137
|
}
|
|
126
138
|
|
|
127
|
-
|
|
128
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Optimized resource fetching function with retry mechanism and caching
|
|
141
|
+
*/
|
|
142
|
+
async function fetchResource(url, resourceCache, logger = null, retries = 1) {
|
|
129
143
|
// Check cache
|
|
130
144
|
if (resourceCache.has(url)) {
|
|
131
145
|
return resourceCache.get(url)
|
|
@@ -150,7 +164,9 @@ async function fetchResource(url, retries = 1) {
|
|
|
150
164
|
} catch (error) {
|
|
151
165
|
lastError = error;
|
|
152
166
|
if (error.name === 'AbortError') {
|
|
153
|
-
|
|
167
|
+
if (logger) {
|
|
168
|
+
logger.warn(`Resource fetch timed out: ${url}`);
|
|
169
|
+
}
|
|
154
170
|
break // Don't retry timeouts
|
|
155
171
|
}
|
|
156
172
|
|
|
@@ -160,144 +176,373 @@ async function fetchResource(url, retries = 1) {
|
|
|
160
176
|
}
|
|
161
177
|
}
|
|
162
178
|
|
|
163
|
-
|
|
179
|
+
if (logger) {
|
|
180
|
+
logger.warn(`Failed to fetch external resource: ${url}`, lastError);
|
|
181
|
+
}
|
|
164
182
|
return null
|
|
165
183
|
}
|
|
166
184
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// Handle absolute path URLs
|
|
177
|
-
if (url.startsWith('/')) {
|
|
178
|
-
// Remove leading slash to match keys in bundle
|
|
179
|
-
return url.substring(1)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Handle relative paths (when config.base is relative)
|
|
183
|
-
if (config.base === './' || config.base === '') {
|
|
184
|
-
return path.posix.resolve(path.posix.dirname(htmlPath), url)
|
|
185
|
-
}
|
|
185
|
+
/**
|
|
186
|
+
* Improved method for getting bundle keys
|
|
187
|
+
*/
|
|
188
|
+
function getBundleKey(htmlPath, url, config) {
|
|
189
|
+
// Handle absolute path URLs
|
|
190
|
+
if (url.startsWith('/')) {
|
|
191
|
+
// Remove leading slash to match keys in bundle
|
|
192
|
+
return url.substring(1)
|
|
193
|
+
}
|
|
186
194
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
};
|
|
195
|
+
// Handle relative paths (when config.base is relative)
|
|
196
|
+
if (config.base === './' || config.base === '') {
|
|
197
|
+
return path.posix.resolve(path.posix.dirname(htmlPath), url)
|
|
198
|
+
}
|
|
192
199
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
200
|
+
// Handle other cases, remove base prefix from URL
|
|
201
|
+
return url.startsWith(config.base)
|
|
202
|
+
? url.substring(config.base.length)
|
|
203
|
+
: url
|
|
204
|
+
}
|
|
198
205
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
/**
|
|
207
|
+
* Calculate SRI integrity hash for a given resource
|
|
208
|
+
*/
|
|
209
|
+
async function calculateIntegrity(
|
|
210
|
+
bundle,
|
|
211
|
+
htmlPath,
|
|
212
|
+
url,
|
|
213
|
+
options,
|
|
214
|
+
config,
|
|
215
|
+
cacheManager,
|
|
216
|
+
logger = null
|
|
217
|
+
) {
|
|
218
|
+
const {
|
|
219
|
+
ignoreMissingAsset,
|
|
220
|
+
bypassDomains,
|
|
221
|
+
hashAlgorithm
|
|
222
|
+
} = options;
|
|
208
223
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
);
|
|
224
|
+
// Skip specified domains
|
|
225
|
+
if (isUrlFromBypassDomain(url, bypassDomains, logger)) {
|
|
226
|
+
return null
|
|
227
|
+
}
|
|
214
228
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
229
|
+
let source;
|
|
230
|
+
if (url.startsWith('http')) {
|
|
231
|
+
const isSupported = await checkResourceSupport(url, cacheManager.getUrlSupportCache(), logger);
|
|
232
|
+
if (!isSupported) return null
|
|
233
|
+
source = await fetchResource(url, cacheManager.getResourceCache(), logger);
|
|
234
|
+
if (!source) return null
|
|
235
|
+
} else {
|
|
236
|
+
const bundleKey = getBundleKey(htmlPath, url, config);
|
|
237
|
+
const bundleItem = bundle[bundleKey];
|
|
238
|
+
|
|
239
|
+
if (!bundleItem) {
|
|
240
|
+
// Try to find a matching item with more flexible matching
|
|
241
|
+
const possibleMatch = Object.keys(bundle).find(key =>
|
|
242
|
+
key.endsWith(bundleKey) || bundleKey.endsWith(key)
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (possibleMatch) {
|
|
246
|
+
source = bundle[possibleMatch].type === 'chunk'
|
|
247
|
+
? bundle[possibleMatch].code
|
|
248
|
+
: bundle[possibleMatch].source;
|
|
249
|
+
} else if (ignoreMissingAsset) {
|
|
250
|
+
if (logger) {
|
|
251
|
+
logger.warn(`Asset not found in bundle: ${url} (path: ${htmlPath}, key: ${bundleKey})`);
|
|
224
252
|
}
|
|
253
|
+
return null
|
|
225
254
|
} else {
|
|
226
|
-
|
|
255
|
+
throw new Error(`Asset ${url} not found in bundle (path: ${htmlPath}, key: ${bundleKey})`)
|
|
227
256
|
}
|
|
257
|
+
} else {
|
|
258
|
+
source = bundleItem.type === 'chunk' ? bundleItem.code : bundleItem.source;
|
|
228
259
|
}
|
|
260
|
+
}
|
|
229
261
|
|
|
230
|
-
|
|
231
|
-
|
|
262
|
+
// Ensure source is a Uint8Array or string
|
|
263
|
+
if (!source) return null
|
|
264
|
+
|
|
265
|
+
if (typeof source === 'string') {
|
|
266
|
+
return `${hashAlgorithm}-${crypto.createHash(hashAlgorithm).update(source).digest('base64')}`
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return `${hashAlgorithm}-${crypto.createHash(hashAlgorithm).update(Buffer.from(source)).digest('base64')}`
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Optimized regex patterns for better readability and efficiency
|
|
273
|
+
const HTML_PATTERNS = {
|
|
274
|
+
script: {
|
|
275
|
+
regex: /<script\b[^>]*?\bsrc\s*=\s*["']([^"']+)["'][^>]*><\/script>/g,
|
|
276
|
+
endOffset: 10
|
|
277
|
+
},
|
|
278
|
+
stylesheet: {
|
|
279
|
+
regex: /<link\b[^>]*?\brel\s*=\s*["']stylesheet["'][^>]*?\bhref\s*=\s*["']([^"']+)["'][^>]*>/g,
|
|
280
|
+
endOffset: 1
|
|
281
|
+
},
|
|
282
|
+
modulepreload: {
|
|
283
|
+
regex: /<link\b[^>]*?\brel\s*=\s*["']modulepreload["'][^>]*?\bhref\s*=\s*["']([^"']+)["'][^>]*>/g,
|
|
284
|
+
endOffset: 1
|
|
285
|
+
}
|
|
286
|
+
};
|
|
232
287
|
|
|
233
|
-
|
|
234
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Validate HTML input
|
|
290
|
+
*/
|
|
291
|
+
function validateHtmlInput(html, htmlPath, logger) {
|
|
292
|
+
if (!html || typeof html !== 'string') {
|
|
293
|
+
logger.warn(`Invalid HTML content for ${htmlPath}`);
|
|
294
|
+
return false
|
|
295
|
+
}
|
|
296
|
+
return true
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Process a single match to create an integrity change object
|
|
301
|
+
*/
|
|
302
|
+
async function processMatch(
|
|
303
|
+
match,
|
|
304
|
+
endOffset,
|
|
305
|
+
bundle,
|
|
306
|
+
htmlPath,
|
|
307
|
+
options,
|
|
308
|
+
config,
|
|
309
|
+
cacheManager,
|
|
310
|
+
logger
|
|
311
|
+
) {
|
|
312
|
+
const [, url] = match;
|
|
313
|
+
if (!url) return null
|
|
314
|
+
|
|
315
|
+
const end = match.index + match[0].length;
|
|
316
|
+
const integrity = await calculateIntegrity(
|
|
317
|
+
bundle,
|
|
318
|
+
htmlPath,
|
|
319
|
+
url,
|
|
320
|
+
options,
|
|
321
|
+
config,
|
|
322
|
+
cacheManager,
|
|
323
|
+
logger
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
if (integrity) {
|
|
327
|
+
return {
|
|
328
|
+
integrity,
|
|
329
|
+
position: end - endOffset,
|
|
330
|
+
url // For logging
|
|
235
331
|
}
|
|
332
|
+
}
|
|
333
|
+
return null
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Process matches for a specific HTML pattern
|
|
338
|
+
*/
|
|
339
|
+
async function processPatternMatches(
|
|
340
|
+
html,
|
|
341
|
+
pattern,
|
|
342
|
+
bundle,
|
|
343
|
+
htmlPath,
|
|
344
|
+
options,
|
|
345
|
+
config,
|
|
346
|
+
cacheManager,
|
|
347
|
+
logger
|
|
348
|
+
) {
|
|
349
|
+
const { regex, endOffset } = pattern;
|
|
350
|
+
const matches = [...html.matchAll(regex)];
|
|
351
|
+
|
|
352
|
+
// Process each match in parallel
|
|
353
|
+
const matchResults = await Promise.all(
|
|
354
|
+
matches.map(match =>
|
|
355
|
+
processMatch(match, endOffset, bundle, htmlPath, options, config, cacheManager, logger)
|
|
356
|
+
)
|
|
357
|
+
);
|
|
236
358
|
|
|
237
|
-
|
|
238
|
-
|
|
359
|
+
// Filter out null results
|
|
360
|
+
return matchResults.filter(Boolean)
|
|
361
|
+
}
|
|
239
362
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
363
|
+
/**
|
|
364
|
+
* Collect all integrity changes from HTML patterns
|
|
365
|
+
*/
|
|
366
|
+
async function collectIntegrityChanges(
|
|
367
|
+
html,
|
|
368
|
+
bundle,
|
|
369
|
+
htmlPath,
|
|
370
|
+
options,
|
|
371
|
+
config,
|
|
372
|
+
cacheManager,
|
|
373
|
+
logger
|
|
374
|
+
) {
|
|
375
|
+
const changes = [];
|
|
376
|
+
|
|
377
|
+
// Collect changes from all patterns in parallel
|
|
378
|
+
await Promise.all(
|
|
379
|
+
Object.values(HTML_PATTERNS).map(async pattern => {
|
|
380
|
+
const patternChanges = await processPatternMatches(
|
|
381
|
+
html,
|
|
382
|
+
pattern,
|
|
383
|
+
bundle,
|
|
384
|
+
htmlPath,
|
|
385
|
+
options,
|
|
386
|
+
config,
|
|
387
|
+
cacheManager,
|
|
388
|
+
logger
|
|
389
|
+
);
|
|
390
|
+
changes.push(...patternChanges);
|
|
391
|
+
})
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
return changes
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Check if integrity attribute already exists in HTML segment
|
|
399
|
+
*/
|
|
400
|
+
function hasExistingIntegrity(html, position, integrity) {
|
|
401
|
+
const segment = html.slice(Math.max(0, position - 100), position + 100);
|
|
402
|
+
return segment.includes(`integrity="${integrity}"`)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Apply integrity changes to HTML content
|
|
407
|
+
*/
|
|
408
|
+
function applyIntegrityChanges(html, changes, logger) {
|
|
409
|
+
// Sort by position in descending order to insert from back to front
|
|
410
|
+
changes.sort((a, b) => b.position - a.position);
|
|
411
|
+
|
|
412
|
+
for (const { integrity, position, url } of changes) {
|
|
413
|
+
// Skip if integrity attribute already exists
|
|
414
|
+
if (hasExistingIntegrity(html, position, integrity)) {
|
|
415
|
+
continue
|
|
244
416
|
}
|
|
245
417
|
|
|
246
|
-
const
|
|
418
|
+
const insertText = ` integrity="${integrity}"`;
|
|
419
|
+
html = html.slice(0, position) + insertText + html.slice(position);
|
|
420
|
+
logger.debug(`Added integrity for: ${url}`);
|
|
421
|
+
}
|
|
247
422
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
Object.values(HTML_PATTERNS).map(async ({ regex, endOffset }) => {
|
|
251
|
-
const matches = [...html.matchAll(regex)];
|
|
423
|
+
return html
|
|
424
|
+
}
|
|
252
425
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
426
|
+
/**
|
|
427
|
+
* Transform HTML by adding SRI integrity attributes
|
|
428
|
+
*/
|
|
429
|
+
async function transformHTML(
|
|
430
|
+
bundle,
|
|
431
|
+
htmlPath,
|
|
432
|
+
html,
|
|
433
|
+
options,
|
|
434
|
+
config,
|
|
435
|
+
cacheManager,
|
|
436
|
+
logger
|
|
437
|
+
) {
|
|
438
|
+
if (!validateHtmlInput(html, htmlPath, logger)) {
|
|
439
|
+
return html
|
|
440
|
+
}
|
|
258
441
|
|
|
259
|
-
|
|
260
|
-
|
|
442
|
+
const changes = await collectIntegrityChanges(
|
|
443
|
+
html,
|
|
444
|
+
bundle,
|
|
445
|
+
htmlPath,
|
|
446
|
+
options,
|
|
447
|
+
config,
|
|
448
|
+
cacheManager,
|
|
449
|
+
logger
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
return applyIntegrityChanges(html, changes, logger)
|
|
453
|
+
}
|
|
261
454
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
455
|
+
/**
|
|
456
|
+
* Create HTML transformer with given options and config
|
|
457
|
+
*/
|
|
458
|
+
function createTransformer(options, config, cacheManager, logger) {
|
|
459
|
+
return {
|
|
460
|
+
transformHTML: (bundle, htmlPath, html) =>
|
|
461
|
+
transformHTML(bundle, htmlPath, html, options, config, cacheManager, logger),
|
|
462
|
+
calculateIntegrity: (bundle, htmlPath, url) =>
|
|
463
|
+
calculateIntegrity(bundle, htmlPath, url, options, config, cacheManager, logger)
|
|
464
|
+
}
|
|
465
|
+
}
|
|
272
466
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
467
|
+
const DEFAULT_PLUGIN_NAME$1 = 'vite-plugin-sri4';
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Logger class to handle different log levels without hijacking global console
|
|
471
|
+
*/
|
|
472
|
+
class Logger {
|
|
473
|
+
constructor(logLevel = 'warn', pluginName = DEFAULT_PLUGIN_NAME$1) {
|
|
474
|
+
this.logLevel = logLevel;
|
|
475
|
+
this.pluginName = pluginName;
|
|
476
|
+
this.levels = {
|
|
477
|
+
silent: 0,
|
|
478
|
+
error: 1,
|
|
479
|
+
warn: 2,
|
|
480
|
+
info: 3,
|
|
481
|
+
debug: 4
|
|
482
|
+
};
|
|
483
|
+
this.currentLevel = this.levels[logLevel] || this.levels.warn;
|
|
484
|
+
}
|
|
277
485
|
|
|
278
|
-
|
|
279
|
-
|
|
486
|
+
/**
|
|
487
|
+
* Format message with plugin name prefix
|
|
488
|
+
*/
|
|
489
|
+
formatMessage(message, ...args) {
|
|
490
|
+
const prefix = `[${this.pluginName}]`;
|
|
491
|
+
if (typeof message === 'string') {
|
|
492
|
+
return [prefix + ' ' + message, ...args]
|
|
493
|
+
}
|
|
494
|
+
return [prefix, message, ...args]
|
|
495
|
+
}
|
|
280
496
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
497
|
+
/**
|
|
498
|
+
* Log error messages
|
|
499
|
+
*/
|
|
500
|
+
error(message, ...args) {
|
|
501
|
+
if (this.currentLevel >= this.levels.error) {
|
|
502
|
+
console.error(...this.formatMessage(message, ...args));
|
|
503
|
+
}
|
|
504
|
+
}
|
|
284
505
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
506
|
+
/**
|
|
507
|
+
* Log warning messages
|
|
508
|
+
*/
|
|
509
|
+
warn(message, ...args) {
|
|
510
|
+
if (this.currentLevel >= this.levels.warn) {
|
|
511
|
+
console.warn(...this.formatMessage(message, ...args));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
290
514
|
|
|
291
|
-
|
|
292
|
-
|
|
515
|
+
/**
|
|
516
|
+
* Log info messages
|
|
517
|
+
*/
|
|
518
|
+
info(message, ...args) {
|
|
519
|
+
if (this.currentLevel >= this.levels.info) {
|
|
520
|
+
console.info(...this.formatMessage(message, ...args));
|
|
293
521
|
}
|
|
522
|
+
}
|
|
294
523
|
|
|
295
|
-
|
|
296
|
-
|
|
524
|
+
/**
|
|
525
|
+
* Log debug messages
|
|
526
|
+
*/
|
|
527
|
+
debug(message, ...args) {
|
|
528
|
+
if (this.currentLevel >= this.levels.debug) {
|
|
529
|
+
console.debug(...this.formatMessage(message, ...args));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
297
532
|
|
|
298
|
-
|
|
533
|
+
/**
|
|
534
|
+
* Create a child logger with the same configuration
|
|
535
|
+
*/
|
|
536
|
+
child(name) {
|
|
537
|
+
return new Logger(this.logLevel, `${this.pluginName}:${name}`)
|
|
538
|
+
}
|
|
299
539
|
}
|
|
300
540
|
|
|
541
|
+
// Constants definition
|
|
542
|
+
const VITE_INTERNAL_ANALYSIS_PLUGIN = 'vite:build-import-analysis';
|
|
543
|
+
const DEFAULT_HASH_ALGORITHM = 'sha384';
|
|
544
|
+
const DEFAULT_PLUGIN_NAME = 'vite-plugin-sri4';
|
|
545
|
+
|
|
301
546
|
function sri(options = {}) {
|
|
302
547
|
const {
|
|
303
548
|
ignoreMissingAsset = false,
|
|
@@ -306,16 +551,9 @@ function sri(options = {}) {
|
|
|
306
551
|
logLevel = 'warn'
|
|
307
552
|
} = options;
|
|
308
553
|
|
|
309
|
-
//
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
if (logLevel === 'error') {
|
|
314
|
-
console.warn = () => {};
|
|
315
|
-
console.debug = () => {};
|
|
316
|
-
} else if (logLevel === 'warn') {
|
|
317
|
-
console.debug = () => {};
|
|
318
|
-
}
|
|
554
|
+
// Create cache manager and logger instances for this plugin instance
|
|
555
|
+
const cacheManager = new CacheManager();
|
|
556
|
+
const logger = new Logger(logLevel, DEFAULT_PLUGIN_NAME);
|
|
319
557
|
|
|
320
558
|
return {
|
|
321
559
|
name: DEFAULT_PLUGIN_NAME,
|
|
@@ -324,13 +562,8 @@ function sri(options = {}) {
|
|
|
324
562
|
|
|
325
563
|
// Cleanup work
|
|
326
564
|
buildEnd() {
|
|
327
|
-
// Restore console functions
|
|
328
|
-
console.warn = originalConsoleWarn;
|
|
329
|
-
console.debug = originalConsoleDebug;
|
|
330
|
-
|
|
331
565
|
// Clear caches
|
|
332
|
-
|
|
333
|
-
resourceCache.clear();
|
|
566
|
+
cacheManager.clearAll();
|
|
334
567
|
},
|
|
335
568
|
|
|
336
569
|
configResolved(config) {
|
|
@@ -338,7 +571,7 @@ function sri(options = {}) {
|
|
|
338
571
|
ignoreMissingAsset,
|
|
339
572
|
bypassDomains,
|
|
340
573
|
hashAlgorithm
|
|
341
|
-
}, config);
|
|
574
|
+
}, config, cacheManager, logger);
|
|
342
575
|
|
|
343
576
|
const generateBundle = async function(_, bundle) {
|
|
344
577
|
const htmlFiles = Object.entries(bundle).filter(
|
|
@@ -348,7 +581,7 @@ function sri(options = {}) {
|
|
|
348
581
|
);
|
|
349
582
|
|
|
350
583
|
if (htmlFiles.length === 0) {
|
|
351
|
-
|
|
584
|
+
logger.debug('No HTML files found in bundle');
|
|
352
585
|
return
|
|
353
586
|
}
|
|
354
587
|
|
|
@@ -360,10 +593,10 @@ function sri(options = {}) {
|
|
|
360
593
|
chunk.source = await transformer.transformHTML(bundle, name, originalContent);
|
|
361
594
|
|
|
362
595
|
if (originalContent !== chunk.source) {
|
|
363
|
-
|
|
596
|
+
logger.debug(`SRI attributes added to ${name}`);
|
|
364
597
|
}
|
|
365
598
|
} catch (error) {
|
|
366
|
-
|
|
599
|
+
logger.warn(`Error processing ${name}:`, error);
|
|
367
600
|
// Keep original content on error
|
|
368
601
|
}
|
|
369
602
|
})
|
package/dist/index.js
CHANGED
|
@@ -2,29 +2,9 @@ import { createHash } from 'crypto';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import fetch from 'cross-fetch';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const DEFAULT_HASH_ALGORITHM = 'sha384';
|
|
9
|
-
const DEFAULT_PLUGIN_NAME = 'vite-plugin-sri4';
|
|
10
|
-
|
|
11
|
-
// Optimized regex patterns for better readability and efficiency
|
|
12
|
-
const HTML_PATTERNS = {
|
|
13
|
-
script: {
|
|
14
|
-
regex: /<script\b[^>]*?\bsrc\s*=\s*["']([^"']+)["'][^>]*><\/script>/g,
|
|
15
|
-
endOffset: 10
|
|
16
|
-
},
|
|
17
|
-
stylesheet: {
|
|
18
|
-
regex: /<link\b[^>]*?\brel\s*=\s*["']stylesheet["'][^>]*?\bhref\s*=\s*["']([^"']+)["'][^>]*>/g,
|
|
19
|
-
endOffset: 1
|
|
20
|
-
},
|
|
21
|
-
modulepreload: {
|
|
22
|
-
regex: /<link\b[^>]*?\brel\s*=\s*["']modulepreload["'][^>]*?\bhref\s*=\s*["']([^"']+)["'][^>]*>/g,
|
|
23
|
-
endOffset: 1
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
// Extended caching mechanism with expiration time
|
|
5
|
+
/**
|
|
6
|
+
* Extended caching mechanism with expiration time
|
|
7
|
+
*/
|
|
28
8
|
class ResourceCache {
|
|
29
9
|
constructor(ttl = 3600000) { // Default cache for 1 hour
|
|
30
10
|
this.cache = new Map();
|
|
@@ -60,11 +40,35 @@ class ResourceCache {
|
|
|
60
40
|
}
|
|
61
41
|
}
|
|
62
42
|
|
|
63
|
-
|
|
64
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Cache manager for plugin instances
|
|
45
|
+
*/
|
|
46
|
+
class CacheManager {
|
|
47
|
+
constructor() {
|
|
48
|
+
this.urlSupportCache = new ResourceCache();
|
|
49
|
+
this.resourceCache = new ResourceCache();
|
|
50
|
+
}
|
|
65
51
|
|
|
66
|
-
|
|
67
|
-
|
|
52
|
+
getUrlSupportCache() {
|
|
53
|
+
return this.urlSupportCache
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getResourceCache() {
|
|
57
|
+
return this.resourceCache
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
clearAll() {
|
|
61
|
+
this.urlSupportCache.clear();
|
|
62
|
+
this.resourceCache.clear();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const DEFAULT_TIMEOUT = 5000;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if URL is from a bypass domain
|
|
70
|
+
*/
|
|
71
|
+
function isUrlFromBypassDomain(url, bypassDomains = [], logger = null) {
|
|
68
72
|
if (!url || typeof url !== 'string' || !url.startsWith('http')) return false
|
|
69
73
|
|
|
70
74
|
try {
|
|
@@ -73,13 +77,17 @@ function isUrlFromBypassDomain(url, bypassDomains = []) {
|
|
|
73
77
|
urlObj.hostname === domain || urlObj.hostname.endsWith(`.${domain}`)
|
|
74
78
|
)
|
|
75
79
|
} catch (error) {
|
|
76
|
-
|
|
80
|
+
if (logger) {
|
|
81
|
+
logger.warn(`Invalid URL: ${url}`, error);
|
|
82
|
+
}
|
|
77
83
|
return false
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
86
|
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Resource check with retry mechanism
|
|
89
|
+
*/
|
|
90
|
+
async function checkResourceSupport(url, urlSupportCache, logger = null, retries = 2) {
|
|
83
91
|
if (urlSupportCache.has(url)) {
|
|
84
92
|
return urlSupportCache.get(url)
|
|
85
93
|
}
|
|
@@ -104,7 +112,9 @@ async function checkResourceSupport(url, retries = 2) {
|
|
|
104
112
|
} catch (error) {
|
|
105
113
|
lastError = error;
|
|
106
114
|
if (error.name === 'AbortError') {
|
|
107
|
-
|
|
115
|
+
if (logger) {
|
|
116
|
+
logger.warn(`Resource check timed out: ${url}`);
|
|
117
|
+
}
|
|
108
118
|
break // Don't retry timeouts
|
|
109
119
|
}
|
|
110
120
|
|
|
@@ -115,13 +125,17 @@ async function checkResourceSupport(url, retries = 2) {
|
|
|
115
125
|
}
|
|
116
126
|
}
|
|
117
127
|
|
|
118
|
-
|
|
128
|
+
if (logger) {
|
|
129
|
+
logger.warn(`Failed to check resource support: ${url}`, lastError);
|
|
130
|
+
}
|
|
119
131
|
urlSupportCache.set(url, false);
|
|
120
132
|
return false
|
|
121
133
|
}
|
|
122
134
|
|
|
123
|
-
|
|
124
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Optimized resource fetching function with retry mechanism and caching
|
|
137
|
+
*/
|
|
138
|
+
async function fetchResource(url, resourceCache, logger = null, retries = 1) {
|
|
125
139
|
// Check cache
|
|
126
140
|
if (resourceCache.has(url)) {
|
|
127
141
|
return resourceCache.get(url)
|
|
@@ -146,7 +160,9 @@ async function fetchResource(url, retries = 1) {
|
|
|
146
160
|
} catch (error) {
|
|
147
161
|
lastError = error;
|
|
148
162
|
if (error.name === 'AbortError') {
|
|
149
|
-
|
|
163
|
+
if (logger) {
|
|
164
|
+
logger.warn(`Resource fetch timed out: ${url}`);
|
|
165
|
+
}
|
|
150
166
|
break // Don't retry timeouts
|
|
151
167
|
}
|
|
152
168
|
|
|
@@ -156,144 +172,373 @@ async function fetchResource(url, retries = 1) {
|
|
|
156
172
|
}
|
|
157
173
|
}
|
|
158
174
|
|
|
159
|
-
|
|
175
|
+
if (logger) {
|
|
176
|
+
logger.warn(`Failed to fetch external resource: ${url}`, lastError);
|
|
177
|
+
}
|
|
160
178
|
return null
|
|
161
179
|
}
|
|
162
180
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// Handle absolute path URLs
|
|
173
|
-
if (url.startsWith('/')) {
|
|
174
|
-
// Remove leading slash to match keys in bundle
|
|
175
|
-
return url.substring(1)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Handle relative paths (when config.base is relative)
|
|
179
|
-
if (config.base === './' || config.base === '') {
|
|
180
|
-
return path.posix.resolve(path.posix.dirname(htmlPath), url)
|
|
181
|
-
}
|
|
181
|
+
/**
|
|
182
|
+
* Improved method for getting bundle keys
|
|
183
|
+
*/
|
|
184
|
+
function getBundleKey(htmlPath, url, config) {
|
|
185
|
+
// Handle absolute path URLs
|
|
186
|
+
if (url.startsWith('/')) {
|
|
187
|
+
// Remove leading slash to match keys in bundle
|
|
188
|
+
return url.substring(1)
|
|
189
|
+
}
|
|
182
190
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
};
|
|
191
|
+
// Handle relative paths (when config.base is relative)
|
|
192
|
+
if (config.base === './' || config.base === '') {
|
|
193
|
+
return path.posix.resolve(path.posix.dirname(htmlPath), url)
|
|
194
|
+
}
|
|
188
195
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
196
|
+
// Handle other cases, remove base prefix from URL
|
|
197
|
+
return url.startsWith(config.base)
|
|
198
|
+
? url.substring(config.base.length)
|
|
199
|
+
: url
|
|
200
|
+
}
|
|
194
201
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Calculate SRI integrity hash for a given resource
|
|
204
|
+
*/
|
|
205
|
+
async function calculateIntegrity(
|
|
206
|
+
bundle,
|
|
207
|
+
htmlPath,
|
|
208
|
+
url,
|
|
209
|
+
options,
|
|
210
|
+
config,
|
|
211
|
+
cacheManager,
|
|
212
|
+
logger = null
|
|
213
|
+
) {
|
|
214
|
+
const {
|
|
215
|
+
ignoreMissingAsset,
|
|
216
|
+
bypassDomains,
|
|
217
|
+
hashAlgorithm
|
|
218
|
+
} = options;
|
|
204
219
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
);
|
|
220
|
+
// Skip specified domains
|
|
221
|
+
if (isUrlFromBypassDomain(url, bypassDomains, logger)) {
|
|
222
|
+
return null
|
|
223
|
+
}
|
|
210
224
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
225
|
+
let source;
|
|
226
|
+
if (url.startsWith('http')) {
|
|
227
|
+
const isSupported = await checkResourceSupport(url, cacheManager.getUrlSupportCache(), logger);
|
|
228
|
+
if (!isSupported) return null
|
|
229
|
+
source = await fetchResource(url, cacheManager.getResourceCache(), logger);
|
|
230
|
+
if (!source) return null
|
|
231
|
+
} else {
|
|
232
|
+
const bundleKey = getBundleKey(htmlPath, url, config);
|
|
233
|
+
const bundleItem = bundle[bundleKey];
|
|
234
|
+
|
|
235
|
+
if (!bundleItem) {
|
|
236
|
+
// Try to find a matching item with more flexible matching
|
|
237
|
+
const possibleMatch = Object.keys(bundle).find(key =>
|
|
238
|
+
key.endsWith(bundleKey) || bundleKey.endsWith(key)
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
if (possibleMatch) {
|
|
242
|
+
source = bundle[possibleMatch].type === 'chunk'
|
|
243
|
+
? bundle[possibleMatch].code
|
|
244
|
+
: bundle[possibleMatch].source;
|
|
245
|
+
} else if (ignoreMissingAsset) {
|
|
246
|
+
if (logger) {
|
|
247
|
+
logger.warn(`Asset not found in bundle: ${url} (path: ${htmlPath}, key: ${bundleKey})`);
|
|
220
248
|
}
|
|
249
|
+
return null
|
|
221
250
|
} else {
|
|
222
|
-
|
|
251
|
+
throw new Error(`Asset ${url} not found in bundle (path: ${htmlPath}, key: ${bundleKey})`)
|
|
223
252
|
}
|
|
253
|
+
} else {
|
|
254
|
+
source = bundleItem.type === 'chunk' ? bundleItem.code : bundleItem.source;
|
|
224
255
|
}
|
|
256
|
+
}
|
|
225
257
|
|
|
226
|
-
|
|
227
|
-
|
|
258
|
+
// Ensure source is a Uint8Array or string
|
|
259
|
+
if (!source) return null
|
|
260
|
+
|
|
261
|
+
if (typeof source === 'string') {
|
|
262
|
+
return `${hashAlgorithm}-${createHash(hashAlgorithm).update(source).digest('base64')}`
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return `${hashAlgorithm}-${createHash(hashAlgorithm).update(Buffer.from(source)).digest('base64')}`
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Optimized regex patterns for better readability and efficiency
|
|
269
|
+
const HTML_PATTERNS = {
|
|
270
|
+
script: {
|
|
271
|
+
regex: /<script\b[^>]*?\bsrc\s*=\s*["']([^"']+)["'][^>]*><\/script>/g,
|
|
272
|
+
endOffset: 10
|
|
273
|
+
},
|
|
274
|
+
stylesheet: {
|
|
275
|
+
regex: /<link\b[^>]*?\brel\s*=\s*["']stylesheet["'][^>]*?\bhref\s*=\s*["']([^"']+)["'][^>]*>/g,
|
|
276
|
+
endOffset: 1
|
|
277
|
+
},
|
|
278
|
+
modulepreload: {
|
|
279
|
+
regex: /<link\b[^>]*?\brel\s*=\s*["']modulepreload["'][^>]*?\bhref\s*=\s*["']([^"']+)["'][^>]*>/g,
|
|
280
|
+
endOffset: 1
|
|
281
|
+
}
|
|
282
|
+
};
|
|
228
283
|
|
|
229
|
-
|
|
230
|
-
|
|
284
|
+
/**
|
|
285
|
+
* Validate HTML input
|
|
286
|
+
*/
|
|
287
|
+
function validateHtmlInput(html, htmlPath, logger) {
|
|
288
|
+
if (!html || typeof html !== 'string') {
|
|
289
|
+
logger.warn(`Invalid HTML content for ${htmlPath}`);
|
|
290
|
+
return false
|
|
291
|
+
}
|
|
292
|
+
return true
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Process a single match to create an integrity change object
|
|
297
|
+
*/
|
|
298
|
+
async function processMatch(
|
|
299
|
+
match,
|
|
300
|
+
endOffset,
|
|
301
|
+
bundle,
|
|
302
|
+
htmlPath,
|
|
303
|
+
options,
|
|
304
|
+
config,
|
|
305
|
+
cacheManager,
|
|
306
|
+
logger
|
|
307
|
+
) {
|
|
308
|
+
const [, url] = match;
|
|
309
|
+
if (!url) return null
|
|
310
|
+
|
|
311
|
+
const end = match.index + match[0].length;
|
|
312
|
+
const integrity = await calculateIntegrity(
|
|
313
|
+
bundle,
|
|
314
|
+
htmlPath,
|
|
315
|
+
url,
|
|
316
|
+
options,
|
|
317
|
+
config,
|
|
318
|
+
cacheManager,
|
|
319
|
+
logger
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
if (integrity) {
|
|
323
|
+
return {
|
|
324
|
+
integrity,
|
|
325
|
+
position: end - endOffset,
|
|
326
|
+
url // For logging
|
|
231
327
|
}
|
|
328
|
+
}
|
|
329
|
+
return null
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Process matches for a specific HTML pattern
|
|
334
|
+
*/
|
|
335
|
+
async function processPatternMatches(
|
|
336
|
+
html,
|
|
337
|
+
pattern,
|
|
338
|
+
bundle,
|
|
339
|
+
htmlPath,
|
|
340
|
+
options,
|
|
341
|
+
config,
|
|
342
|
+
cacheManager,
|
|
343
|
+
logger
|
|
344
|
+
) {
|
|
345
|
+
const { regex, endOffset } = pattern;
|
|
346
|
+
const matches = [...html.matchAll(regex)];
|
|
347
|
+
|
|
348
|
+
// Process each match in parallel
|
|
349
|
+
const matchResults = await Promise.all(
|
|
350
|
+
matches.map(match =>
|
|
351
|
+
processMatch(match, endOffset, bundle, htmlPath, options, config, cacheManager, logger)
|
|
352
|
+
)
|
|
353
|
+
);
|
|
232
354
|
|
|
233
|
-
|
|
234
|
-
|
|
355
|
+
// Filter out null results
|
|
356
|
+
return matchResults.filter(Boolean)
|
|
357
|
+
}
|
|
235
358
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
359
|
+
/**
|
|
360
|
+
* Collect all integrity changes from HTML patterns
|
|
361
|
+
*/
|
|
362
|
+
async function collectIntegrityChanges(
|
|
363
|
+
html,
|
|
364
|
+
bundle,
|
|
365
|
+
htmlPath,
|
|
366
|
+
options,
|
|
367
|
+
config,
|
|
368
|
+
cacheManager,
|
|
369
|
+
logger
|
|
370
|
+
) {
|
|
371
|
+
const changes = [];
|
|
372
|
+
|
|
373
|
+
// Collect changes from all patterns in parallel
|
|
374
|
+
await Promise.all(
|
|
375
|
+
Object.values(HTML_PATTERNS).map(async pattern => {
|
|
376
|
+
const patternChanges = await processPatternMatches(
|
|
377
|
+
html,
|
|
378
|
+
pattern,
|
|
379
|
+
bundle,
|
|
380
|
+
htmlPath,
|
|
381
|
+
options,
|
|
382
|
+
config,
|
|
383
|
+
cacheManager,
|
|
384
|
+
logger
|
|
385
|
+
);
|
|
386
|
+
changes.push(...patternChanges);
|
|
387
|
+
})
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
return changes
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Check if integrity attribute already exists in HTML segment
|
|
395
|
+
*/
|
|
396
|
+
function hasExistingIntegrity(html, position, integrity) {
|
|
397
|
+
const segment = html.slice(Math.max(0, position - 100), position + 100);
|
|
398
|
+
return segment.includes(`integrity="${integrity}"`)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Apply integrity changes to HTML content
|
|
403
|
+
*/
|
|
404
|
+
function applyIntegrityChanges(html, changes, logger) {
|
|
405
|
+
// Sort by position in descending order to insert from back to front
|
|
406
|
+
changes.sort((a, b) => b.position - a.position);
|
|
407
|
+
|
|
408
|
+
for (const { integrity, position, url } of changes) {
|
|
409
|
+
// Skip if integrity attribute already exists
|
|
410
|
+
if (hasExistingIntegrity(html, position, integrity)) {
|
|
411
|
+
continue
|
|
240
412
|
}
|
|
241
413
|
|
|
242
|
-
const
|
|
414
|
+
const insertText = ` integrity="${integrity}"`;
|
|
415
|
+
html = html.slice(0, position) + insertText + html.slice(position);
|
|
416
|
+
logger.debug(`Added integrity for: ${url}`);
|
|
417
|
+
}
|
|
243
418
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
Object.values(HTML_PATTERNS).map(async ({ regex, endOffset }) => {
|
|
247
|
-
const matches = [...html.matchAll(regex)];
|
|
419
|
+
return html
|
|
420
|
+
}
|
|
248
421
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
422
|
+
/**
|
|
423
|
+
* Transform HTML by adding SRI integrity attributes
|
|
424
|
+
*/
|
|
425
|
+
async function transformHTML(
|
|
426
|
+
bundle,
|
|
427
|
+
htmlPath,
|
|
428
|
+
html,
|
|
429
|
+
options,
|
|
430
|
+
config,
|
|
431
|
+
cacheManager,
|
|
432
|
+
logger
|
|
433
|
+
) {
|
|
434
|
+
if (!validateHtmlInput(html, htmlPath, logger)) {
|
|
435
|
+
return html
|
|
436
|
+
}
|
|
254
437
|
|
|
255
|
-
|
|
256
|
-
|
|
438
|
+
const changes = await collectIntegrityChanges(
|
|
439
|
+
html,
|
|
440
|
+
bundle,
|
|
441
|
+
htmlPath,
|
|
442
|
+
options,
|
|
443
|
+
config,
|
|
444
|
+
cacheManager,
|
|
445
|
+
logger
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
return applyIntegrityChanges(html, changes, logger)
|
|
449
|
+
}
|
|
257
450
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
451
|
+
/**
|
|
452
|
+
* Create HTML transformer with given options and config
|
|
453
|
+
*/
|
|
454
|
+
function createTransformer(options, config, cacheManager, logger) {
|
|
455
|
+
return {
|
|
456
|
+
transformHTML: (bundle, htmlPath, html) =>
|
|
457
|
+
transformHTML(bundle, htmlPath, html, options, config, cacheManager, logger),
|
|
458
|
+
calculateIntegrity: (bundle, htmlPath, url) =>
|
|
459
|
+
calculateIntegrity(bundle, htmlPath, url, options, config, cacheManager, logger)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
268
462
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
463
|
+
const DEFAULT_PLUGIN_NAME$1 = 'vite-plugin-sri4';
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Logger class to handle different log levels without hijacking global console
|
|
467
|
+
*/
|
|
468
|
+
class Logger {
|
|
469
|
+
constructor(logLevel = 'warn', pluginName = DEFAULT_PLUGIN_NAME$1) {
|
|
470
|
+
this.logLevel = logLevel;
|
|
471
|
+
this.pluginName = pluginName;
|
|
472
|
+
this.levels = {
|
|
473
|
+
silent: 0,
|
|
474
|
+
error: 1,
|
|
475
|
+
warn: 2,
|
|
476
|
+
info: 3,
|
|
477
|
+
debug: 4
|
|
478
|
+
};
|
|
479
|
+
this.currentLevel = this.levels[logLevel] || this.levels.warn;
|
|
480
|
+
}
|
|
273
481
|
|
|
274
|
-
|
|
275
|
-
|
|
482
|
+
/**
|
|
483
|
+
* Format message with plugin name prefix
|
|
484
|
+
*/
|
|
485
|
+
formatMessage(message, ...args) {
|
|
486
|
+
const prefix = `[${this.pluginName}]`;
|
|
487
|
+
if (typeof message === 'string') {
|
|
488
|
+
return [prefix + ' ' + message, ...args]
|
|
489
|
+
}
|
|
490
|
+
return [prefix, message, ...args]
|
|
491
|
+
}
|
|
276
492
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
493
|
+
/**
|
|
494
|
+
* Log error messages
|
|
495
|
+
*/
|
|
496
|
+
error(message, ...args) {
|
|
497
|
+
if (this.currentLevel >= this.levels.error) {
|
|
498
|
+
console.error(...this.formatMessage(message, ...args));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
280
501
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
502
|
+
/**
|
|
503
|
+
* Log warning messages
|
|
504
|
+
*/
|
|
505
|
+
warn(message, ...args) {
|
|
506
|
+
if (this.currentLevel >= this.levels.warn) {
|
|
507
|
+
console.warn(...this.formatMessage(message, ...args));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
286
510
|
|
|
287
|
-
|
|
288
|
-
|
|
511
|
+
/**
|
|
512
|
+
* Log info messages
|
|
513
|
+
*/
|
|
514
|
+
info(message, ...args) {
|
|
515
|
+
if (this.currentLevel >= this.levels.info) {
|
|
516
|
+
console.info(...this.formatMessage(message, ...args));
|
|
289
517
|
}
|
|
518
|
+
}
|
|
290
519
|
|
|
291
|
-
|
|
292
|
-
|
|
520
|
+
/**
|
|
521
|
+
* Log debug messages
|
|
522
|
+
*/
|
|
523
|
+
debug(message, ...args) {
|
|
524
|
+
if (this.currentLevel >= this.levels.debug) {
|
|
525
|
+
console.debug(...this.formatMessage(message, ...args));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
293
528
|
|
|
294
|
-
|
|
529
|
+
/**
|
|
530
|
+
* Create a child logger with the same configuration
|
|
531
|
+
*/
|
|
532
|
+
child(name) {
|
|
533
|
+
return new Logger(this.logLevel, `${this.pluginName}:${name}`)
|
|
534
|
+
}
|
|
295
535
|
}
|
|
296
536
|
|
|
537
|
+
// Constants definition
|
|
538
|
+
const VITE_INTERNAL_ANALYSIS_PLUGIN = 'vite:build-import-analysis';
|
|
539
|
+
const DEFAULT_HASH_ALGORITHM = 'sha384';
|
|
540
|
+
const DEFAULT_PLUGIN_NAME = 'vite-plugin-sri4';
|
|
541
|
+
|
|
297
542
|
function sri(options = {}) {
|
|
298
543
|
const {
|
|
299
544
|
ignoreMissingAsset = false,
|
|
@@ -302,16 +547,9 @@ function sri(options = {}) {
|
|
|
302
547
|
logLevel = 'warn'
|
|
303
548
|
} = options;
|
|
304
549
|
|
|
305
|
-
//
|
|
306
|
-
const
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
if (logLevel === 'error') {
|
|
310
|
-
console.warn = () => {};
|
|
311
|
-
console.debug = () => {};
|
|
312
|
-
} else if (logLevel === 'warn') {
|
|
313
|
-
console.debug = () => {};
|
|
314
|
-
}
|
|
550
|
+
// Create cache manager and logger instances for this plugin instance
|
|
551
|
+
const cacheManager = new CacheManager();
|
|
552
|
+
const logger = new Logger(logLevel, DEFAULT_PLUGIN_NAME);
|
|
315
553
|
|
|
316
554
|
return {
|
|
317
555
|
name: DEFAULT_PLUGIN_NAME,
|
|
@@ -320,13 +558,8 @@ function sri(options = {}) {
|
|
|
320
558
|
|
|
321
559
|
// Cleanup work
|
|
322
560
|
buildEnd() {
|
|
323
|
-
// Restore console functions
|
|
324
|
-
console.warn = originalConsoleWarn;
|
|
325
|
-
console.debug = originalConsoleDebug;
|
|
326
|
-
|
|
327
561
|
// Clear caches
|
|
328
|
-
|
|
329
|
-
resourceCache.clear();
|
|
562
|
+
cacheManager.clearAll();
|
|
330
563
|
},
|
|
331
564
|
|
|
332
565
|
configResolved(config) {
|
|
@@ -334,7 +567,7 @@ function sri(options = {}) {
|
|
|
334
567
|
ignoreMissingAsset,
|
|
335
568
|
bypassDomains,
|
|
336
569
|
hashAlgorithm
|
|
337
|
-
}, config);
|
|
570
|
+
}, config, cacheManager, logger);
|
|
338
571
|
|
|
339
572
|
const generateBundle = async function(_, bundle) {
|
|
340
573
|
const htmlFiles = Object.entries(bundle).filter(
|
|
@@ -344,7 +577,7 @@ function sri(options = {}) {
|
|
|
344
577
|
);
|
|
345
578
|
|
|
346
579
|
if (htmlFiles.length === 0) {
|
|
347
|
-
|
|
580
|
+
logger.debug('No HTML files found in bundle');
|
|
348
581
|
return
|
|
349
582
|
}
|
|
350
583
|
|
|
@@ -356,10 +589,10 @@ function sri(options = {}) {
|
|
|
356
589
|
chunk.source = await transformer.transformHTML(bundle, name, originalContent);
|
|
357
590
|
|
|
358
591
|
if (originalContent !== chunk.source) {
|
|
359
|
-
|
|
592
|
+
logger.debug(`SRI attributes added to ${name}`);
|
|
360
593
|
}
|
|
361
594
|
} catch (error) {
|
|
362
|
-
|
|
595
|
+
logger.warn(`Error processing ${name}:`, error);
|
|
363
596
|
// Keep original content on error
|
|
364
597
|
}
|
|
365
598
|
})
|