vite-plugin-sri4 5.1.0 → 5.1.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 +45 -5
- package/dist/index.cjs +19 -40
- package/dist/index.js +19 -40
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -16,9 +16,11 @@ A Vite plugin to generate Subresource Integrity (SRI) hashes for your assets dur
|
|
|
16
16
|
- [External Resources](#external-resources)
|
|
17
17
|
- [When SRI Actually Helps](#when-sri-actually-helps)
|
|
18
18
|
- [How It Attaches Hashes](#how-it-attaches-hashes)
|
|
19
|
+
- [Differences from vite-plugin-sri3](#differences-from-vite-plugin-sri3)
|
|
19
20
|
- [Example Project](#example-project)
|
|
20
21
|
- [Best Practices](#best-practices)
|
|
21
22
|
- [Troubleshooting](#troubleshooting)
|
|
23
|
+
- [Testing](#testing)
|
|
22
24
|
- [Contributing](#contributing)
|
|
23
25
|
- [Inspiration](#inspiration)
|
|
24
26
|
- [License](#license)
|
|
@@ -144,9 +146,11 @@ The `writeBundle` drift check above covers **your own build outputs only**. Ever
|
|
|
144
146
|
|
|
145
147
|
An external URL pointing at someone else's origin is different. It is fetched **once, at build time, from your build machine**, and the hash is taken from that copy. An `integrity` attribute pins those bytes forever, so it is only correct on a URL whose bytes never change — and the origin is the only party that knows whether that is true.
|
|
146
148
|
|
|
147
|
-
So the plugin asks it.
|
|
149
|
+
So the plugin asks it. One `GET` does the whole job — it carries both the bytes to hash and the headers the answer depends on. There is no separate `HEAD` probe, so a host that serves `GET` and refuses `HEAD` is not a problem; `js.tappaysdk.com` answers `403` to `HEAD` and `200` to `GET`, and is read correctly.
|
|
148
150
|
|
|
149
|
-
|
|
151
|
+
An external resource is hashed only when **all three** hold:
|
|
152
|
+
|
|
153
|
+
1. **The request succeeds.** A non-2xx response leaves nothing to check.
|
|
150
154
|
2. **`Access-Control-Allow-Origin: *`.** Injecting `integrity` also injects `crossorigin`, so a response scoped to one specific origin — or to none — would turn a working resource into a blocked one.
|
|
151
155
|
3. **The origin declares the URL immutable**: `Cache-Control: immutable`, or a `max-age` of a year or more — and nothing in the same header contradicting it. `private`, `no-store` and `no-cache` each veto it: freshness and shareability are orthogonal, so a per-client response can carry a long `max-age`, and `no-cache, max-age=<long>` is a real CDN spelling of "cache it, but revalidate every time". Or the host is in `trustDomains`.
|
|
152
156
|
|
|
@@ -247,7 +251,7 @@ There are three places a Vite plugin can compute SRI hashes, and they are not eq
|
|
|
247
251
|
|
|
248
252
|
Only the entry chunk is affected, so a build without a dynamic import will not reveal the difference. As a second safeguard, every hashed file is re-hashed in `writeBundle` and the build fails if anything changed after the hash was taken.
|
|
249
253
|
|
|
250
|
-
This ordering constraint was first identified by [vite-plugin-sri3](https://github.com/yoyo930021/vite-plugin-sri3), which this plugin began as a fork of.
|
|
254
|
+
This ordering constraint was first identified by [vite-plugin-sri3](https://github.com/yoyo930021/vite-plugin-sri3), which this plugin began as a fork of. See [Differences from vite-plugin-sri3](#differences-from-vite-plugin-sri3).
|
|
251
255
|
|
|
252
256
|
## Example Project
|
|
253
257
|
|
|
@@ -334,6 +338,20 @@ This will show:
|
|
|
334
338
|
- Missing asset warnings
|
|
335
339
|
- Bundle-key fallback matches (when a URL is resolved via suffix match)
|
|
336
340
|
|
|
341
|
+
## Testing
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
npm test # 174 tests
|
|
345
|
+
npm run test:coverage # the same, with coverage thresholds enforced at 100%
|
|
346
|
+
npm run test:mutation # Stryker, ~4 minutes
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Coverage is held at **100%** of statements, branches, functions and lines, enforced by thresholds in `vitest.config.js` — an uncovered path fails the run rather than quietly lowering the number.
|
|
350
|
+
|
|
351
|
+
Coverage only proves a line ran. Mutation testing changes the code and checks whether a test notices, which is a much harder bar: at 100% coverage this suite still let 162 mutants through on the first run. The gaps it found were real — `no-store` and the qualified `private="…"` / `no-cache="…"` forms were never exercised, the `publicDir` path-traversal guard had no test, `base: './'` and `base: ''` were unreachable from any test, and the immutability tests all used `max-age=31536000, immutable`, where **both** passing conditions hold at once, so neither was actually pinned.
|
|
352
|
+
|
|
353
|
+
The mutation score is **81.19%**, with a break threshold of 80. The remaining survivors are mostly equivalent mutants — warning message wording, cache clearing that has no observable effect, and `typeof source === 'string' ? source : Buffer.from(source)`, whose two branches hash identically. Killing those would mean asserting log text verbatim, which costs more than it protects.
|
|
354
|
+
|
|
337
355
|
## Contributing
|
|
338
356
|
|
|
339
357
|
We welcome contributions! Here's how you can help:
|
|
@@ -350,9 +368,31 @@ Please make sure to:
|
|
|
350
368
|
- Follow the existing code style
|
|
351
369
|
- Update the CHANGELOG.md
|
|
352
370
|
|
|
353
|
-
##
|
|
371
|
+
## Differences from vite-plugin-sri3
|
|
372
|
+
|
|
373
|
+
This plugin began as a fork of [vite-plugin-sri3](https://github.com/yoyo930021/vite-plugin-sri3) and the two have since diverged. Compared against sri3 `2.0.0`:
|
|
354
374
|
|
|
355
|
-
|
|
375
|
+
| | sri3 2.0.0 | sri4 5.1.0 |
|
|
376
|
+
|---|---|---|
|
|
377
|
+
| Vite range | `^3 ‖ ^4 ‖ ^5 ‖ ^6 ‖ ^7 ‖ ^8` | `^6.4 ‖ ^7 ‖ ^8` |
|
|
378
|
+
| Bundle outputs | ✅ | ✅ |
|
|
379
|
+
| `publicDir` assets | ✅ | ✅ |
|
|
380
|
+
| `skip-sri` per-tag opt-out | ✅ | ✅ |
|
|
381
|
+
| TypeScript definitions | ✅ | ✅ |
|
|
382
|
+
| Hash algorithm | `sha384`, fixed | `sha256` / `sha384` / `sha512`, validated at startup |
|
|
383
|
+
| `crossorigin` attribute | not injected | injected, `anonymous` or `use-credentials` |
|
|
384
|
+
| External resources | fetched unconditionally | gated on reachability, CORS and immutability |
|
|
385
|
+
| Timeout / retry / cache on those fetches | ❌ | ✅ |
|
|
386
|
+
| `bypassDomains` / `trustDomains` | ❌ | ✅ |
|
|
387
|
+
| Hash drift detection | ❌ | re-hashed in `writeBundle`, build fails on drift |
|
|
388
|
+
| `import()`-loaded routes, SSR | ❌ | `importmap` and `manifest` options |
|
|
389
|
+
| Hook ordering | monkey-patches Vite's `generateBundle` | repositions itself in `config.plugins` |
|
|
390
|
+
|
|
391
|
+
**Where sri3 is the better fit:** it supports Vite 3 through 5, which this plugin dropped. If you are on an older Vite, it is the only one of the two that works.
|
|
392
|
+
|
|
393
|
+
**The difference that matters most:** sri3 injects `integrity` without `crossorigin`. SRI on a cross-origin resource requires CORS, so a browser blocks a cross-origin `<script>` or `<link>` that carries `integrity` and no `crossorigin` — which makes sri3's external-resource support difficult to use for the case SRI is usually reached for. That gap is what most of the column above grew out of: injecting `crossorigin` means the CORS response has to be checked at build time, and checking it exposed everything else worth checking.
|
|
394
|
+
|
|
395
|
+
## Inspiration
|
|
356
396
|
|
|
357
397
|
Other projects that influenced this work:
|
|
358
398
|
- [rollup-plugin-sri](https://github.com/JonasKruckenberg/rollup-plugin-sri)
|
package/dist/index.cjs
CHANGED
|
@@ -69,7 +69,7 @@ const DEFAULT_TIMEOUT = 5000;
|
|
|
69
69
|
* Does an external URL's host match one of `domains`, or a subdomain of one?
|
|
70
70
|
* Used by both `bypassDomains` and `trustDomains`.
|
|
71
71
|
*/
|
|
72
|
-
function matchesDomain(url, domains = [], logger
|
|
72
|
+
function matchesDomain(url, domains = [], logger) {
|
|
73
73
|
if (!url || typeof url !== 'string' || !url.startsWith('http')) return false
|
|
74
74
|
if (domains.length === 0) return false
|
|
75
75
|
|
|
@@ -79,9 +79,7 @@ function matchesDomain(url, domains = [], logger = null) {
|
|
|
79
79
|
urlObj.hostname === domain || urlObj.hostname.endsWith(`.${domain}`)
|
|
80
80
|
)
|
|
81
81
|
} catch (error) {
|
|
82
|
-
|
|
83
|
-
logger.warn(`Invalid URL: ${url}`, error);
|
|
84
|
-
}
|
|
82
|
+
logger.warn(`Invalid URL: ${url}`, error);
|
|
85
83
|
return false
|
|
86
84
|
}
|
|
87
85
|
}
|
|
@@ -175,13 +173,13 @@ function isImmutableResponse(cacheControl) {
|
|
|
175
173
|
* That is the right side to lose on: the accepted case, which is every build
|
|
176
174
|
* that actually ships hashes, goes from two requests to one.
|
|
177
175
|
*/
|
|
178
|
-
async function fetchVerifiedResource(url, resourceCache, logger
|
|
176
|
+
async function fetchVerifiedResource(url, resourceCache, logger, trusted = false, retries = 1) {
|
|
179
177
|
if (resourceCache.has(url)) {
|
|
180
178
|
return resourceCache.get(url)
|
|
181
179
|
}
|
|
182
180
|
|
|
183
181
|
const reject = (message) => {
|
|
184
|
-
|
|
182
|
+
logger.warn(message);
|
|
185
183
|
resourceCache.set(url, null);
|
|
186
184
|
return null
|
|
187
185
|
};
|
|
@@ -257,9 +255,7 @@ async function fetchVerifiedResource(url, resourceCache, logger = null, trusted
|
|
|
257
255
|
}
|
|
258
256
|
}
|
|
259
257
|
|
|
260
|
-
|
|
261
|
-
logger.warn(`Skipping SRI for ${url}: the request failed.`, lastError);
|
|
262
|
-
}
|
|
258
|
+
logger.warn(`Skipping SRI for ${url}: the request failed.`, lastError);
|
|
263
259
|
resourceCache.set(url, null);
|
|
264
260
|
return null
|
|
265
261
|
}
|
|
@@ -333,14 +329,14 @@ function getBundleKey(htmlPath, url, config) {
|
|
|
333
329
|
* `assets/vendor-main.js` - a cross-filename match would inject a valid-looking
|
|
334
330
|
* but wrong hash, which the browser rejects with no build-time error.
|
|
335
331
|
*/
|
|
336
|
-
function findBundleKey(bundle, bundleKey, logger
|
|
332
|
+
function findBundleKey(bundle, bundleKey, logger) {
|
|
337
333
|
const candidates = Object.keys(bundle).filter(key =>
|
|
338
334
|
key === bundleKey ||
|
|
339
335
|
key.endsWith(`/${bundleKey}`) ||
|
|
340
336
|
bundleKey.endsWith(`/${key}`)
|
|
341
337
|
);
|
|
342
338
|
|
|
343
|
-
if (candidates.length > 1
|
|
339
|
+
if (candidates.length > 1) {
|
|
344
340
|
logger.warn(
|
|
345
341
|
`Ambiguous bundle key for "${bundleKey}": ${candidates.join(', ')} - using ${candidates[0]}`
|
|
346
342
|
);
|
|
@@ -374,17 +370,13 @@ async function readPublicAsset(config, bundleKey, logger) {
|
|
|
374
370
|
// A URL must never reach outside publicDir, however it is spelled
|
|
375
371
|
const relative = path.relative(publicDir, filePath);
|
|
376
372
|
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
377
|
-
|
|
378
|
-
logger.warn(`Refusing to read outside publicDir: ${bundleKey}`);
|
|
379
|
-
}
|
|
373
|
+
logger.warn(`Refusing to read outside publicDir: ${bundleKey}`);
|
|
380
374
|
return null
|
|
381
375
|
}
|
|
382
376
|
|
|
383
377
|
try {
|
|
384
378
|
const source = await promises.readFile(filePath);
|
|
385
|
-
|
|
386
|
-
logger.debug(`Resolved from publicDir: ${bundleKey}`);
|
|
387
|
-
}
|
|
379
|
+
logger.debug(`Resolved from publicDir: ${bundleKey}`);
|
|
388
380
|
return source
|
|
389
381
|
} catch {
|
|
390
382
|
return null
|
|
@@ -401,7 +393,7 @@ async function calculateIntegrity(
|
|
|
401
393
|
options,
|
|
402
394
|
config,
|
|
403
395
|
cacheManager,
|
|
404
|
-
logger
|
|
396
|
+
logger
|
|
405
397
|
) {
|
|
406
398
|
const {
|
|
407
399
|
ignoreMissingAsset,
|
|
@@ -438,9 +430,7 @@ async function calculateIntegrity(
|
|
|
438
430
|
if (!source) return null
|
|
439
431
|
} else if (!ownAsset && SCHEME_RE.test(url)) {
|
|
440
432
|
// data:/blob: and unknown schemes cannot be resolved to a bundle asset
|
|
441
|
-
|
|
442
|
-
logger.debug(`Skipping URL that is not a bundle asset: ${url}`);
|
|
443
|
-
}
|
|
433
|
+
logger.debug(`Skipping URL that is not a bundle asset: ${url}`);
|
|
444
434
|
return null
|
|
445
435
|
} else {
|
|
446
436
|
const bundleKey = getBundleKey(htmlPath, url, config);
|
|
@@ -453,9 +443,7 @@ async function calculateIntegrity(
|
|
|
453
443
|
const possibleMatch = findBundleKey(bundle, bundleKey, logger);
|
|
454
444
|
|
|
455
445
|
if (possibleMatch) {
|
|
456
|
-
|
|
457
|
-
logger.debug(`Bundle key fallback: ${bundleKey} -> ${possibleMatch}`);
|
|
458
|
-
}
|
|
446
|
+
logger.debug(`Bundle key fallback: ${bundleKey} -> ${possibleMatch}`);
|
|
459
447
|
bundleFileName = possibleMatch;
|
|
460
448
|
source = bundleSource(bundle[possibleMatch]);
|
|
461
449
|
} else {
|
|
@@ -464,11 +452,9 @@ async function calculateIntegrity(
|
|
|
464
452
|
|
|
465
453
|
if (!source) {
|
|
466
454
|
if (ignoreMissingAsset) {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
);
|
|
471
|
-
}
|
|
455
|
+
logger.warn(
|
|
456
|
+
`Asset not found in bundle or publicDir: ${url} (path: ${htmlPath}, key: ${bundleKey})`
|
|
457
|
+
);
|
|
472
458
|
return null
|
|
473
459
|
}
|
|
474
460
|
throw new Error(
|
|
@@ -519,7 +505,9 @@ const SRI_LINK_RELS = new Set(['stylesheet', 'modulepreload']);
|
|
|
519
505
|
function getAttr(tag, re) {
|
|
520
506
|
const match = tag.match(re);
|
|
521
507
|
if (!match) return null
|
|
522
|
-
|
|
508
|
+
// One of the three alternatives matched or the regex would not have, and an
|
|
509
|
+
// empty value is a string rather than undefined - so there is no fourth case.
|
|
510
|
+
return match[1] ?? match[2] ?? match[3]
|
|
523
511
|
}
|
|
524
512
|
|
|
525
513
|
const HTML_PATTERNS = {
|
|
@@ -771,9 +759,7 @@ async function transformHTML(
|
|
|
771
759
|
function createTransformer(options, config, cacheManager, logger) {
|
|
772
760
|
return {
|
|
773
761
|
transformHTML: (bundle, htmlPath, html) =>
|
|
774
|
-
transformHTML(bundle, htmlPath, html, options, config, cacheManager, logger)
|
|
775
|
-
calculateIntegrity: (bundle, htmlPath, url) =>
|
|
776
|
-
calculateIntegrity(bundle, htmlPath, url, options, config, cacheManager, logger)
|
|
762
|
+
transformHTML(bundle, htmlPath, html, options, config, cacheManager, logger)
|
|
777
763
|
}
|
|
778
764
|
}
|
|
779
765
|
|
|
@@ -844,13 +830,6 @@ class Logger {
|
|
|
844
830
|
console.debug(...this.formatMessage(message, ...args));
|
|
845
831
|
}
|
|
846
832
|
}
|
|
847
|
-
|
|
848
|
-
/**
|
|
849
|
-
* Create a child logger with the same configuration
|
|
850
|
-
*/
|
|
851
|
-
child(name) {
|
|
852
|
-
return new Logger(this.logLevel, `${this.pluginName}:${name}`)
|
|
853
|
-
}
|
|
854
833
|
}
|
|
855
834
|
|
|
856
835
|
// Vite 6/7 uses `vite:build-import-analysis`; Vite 8 Rolldown native path adds
|
package/dist/index.js
CHANGED
|
@@ -65,7 +65,7 @@ const DEFAULT_TIMEOUT = 5000;
|
|
|
65
65
|
* Does an external URL's host match one of `domains`, or a subdomain of one?
|
|
66
66
|
* Used by both `bypassDomains` and `trustDomains`.
|
|
67
67
|
*/
|
|
68
|
-
function matchesDomain(url, domains = [], logger
|
|
68
|
+
function matchesDomain(url, domains = [], logger) {
|
|
69
69
|
if (!url || typeof url !== 'string' || !url.startsWith('http')) return false
|
|
70
70
|
if (domains.length === 0) return false
|
|
71
71
|
|
|
@@ -75,9 +75,7 @@ function matchesDomain(url, domains = [], logger = null) {
|
|
|
75
75
|
urlObj.hostname === domain || urlObj.hostname.endsWith(`.${domain}`)
|
|
76
76
|
)
|
|
77
77
|
} catch (error) {
|
|
78
|
-
|
|
79
|
-
logger.warn(`Invalid URL: ${url}`, error);
|
|
80
|
-
}
|
|
78
|
+
logger.warn(`Invalid URL: ${url}`, error);
|
|
81
79
|
return false
|
|
82
80
|
}
|
|
83
81
|
}
|
|
@@ -171,13 +169,13 @@ function isImmutableResponse(cacheControl) {
|
|
|
171
169
|
* That is the right side to lose on: the accepted case, which is every build
|
|
172
170
|
* that actually ships hashes, goes from two requests to one.
|
|
173
171
|
*/
|
|
174
|
-
async function fetchVerifiedResource(url, resourceCache, logger
|
|
172
|
+
async function fetchVerifiedResource(url, resourceCache, logger, trusted = false, retries = 1) {
|
|
175
173
|
if (resourceCache.has(url)) {
|
|
176
174
|
return resourceCache.get(url)
|
|
177
175
|
}
|
|
178
176
|
|
|
179
177
|
const reject = (message) => {
|
|
180
|
-
|
|
178
|
+
logger.warn(message);
|
|
181
179
|
resourceCache.set(url, null);
|
|
182
180
|
return null
|
|
183
181
|
};
|
|
@@ -253,9 +251,7 @@ async function fetchVerifiedResource(url, resourceCache, logger = null, trusted
|
|
|
253
251
|
}
|
|
254
252
|
}
|
|
255
253
|
|
|
256
|
-
|
|
257
|
-
logger.warn(`Skipping SRI for ${url}: the request failed.`, lastError);
|
|
258
|
-
}
|
|
254
|
+
logger.warn(`Skipping SRI for ${url}: the request failed.`, lastError);
|
|
259
255
|
resourceCache.set(url, null);
|
|
260
256
|
return null
|
|
261
257
|
}
|
|
@@ -329,14 +325,14 @@ function getBundleKey(htmlPath, url, config) {
|
|
|
329
325
|
* `assets/vendor-main.js` - a cross-filename match would inject a valid-looking
|
|
330
326
|
* but wrong hash, which the browser rejects with no build-time error.
|
|
331
327
|
*/
|
|
332
|
-
function findBundleKey(bundle, bundleKey, logger
|
|
328
|
+
function findBundleKey(bundle, bundleKey, logger) {
|
|
333
329
|
const candidates = Object.keys(bundle).filter(key =>
|
|
334
330
|
key === bundleKey ||
|
|
335
331
|
key.endsWith(`/${bundleKey}`) ||
|
|
336
332
|
bundleKey.endsWith(`/${key}`)
|
|
337
333
|
);
|
|
338
334
|
|
|
339
|
-
if (candidates.length > 1
|
|
335
|
+
if (candidates.length > 1) {
|
|
340
336
|
logger.warn(
|
|
341
337
|
`Ambiguous bundle key for "${bundleKey}": ${candidates.join(', ')} - using ${candidates[0]}`
|
|
342
338
|
);
|
|
@@ -370,17 +366,13 @@ async function readPublicAsset(config, bundleKey, logger) {
|
|
|
370
366
|
// A URL must never reach outside publicDir, however it is spelled
|
|
371
367
|
const relative = path.relative(publicDir, filePath);
|
|
372
368
|
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
373
|
-
|
|
374
|
-
logger.warn(`Refusing to read outside publicDir: ${bundleKey}`);
|
|
375
|
-
}
|
|
369
|
+
logger.warn(`Refusing to read outside publicDir: ${bundleKey}`);
|
|
376
370
|
return null
|
|
377
371
|
}
|
|
378
372
|
|
|
379
373
|
try {
|
|
380
374
|
const source = await readFile(filePath);
|
|
381
|
-
|
|
382
|
-
logger.debug(`Resolved from publicDir: ${bundleKey}`);
|
|
383
|
-
}
|
|
375
|
+
logger.debug(`Resolved from publicDir: ${bundleKey}`);
|
|
384
376
|
return source
|
|
385
377
|
} catch {
|
|
386
378
|
return null
|
|
@@ -397,7 +389,7 @@ async function calculateIntegrity(
|
|
|
397
389
|
options,
|
|
398
390
|
config,
|
|
399
391
|
cacheManager,
|
|
400
|
-
logger
|
|
392
|
+
logger
|
|
401
393
|
) {
|
|
402
394
|
const {
|
|
403
395
|
ignoreMissingAsset,
|
|
@@ -434,9 +426,7 @@ async function calculateIntegrity(
|
|
|
434
426
|
if (!source) return null
|
|
435
427
|
} else if (!ownAsset && SCHEME_RE.test(url)) {
|
|
436
428
|
// data:/blob: and unknown schemes cannot be resolved to a bundle asset
|
|
437
|
-
|
|
438
|
-
logger.debug(`Skipping URL that is not a bundle asset: ${url}`);
|
|
439
|
-
}
|
|
429
|
+
logger.debug(`Skipping URL that is not a bundle asset: ${url}`);
|
|
440
430
|
return null
|
|
441
431
|
} else {
|
|
442
432
|
const bundleKey = getBundleKey(htmlPath, url, config);
|
|
@@ -449,9 +439,7 @@ async function calculateIntegrity(
|
|
|
449
439
|
const possibleMatch = findBundleKey(bundle, bundleKey, logger);
|
|
450
440
|
|
|
451
441
|
if (possibleMatch) {
|
|
452
|
-
|
|
453
|
-
logger.debug(`Bundle key fallback: ${bundleKey} -> ${possibleMatch}`);
|
|
454
|
-
}
|
|
442
|
+
logger.debug(`Bundle key fallback: ${bundleKey} -> ${possibleMatch}`);
|
|
455
443
|
bundleFileName = possibleMatch;
|
|
456
444
|
source = bundleSource(bundle[possibleMatch]);
|
|
457
445
|
} else {
|
|
@@ -460,11 +448,9 @@ async function calculateIntegrity(
|
|
|
460
448
|
|
|
461
449
|
if (!source) {
|
|
462
450
|
if (ignoreMissingAsset) {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
);
|
|
467
|
-
}
|
|
451
|
+
logger.warn(
|
|
452
|
+
`Asset not found in bundle or publicDir: ${url} (path: ${htmlPath}, key: ${bundleKey})`
|
|
453
|
+
);
|
|
468
454
|
return null
|
|
469
455
|
}
|
|
470
456
|
throw new Error(
|
|
@@ -515,7 +501,9 @@ const SRI_LINK_RELS = new Set(['stylesheet', 'modulepreload']);
|
|
|
515
501
|
function getAttr(tag, re) {
|
|
516
502
|
const match = tag.match(re);
|
|
517
503
|
if (!match) return null
|
|
518
|
-
|
|
504
|
+
// One of the three alternatives matched or the regex would not have, and an
|
|
505
|
+
// empty value is a string rather than undefined - so there is no fourth case.
|
|
506
|
+
return match[1] ?? match[2] ?? match[3]
|
|
519
507
|
}
|
|
520
508
|
|
|
521
509
|
const HTML_PATTERNS = {
|
|
@@ -767,9 +755,7 @@ async function transformHTML(
|
|
|
767
755
|
function createTransformer(options, config, cacheManager, logger) {
|
|
768
756
|
return {
|
|
769
757
|
transformHTML: (bundle, htmlPath, html) =>
|
|
770
|
-
transformHTML(bundle, htmlPath, html, options, config, cacheManager, logger)
|
|
771
|
-
calculateIntegrity: (bundle, htmlPath, url) =>
|
|
772
|
-
calculateIntegrity(bundle, htmlPath, url, options, config, cacheManager, logger)
|
|
758
|
+
transformHTML(bundle, htmlPath, html, options, config, cacheManager, logger)
|
|
773
759
|
}
|
|
774
760
|
}
|
|
775
761
|
|
|
@@ -840,13 +826,6 @@ class Logger {
|
|
|
840
826
|
console.debug(...this.formatMessage(message, ...args));
|
|
841
827
|
}
|
|
842
828
|
}
|
|
843
|
-
|
|
844
|
-
/**
|
|
845
|
-
* Create a child logger with the same configuration
|
|
846
|
-
*/
|
|
847
|
-
child(name) {
|
|
848
|
-
return new Logger(this.logLevel, `${this.pluginName}:${name}`)
|
|
849
|
-
}
|
|
850
829
|
}
|
|
851
830
|
|
|
852
831
|
// Vite 6/7 uses `vite:build-import-analysis`; Vite 8 Rolldown native path adds
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-plugin-sri4",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.1",
|
|
4
4
|
"description": "A Vite plugin to generate Subresource Integrity (SRI) hashes for output files.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"test": "vitest run",
|
|
25
25
|
"test:watch": "vitest",
|
|
26
26
|
"test:coverage": "vitest run --coverage",
|
|
27
|
-
"prepublishOnly": "npm run build"
|
|
27
|
+
"prepublishOnly": "npm run build",
|
|
28
|
+
"test:mutation": "stryker run"
|
|
28
29
|
},
|
|
29
30
|
"peerDependencies": {
|
|
30
31
|
"vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
|
|
@@ -32,6 +33,8 @@
|
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@rollup/plugin-commonjs": "^29.0.3",
|
|
34
35
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
36
|
+
"@stryker-mutator/core": "^10.0.0",
|
|
37
|
+
"@stryker-mutator/vitest-runner": "^10.0.0",
|
|
35
38
|
"@vitest/coverage-v8": "^5.0.0",
|
|
36
39
|
"oxlint": "^1.82.0",
|
|
37
40
|
"rollup": "^4.63.1",
|