vite-plugin-sri4 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -11
- package/dist/index.cjs +442 -177
- package/dist/index.js +442 -177
- package/package.json +3 -5
package/dist/index.cjs
CHANGED
|
@@ -2,33 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
-
var
|
|
6
|
-
var path = require('path');
|
|
5
|
+
var node_crypto = require('node:crypto');
|
|
6
|
+
var path = require('node: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
|
+
}
|
|
55
|
+
|
|
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;
|
|
69
71
|
|
|
70
|
-
|
|
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,397 @@ 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
|
+
// Fall back to suffix match in either direction to absorb hashed
|
|
241
|
+
// filenames AND base-prefix mismatches (e.g. URL "/base/main.js" with
|
|
242
|
+
// bare bundle key "main.js"). A mismatch here just produces a wrong
|
|
243
|
+
// integrity hash, which the browser rejects — failure-closed.
|
|
244
|
+
const possibleMatch = Object.keys(bundle).find(key =>
|
|
245
|
+
key.endsWith(bundleKey) || bundleKey.endsWith(key)
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
if (possibleMatch) {
|
|
249
|
+
if (logger) {
|
|
250
|
+
logger.debug(`Bundle key fallback: ${bundleKey} -> ${possibleMatch}`);
|
|
251
|
+
}
|
|
252
|
+
source = bundle[possibleMatch].type === 'chunk'
|
|
253
|
+
? bundle[possibleMatch].code
|
|
254
|
+
: bundle[possibleMatch].source;
|
|
255
|
+
} else if (ignoreMissingAsset) {
|
|
256
|
+
if (logger) {
|
|
257
|
+
logger.warn(`Asset not found in bundle: ${url} (path: ${htmlPath}, key: ${bundleKey})`);
|
|
224
258
|
}
|
|
259
|
+
return null
|
|
225
260
|
} else {
|
|
226
|
-
|
|
261
|
+
throw new Error(`Asset ${url} not found in bundle (path: ${htmlPath}, key: ${bundleKey})`)
|
|
227
262
|
}
|
|
263
|
+
} else {
|
|
264
|
+
source = bundleItem.type === 'chunk' ? bundleItem.code : bundleItem.source;
|
|
228
265
|
}
|
|
266
|
+
}
|
|
229
267
|
|
|
230
|
-
|
|
231
|
-
|
|
268
|
+
// Ensure source is a Uint8Array or string
|
|
269
|
+
if (!source) return null
|
|
270
|
+
|
|
271
|
+
if (typeof source === 'string') {
|
|
272
|
+
return `${hashAlgorithm}-${node_crypto.createHash(hashAlgorithm).update(source).digest('base64')}`
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return `${hashAlgorithm}-${node_crypto.createHash(hashAlgorithm).update(Buffer.from(source)).digest('base64')}`
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Optimized regex patterns for better readability and efficiency
|
|
279
|
+
const HTML_PATTERNS = {
|
|
280
|
+
script: {
|
|
281
|
+
regex: /<script\b[^>]*?\bsrc\s*=\s*["']([^"']+)["'][^>]*><\/script>/g,
|
|
282
|
+
endOffset: 10
|
|
283
|
+
},
|
|
284
|
+
stylesheet: {
|
|
285
|
+
regex: /<link\b[^>]*?\brel\s*=\s*["']stylesheet["'][^>]*?\bhref\s*=\s*["']([^"']+)["'][^>]*>/g,
|
|
286
|
+
endOffset: 1
|
|
287
|
+
},
|
|
288
|
+
modulepreload: {
|
|
289
|
+
regex: /<link\b[^>]*?\brel\s*=\s*["']modulepreload["'][^>]*?\bhref\s*=\s*["']([^"']+)["'][^>]*>/g,
|
|
290
|
+
endOffset: 1
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Validate HTML input
|
|
296
|
+
*/
|
|
297
|
+
function validateHtmlInput(html, htmlPath, logger) {
|
|
298
|
+
if (!html || typeof html !== 'string') {
|
|
299
|
+
logger.warn(`Invalid HTML content for ${htmlPath}`);
|
|
300
|
+
return false
|
|
301
|
+
}
|
|
302
|
+
return true
|
|
303
|
+
}
|
|
232
304
|
|
|
233
|
-
|
|
234
|
-
|
|
305
|
+
/**
|
|
306
|
+
* Process a single match to create an integrity change object
|
|
307
|
+
*/
|
|
308
|
+
async function processMatch(
|
|
309
|
+
match,
|
|
310
|
+
endOffset,
|
|
311
|
+
bundle,
|
|
312
|
+
htmlPath,
|
|
313
|
+
options,
|
|
314
|
+
config,
|
|
315
|
+
cacheManager,
|
|
316
|
+
logger
|
|
317
|
+
) {
|
|
318
|
+
const [, url] = match;
|
|
319
|
+
if (!url) return null
|
|
320
|
+
|
|
321
|
+
const end = match.index + match[0].length;
|
|
322
|
+
const integrity = await calculateIntegrity(
|
|
323
|
+
bundle,
|
|
324
|
+
htmlPath,
|
|
325
|
+
url,
|
|
326
|
+
options,
|
|
327
|
+
config,
|
|
328
|
+
cacheManager,
|
|
329
|
+
logger
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
if (integrity) {
|
|
333
|
+
return {
|
|
334
|
+
integrity,
|
|
335
|
+
position: end - endOffset,
|
|
336
|
+
tagStart: match.index,
|
|
337
|
+
url // For logging
|
|
235
338
|
}
|
|
339
|
+
}
|
|
340
|
+
return null
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Process matches for a specific HTML pattern
|
|
345
|
+
*/
|
|
346
|
+
async function processPatternMatches(
|
|
347
|
+
html,
|
|
348
|
+
pattern,
|
|
349
|
+
bundle,
|
|
350
|
+
htmlPath,
|
|
351
|
+
options,
|
|
352
|
+
config,
|
|
353
|
+
cacheManager,
|
|
354
|
+
logger
|
|
355
|
+
) {
|
|
356
|
+
const { regex, endOffset } = pattern;
|
|
357
|
+
const matches = [...html.matchAll(regex)];
|
|
358
|
+
|
|
359
|
+
// Process each match in parallel
|
|
360
|
+
const matchResults = await Promise.all(
|
|
361
|
+
matches.map(match =>
|
|
362
|
+
processMatch(match, endOffset, bundle, htmlPath, options, config, cacheManager, logger)
|
|
363
|
+
)
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
// Filter out null results
|
|
367
|
+
return matchResults.filter(Boolean)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Collect all integrity changes from HTML patterns
|
|
372
|
+
*/
|
|
373
|
+
async function collectIntegrityChanges(
|
|
374
|
+
html,
|
|
375
|
+
bundle,
|
|
376
|
+
htmlPath,
|
|
377
|
+
options,
|
|
378
|
+
config,
|
|
379
|
+
cacheManager,
|
|
380
|
+
logger
|
|
381
|
+
) {
|
|
382
|
+
const changes = [];
|
|
383
|
+
|
|
384
|
+
// Collect changes from all patterns in parallel
|
|
385
|
+
await Promise.all(
|
|
386
|
+
Object.values(HTML_PATTERNS).map(async pattern => {
|
|
387
|
+
const patternChanges = await processPatternMatches(
|
|
388
|
+
html,
|
|
389
|
+
pattern,
|
|
390
|
+
bundle,
|
|
391
|
+
htmlPath,
|
|
392
|
+
options,
|
|
393
|
+
config,
|
|
394
|
+
cacheManager,
|
|
395
|
+
logger
|
|
396
|
+
);
|
|
397
|
+
changes.push(...patternChanges);
|
|
398
|
+
})
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
return changes
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const CROSSORIGIN_ATTR_RE = /\bcrossorigin\s*=/i;
|
|
405
|
+
const INTEGRITY_ATTR_RE = /\bintegrity\s*=/i;
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Check if integrity attribute already exists in the same tag
|
|
409
|
+
*/
|
|
410
|
+
function hasExistingIntegrity(html, tagStart, position) {
|
|
411
|
+
return INTEGRITY_ATTR_RE.test(html.slice(tagStart, position))
|
|
412
|
+
}
|
|
236
413
|
|
|
237
|
-
|
|
238
|
-
|
|
414
|
+
/**
|
|
415
|
+
* Check if crossorigin attribute already exists in the same tag
|
|
416
|
+
*/
|
|
417
|
+
function hasExistingCrossorigin(html, tagStart, position) {
|
|
418
|
+
return CROSSORIGIN_ATTR_RE.test(html.slice(tagStart, position))
|
|
419
|
+
}
|
|
239
420
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
421
|
+
/**
|
|
422
|
+
* Apply integrity changes to HTML content
|
|
423
|
+
*/
|
|
424
|
+
function applyIntegrityChanges(html, changes, logger) {
|
|
425
|
+
// Sort by position in descending order to insert from back to front
|
|
426
|
+
changes.sort((a, b) => b.position - a.position);
|
|
427
|
+
|
|
428
|
+
for (const { integrity, position, tagStart, url } of changes) {
|
|
429
|
+
// Skip if integrity attribute already exists on this tag
|
|
430
|
+
if (hasExistingIntegrity(html, tagStart, position)) {
|
|
431
|
+
continue
|
|
244
432
|
}
|
|
245
433
|
|
|
246
|
-
|
|
434
|
+
let insertText = ` integrity="${integrity}"`;
|
|
435
|
+
if (!hasExistingCrossorigin(html, tagStart, position)) {
|
|
436
|
+
insertText += ' crossorigin="anonymous"';
|
|
437
|
+
}
|
|
438
|
+
html = html.slice(0, position) + insertText + html.slice(position);
|
|
439
|
+
logger.debug(`Added integrity for: ${url}`);
|
|
440
|
+
}
|
|
247
441
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
Object.values(HTML_PATTERNS).map(async ({ regex, endOffset }) => {
|
|
251
|
-
const matches = [...html.matchAll(regex)];
|
|
442
|
+
return html
|
|
443
|
+
}
|
|
252
444
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
445
|
+
/**
|
|
446
|
+
* Transform HTML by adding SRI integrity attributes
|
|
447
|
+
*/
|
|
448
|
+
async function transformHTML(
|
|
449
|
+
bundle,
|
|
450
|
+
htmlPath,
|
|
451
|
+
html,
|
|
452
|
+
options,
|
|
453
|
+
config,
|
|
454
|
+
cacheManager,
|
|
455
|
+
logger
|
|
456
|
+
) {
|
|
457
|
+
if (!validateHtmlInput(html, htmlPath, logger)) {
|
|
458
|
+
return html
|
|
459
|
+
}
|
|
258
460
|
|
|
259
|
-
|
|
260
|
-
|
|
461
|
+
const changes = await collectIntegrityChanges(
|
|
462
|
+
html,
|
|
463
|
+
bundle,
|
|
464
|
+
htmlPath,
|
|
465
|
+
options,
|
|
466
|
+
config,
|
|
467
|
+
cacheManager,
|
|
468
|
+
logger
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
return applyIntegrityChanges(html, changes, logger)
|
|
472
|
+
}
|
|
261
473
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
474
|
+
/**
|
|
475
|
+
* Create HTML transformer with given options and config
|
|
476
|
+
*/
|
|
477
|
+
function createTransformer(options, config, cacheManager, logger) {
|
|
478
|
+
return {
|
|
479
|
+
transformHTML: (bundle, htmlPath, html) =>
|
|
480
|
+
transformHTML(bundle, htmlPath, html, options, config, cacheManager, logger),
|
|
481
|
+
calculateIntegrity: (bundle, htmlPath, url) =>
|
|
482
|
+
calculateIntegrity(bundle, htmlPath, url, options, config, cacheManager, logger)
|
|
483
|
+
}
|
|
484
|
+
}
|
|
272
485
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
486
|
+
const DEFAULT_PLUGIN_NAME$1 = 'vite-plugin-sri4';
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Logger class to handle different log levels without hijacking global console
|
|
490
|
+
*/
|
|
491
|
+
class Logger {
|
|
492
|
+
constructor(logLevel = 'warn', pluginName = DEFAULT_PLUGIN_NAME$1) {
|
|
493
|
+
this.logLevel = logLevel;
|
|
494
|
+
this.pluginName = pluginName;
|
|
495
|
+
this.levels = {
|
|
496
|
+
silent: 0,
|
|
497
|
+
error: 1,
|
|
498
|
+
warn: 2,
|
|
499
|
+
info: 3,
|
|
500
|
+
debug: 4
|
|
501
|
+
};
|
|
502
|
+
this.currentLevel = this.levels[logLevel] || this.levels.warn;
|
|
503
|
+
}
|
|
277
504
|
|
|
278
|
-
|
|
279
|
-
|
|
505
|
+
/**
|
|
506
|
+
* Format message with plugin name prefix
|
|
507
|
+
*/
|
|
508
|
+
formatMessage(message, ...args) {
|
|
509
|
+
const prefix = `[${this.pluginName}]`;
|
|
510
|
+
if (typeof message === 'string') {
|
|
511
|
+
return [prefix + ' ' + message, ...args]
|
|
512
|
+
}
|
|
513
|
+
return [prefix, message, ...args]
|
|
514
|
+
}
|
|
280
515
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
516
|
+
/**
|
|
517
|
+
* Log error messages
|
|
518
|
+
*/
|
|
519
|
+
error(message, ...args) {
|
|
520
|
+
if (this.currentLevel >= this.levels.error) {
|
|
521
|
+
console.error(...this.formatMessage(message, ...args));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
284
524
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
525
|
+
/**
|
|
526
|
+
* Log warning messages
|
|
527
|
+
*/
|
|
528
|
+
warn(message, ...args) {
|
|
529
|
+
if (this.currentLevel >= this.levels.warn) {
|
|
530
|
+
console.warn(...this.formatMessage(message, ...args));
|
|
531
|
+
}
|
|
532
|
+
}
|
|
290
533
|
|
|
291
|
-
|
|
292
|
-
|
|
534
|
+
/**
|
|
535
|
+
* Log info messages
|
|
536
|
+
*/
|
|
537
|
+
info(message, ...args) {
|
|
538
|
+
if (this.currentLevel >= this.levels.info) {
|
|
539
|
+
console.info(...this.formatMessage(message, ...args));
|
|
293
540
|
}
|
|
541
|
+
}
|
|
294
542
|
|
|
295
|
-
|
|
296
|
-
|
|
543
|
+
/**
|
|
544
|
+
* Log debug messages
|
|
545
|
+
*/
|
|
546
|
+
debug(message, ...args) {
|
|
547
|
+
if (this.currentLevel >= this.levels.debug) {
|
|
548
|
+
console.debug(...this.formatMessage(message, ...args));
|
|
549
|
+
}
|
|
550
|
+
}
|
|
297
551
|
|
|
298
|
-
|
|
552
|
+
/**
|
|
553
|
+
* Create a child logger with the same configuration
|
|
554
|
+
*/
|
|
555
|
+
child(name) {
|
|
556
|
+
return new Logger(this.logLevel, `${this.pluginName}:${name}`)
|
|
557
|
+
}
|
|
299
558
|
}
|
|
300
559
|
|
|
560
|
+
// Constants definition
|
|
561
|
+
// Vite 6/7 uses `vite:build-import-analysis`; Vite 8 Rolldown native path
|
|
562
|
+
// adds `native:import-analysis-build`. We patch whichever (or both) is present.
|
|
563
|
+
const VITE_INTERNAL_ANALYSIS_PLUGINS = [
|
|
564
|
+
'vite:build-import-analysis',
|
|
565
|
+
'native:import-analysis-build'
|
|
566
|
+
];
|
|
567
|
+
const DEFAULT_HASH_ALGORITHM = 'sha384';
|
|
568
|
+
const DEFAULT_PLUGIN_NAME = 'vite-plugin-sri4';
|
|
569
|
+
|
|
301
570
|
function sri(options = {}) {
|
|
302
571
|
const {
|
|
303
572
|
ignoreMissingAsset = false,
|
|
@@ -306,16 +575,9 @@ function sri(options = {}) {
|
|
|
306
575
|
logLevel = 'warn'
|
|
307
576
|
} = options;
|
|
308
577
|
|
|
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
|
-
}
|
|
578
|
+
// Create cache manager and logger instances for this plugin instance
|
|
579
|
+
const cacheManager = new CacheManager();
|
|
580
|
+
const logger = new Logger(logLevel, DEFAULT_PLUGIN_NAME);
|
|
319
581
|
|
|
320
582
|
return {
|
|
321
583
|
name: DEFAULT_PLUGIN_NAME,
|
|
@@ -324,13 +586,8 @@ function sri(options = {}) {
|
|
|
324
586
|
|
|
325
587
|
// Cleanup work
|
|
326
588
|
buildEnd() {
|
|
327
|
-
// Restore console functions
|
|
328
|
-
console.warn = originalConsoleWarn;
|
|
329
|
-
console.debug = originalConsoleDebug;
|
|
330
|
-
|
|
331
589
|
// Clear caches
|
|
332
|
-
|
|
333
|
-
resourceCache.clear();
|
|
590
|
+
cacheManager.clearAll();
|
|
334
591
|
},
|
|
335
592
|
|
|
336
593
|
configResolved(config) {
|
|
@@ -338,7 +595,7 @@ function sri(options = {}) {
|
|
|
338
595
|
ignoreMissingAsset,
|
|
339
596
|
bypassDomains,
|
|
340
597
|
hashAlgorithm
|
|
341
|
-
}, config);
|
|
598
|
+
}, config, cacheManager, logger);
|
|
342
599
|
|
|
343
600
|
const generateBundle = async function(_, bundle) {
|
|
344
601
|
const htmlFiles = Object.entries(bundle).filter(
|
|
@@ -348,7 +605,7 @@ function sri(options = {}) {
|
|
|
348
605
|
);
|
|
349
606
|
|
|
350
607
|
if (htmlFiles.length === 0) {
|
|
351
|
-
|
|
608
|
+
logger.debug('No HTML files found in bundle');
|
|
352
609
|
return
|
|
353
610
|
}
|
|
354
611
|
|
|
@@ -360,33 +617,41 @@ function sri(options = {}) {
|
|
|
360
617
|
chunk.source = await transformer.transformHTML(bundle, name, originalContent);
|
|
361
618
|
|
|
362
619
|
if (originalContent !== chunk.source) {
|
|
363
|
-
|
|
620
|
+
logger.debug(`SRI attributes added to ${name}`);
|
|
364
621
|
}
|
|
365
622
|
} catch (error) {
|
|
366
|
-
|
|
623
|
+
logger.warn(`Error processing ${name}:`, error);
|
|
367
624
|
// Keep original content on error
|
|
368
625
|
}
|
|
369
626
|
})
|
|
370
627
|
);
|
|
371
628
|
};
|
|
372
629
|
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
630
|
+
const targets = config.plugins.filter(
|
|
631
|
+
p => p && VITE_INTERNAL_ANALYSIS_PLUGINS.includes(p.name)
|
|
632
|
+
);
|
|
633
|
+
if (targets.length === 0) {
|
|
634
|
+
throw new Error(
|
|
635
|
+
`[${DEFAULT_PLUGIN_NAME}] could not find a Vite import-analysis plugin to hook into ` +
|
|
636
|
+
`(looked for: ${VITE_INTERNAL_ANALYSIS_PLUGINS.join(', ')}). ` +
|
|
637
|
+
`Requires Vite 6.0.0 or higher.`
|
|
638
|
+
)
|
|
376
639
|
}
|
|
377
640
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
641
|
+
for (const plugin of targets) {
|
|
642
|
+
if (typeof plugin.generateBundle === 'object' && plugin.generateBundle.handler) {
|
|
643
|
+
const originalHandler = plugin.generateBundle.handler;
|
|
644
|
+
plugin.generateBundle.handler = async function(...args) {
|
|
645
|
+
await originalHandler.apply(this, args);
|
|
646
|
+
await generateBundle.apply(this, args);
|
|
647
|
+
};
|
|
648
|
+
} else if (typeof plugin.generateBundle === 'function') {
|
|
649
|
+
const originalHandler = plugin.generateBundle;
|
|
650
|
+
plugin.generateBundle = async function(...args) {
|
|
651
|
+
await originalHandler.apply(this, args);
|
|
652
|
+
await generateBundle.apply(this, args);
|
|
653
|
+
};
|
|
654
|
+
}
|
|
390
655
|
}
|
|
391
656
|
}
|
|
392
657
|
}
|