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