vite-plugin-sri4 3.1.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 CHANGED
@@ -27,7 +27,7 @@ A Vite plugin to generate Subresource Integrity (SRI) hashes for your assets dur
27
27
  - **Bypass Domains:** Option to specify domains to bypass SRI injection.
28
28
  - **Missing Asset Handling:** Configurable warning suppression for missing assets.
29
29
  - **Robust Content Support:** Handles various content types including strings, Buffer, and Uint8Array.
30
- - **Vite Compatibility:** Compatible with Vite 6.0 and 7.0.
30
+ - **Vite Compatibility:** Compatible with Vite 7.0 and Vite 8.0 (including the Rolldown-based native build path).
31
31
 
32
32
  ## Installation
33
33
 
@@ -48,13 +48,13 @@ export default defineConfig({
48
48
  plugins: [
49
49
  sri({
50
50
  // Optional. The security hash algorithm. Defaults to "sha384".
51
- algorithm: 'sha384',
51
+ hashAlgorithm: 'sha384',
52
52
  // Optional. Domains to bypass SRI injection.
53
53
  bypassDomains: ['example.com'],
54
54
  // Optional. Suppress warnings for missing assets.
55
55
  ignoreMissingAsset: false,
56
- // Optional. Enable debug logging.
57
- debug: false
56
+ // Optional. Log verbosity: 'silent' | 'error' | 'warn' | 'info' | 'debug'. Defaults to 'warn'.
57
+ logLevel: 'warn'
58
58
  })
59
59
  ]
60
60
  });
@@ -76,14 +76,14 @@ Output:
76
76
 
77
77
  ## Plugin Options
78
78
 
79
- * `algorithm` (string):
80
- The hash algorithm used for computing SRI. Default is sha384. You may change it to other supported algorithms like sha256.
79
+ * `hashAlgorithm` (string):
80
+ The hash algorithm used for computing SRI. Default is `sha384`. You may change it to other supported algorithms like `sha256` or `sha512`.
81
81
  * `bypassDomains` (Array<string>):
82
82
  Array of domain names where SRI injection should be skipped. This allows external resources from specified domains to be excluded from SRI checks (for example, when they may not support CORS).
83
83
  * `ignoreMissingAsset` (boolean):
84
- When true, suppresses warnings for assets that are not found in the bundle. Default is false.
85
- * `debug` (boolean):
86
- When true, enables detailed debug logging. Default is false.
84
+ When true, suppresses warnings for assets that are not found in the bundle. Default is `false`.
85
+ * `logLevel` (string):
86
+ Log verbosity. One of `silent`, `error`, `warn`, `info`, `debug`. Default is `warn`. Use `debug` to see per-resource decisions during the build.
87
87
 
88
88
  ## Example Project
89
89
 
@@ -150,11 +150,11 @@ The example project shows:
150
150
 
151
151
  ### Debug Mode
152
152
 
153
- Enable debug mode to see detailed logs:
153
+ Set `logLevel: 'debug'` to see detailed logs:
154
154
 
155
155
  ```javascript
156
156
  sri({
157
- debug: true
157
+ logLevel: 'debug'
158
158
  })
159
159
  ```
160
160
 
@@ -163,6 +163,7 @@ This will show:
163
163
  - SRI hash computation
164
164
  - CORS checks
165
165
  - Missing asset warnings
166
+ - Bundle-key fallback matches (when a URL is resolved via suffix match)
166
167
 
167
168
  ## Contributing
168
169
 
package/dist/index.cjs CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var crypto = require('crypto');
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
9
  /**
@@ -237,12 +237,18 @@ async function calculateIntegrity(
237
237
  const bundleItem = bundle[bundleKey];
238
238
 
239
239
  if (!bundleItem) {
240
- // Try to find a matching item with more flexible matching
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.
241
244
  const possibleMatch = Object.keys(bundle).find(key =>
242
245
  key.endsWith(bundleKey) || bundleKey.endsWith(key)
243
246
  );
244
247
 
245
248
  if (possibleMatch) {
249
+ if (logger) {
250
+ logger.debug(`Bundle key fallback: ${bundleKey} -> ${possibleMatch}`);
251
+ }
246
252
  source = bundle[possibleMatch].type === 'chunk'
247
253
  ? bundle[possibleMatch].code
248
254
  : bundle[possibleMatch].source;
@@ -263,10 +269,10 @@ async function calculateIntegrity(
263
269
  if (!source) return null
264
270
 
265
271
  if (typeof source === 'string') {
266
- return `${hashAlgorithm}-${crypto.createHash(hashAlgorithm).update(source).digest('base64')}`
272
+ return `${hashAlgorithm}-${node_crypto.createHash(hashAlgorithm).update(source).digest('base64')}`
267
273
  }
268
274
 
269
- return `${hashAlgorithm}-${crypto.createHash(hashAlgorithm).update(Buffer.from(source)).digest('base64')}`
275
+ return `${hashAlgorithm}-${node_crypto.createHash(hashAlgorithm).update(Buffer.from(source)).digest('base64')}`
270
276
  }
271
277
 
272
278
  // Optimized regex patterns for better readability and efficiency
@@ -327,6 +333,7 @@ async function processMatch(
327
333
  return {
328
334
  integrity,
329
335
  position: end - endOffset,
336
+ tagStart: match.index,
330
337
  url // For logging
331
338
  }
332
339
  }
@@ -394,12 +401,21 @@ async function collectIntegrityChanges(
394
401
  return changes
395
402
  }
396
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
+ }
413
+
397
414
  /**
398
- * Check if integrity attribute already exists in HTML segment
415
+ * Check if crossorigin attribute already exists in the same tag
399
416
  */
400
- function hasExistingIntegrity(html, position, integrity) {
401
- const segment = html.slice(Math.max(0, position - 100), position + 100);
402
- return segment.includes(`integrity="${integrity}"`)
417
+ function hasExistingCrossorigin(html, tagStart, position) {
418
+ return CROSSORIGIN_ATTR_RE.test(html.slice(tagStart, position))
403
419
  }
404
420
 
405
421
  /**
@@ -409,13 +425,16 @@ function applyIntegrityChanges(html, changes, logger) {
409
425
  // Sort by position in descending order to insert from back to front
410
426
  changes.sort((a, b) => b.position - a.position);
411
427
 
412
- for (const { integrity, position, url } of changes) {
413
- // Skip if integrity attribute already exists
414
- if (hasExistingIntegrity(html, position, integrity)) {
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)) {
415
431
  continue
416
432
  }
417
433
 
418
- const insertText = ` integrity="${integrity}"`;
434
+ let insertText = ` integrity="${integrity}"`;
435
+ if (!hasExistingCrossorigin(html, tagStart, position)) {
436
+ insertText += ' crossorigin="anonymous"';
437
+ }
419
438
  html = html.slice(0, position) + insertText + html.slice(position);
420
439
  logger.debug(`Added integrity for: ${url}`);
421
440
  }
@@ -539,7 +558,12 @@ class Logger {
539
558
  }
540
559
 
541
560
  // Constants definition
542
- const VITE_INTERNAL_ANALYSIS_PLUGIN = 'vite:build-import-analysis';
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
+ ];
543
567
  const DEFAULT_HASH_ALGORITHM = 'sha384';
544
568
  const DEFAULT_PLUGIN_NAME = 'vite-plugin-sri4';
545
569
 
@@ -603,23 +627,31 @@ function sri(options = {}) {
603
627
  );
604
628
  };
605
629
 
606
- const plugin = config.plugins.find(p => p.name === VITE_INTERNAL_ANALYSIS_PLUGIN);
607
- if (!plugin) {
608
- throw new Error(`[${DEFAULT_PLUGIN_NAME}] requires Vite 2.0.0 or higher`)
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
+ )
609
639
  }
610
640
 
611
- if (typeof plugin.generateBundle === 'object' && plugin.generateBundle.handler) {
612
- const originalHandler = plugin.generateBundle.handler;
613
- plugin.generateBundle.handler = async function(...args) {
614
- await originalHandler.apply(this, args);
615
- await generateBundle.apply(this, args);
616
- };
617
- } else if (typeof plugin.generateBundle === 'function') {
618
- const originalHandler = plugin.generateBundle;
619
- plugin.generateBundle = async function(...args) {
620
- await originalHandler.apply(this, args);
621
- await generateBundle.apply(this, args);
622
- };
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
+ }
623
655
  }
624
656
  }
625
657
  }
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
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
5
  /**
@@ -233,12 +233,18 @@ async function calculateIntegrity(
233
233
  const bundleItem = bundle[bundleKey];
234
234
 
235
235
  if (!bundleItem) {
236
- // Try to find a matching item with more flexible matching
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.
237
240
  const possibleMatch = Object.keys(bundle).find(key =>
238
241
  key.endsWith(bundleKey) || bundleKey.endsWith(key)
239
242
  );
240
243
 
241
244
  if (possibleMatch) {
245
+ if (logger) {
246
+ logger.debug(`Bundle key fallback: ${bundleKey} -> ${possibleMatch}`);
247
+ }
242
248
  source = bundle[possibleMatch].type === 'chunk'
243
249
  ? bundle[possibleMatch].code
244
250
  : bundle[possibleMatch].source;
@@ -323,6 +329,7 @@ async function processMatch(
323
329
  return {
324
330
  integrity,
325
331
  position: end - endOffset,
332
+ tagStart: match.index,
326
333
  url // For logging
327
334
  }
328
335
  }
@@ -390,12 +397,21 @@ async function collectIntegrityChanges(
390
397
  return changes
391
398
  }
392
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
+ }
409
+
393
410
  /**
394
- * Check if integrity attribute already exists in HTML segment
411
+ * Check if crossorigin attribute already exists in the same tag
395
412
  */
396
- function hasExistingIntegrity(html, position, integrity) {
397
- const segment = html.slice(Math.max(0, position - 100), position + 100);
398
- return segment.includes(`integrity="${integrity}"`)
413
+ function hasExistingCrossorigin(html, tagStart, position) {
414
+ return CROSSORIGIN_ATTR_RE.test(html.slice(tagStart, position))
399
415
  }
400
416
 
401
417
  /**
@@ -405,13 +421,16 @@ function applyIntegrityChanges(html, changes, logger) {
405
421
  // Sort by position in descending order to insert from back to front
406
422
  changes.sort((a, b) => b.position - a.position);
407
423
 
408
- for (const { integrity, position, url } of changes) {
409
- // Skip if integrity attribute already exists
410
- if (hasExistingIntegrity(html, position, integrity)) {
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)) {
411
427
  continue
412
428
  }
413
429
 
414
- const insertText = ` integrity="${integrity}"`;
430
+ let insertText = ` integrity="${integrity}"`;
431
+ if (!hasExistingCrossorigin(html, tagStart, position)) {
432
+ insertText += ' crossorigin="anonymous"';
433
+ }
415
434
  html = html.slice(0, position) + insertText + html.slice(position);
416
435
  logger.debug(`Added integrity for: ${url}`);
417
436
  }
@@ -535,7 +554,12 @@ class Logger {
535
554
  }
536
555
 
537
556
  // Constants definition
538
- const VITE_INTERNAL_ANALYSIS_PLUGIN = 'vite:build-import-analysis';
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
+ ];
539
563
  const DEFAULT_HASH_ALGORITHM = 'sha384';
540
564
  const DEFAULT_PLUGIN_NAME = 'vite-plugin-sri4';
541
565
 
@@ -599,23 +623,31 @@ function sri(options = {}) {
599
623
  );
600
624
  };
601
625
 
602
- const plugin = config.plugins.find(p => p.name === VITE_INTERNAL_ANALYSIS_PLUGIN);
603
- if (!plugin) {
604
- throw new Error(`[${DEFAULT_PLUGIN_NAME}] requires Vite 2.0.0 or higher`)
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
+ )
605
635
  }
606
636
 
607
- if (typeof plugin.generateBundle === 'object' && plugin.generateBundle.handler) {
608
- const originalHandler = plugin.generateBundle.handler;
609
- plugin.generateBundle.handler = async function(...args) {
610
- await originalHandler.apply(this, args);
611
- await generateBundle.apply(this, args);
612
- };
613
- } else if (typeof plugin.generateBundle === 'function') {
614
- const originalHandler = plugin.generateBundle;
615
- plugin.generateBundle = async function(...args) {
616
- await originalHandler.apply(this, args);
617
- await generateBundle.apply(this, args);
618
- };
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
+ }
619
651
  }
620
652
  }
621
653
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-sri4",
3
- "version": "3.1.0",
3
+ "version": "4.0.0",
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",
@@ -23,20 +23,18 @@
23
23
  "prepublishOnly": "npm run build"
24
24
  },
25
25
  "dependencies": {
26
- "cheerio": "^1.0.0",
27
26
  "cross-fetch": "^4.1.0"
28
27
  },
29
28
  "peerDependencies": {
30
- "vite": "^6.0.0 || ^7.0.0"
29
+ "vite": "^7.0.0 || ^8.0.0"
31
30
  },
32
31
  "devDependencies": {
33
32
  "@rollup/plugin-commonjs": "^25.0.7",
34
33
  "@rollup/plugin-node-resolve": "^15.2.3",
35
34
  "@vitest/coverage-v8": "^3.2.4",
36
- "cross-fetch": "^4.0.0",
37
35
  "memfs": "^4.6.0",
38
36
  "rollup": "^4.9.6",
39
- "vite": "^7.0.0",
37
+ "vite": "^8.0.0",
40
38
  "vitest": "^3.2.4"
41
39
  },
42
40
  "keywords": [