vite-plugin-sri4 1.8.3 → 1.8.6

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
@@ -2,27 +2,39 @@
2
2
 
3
3
  ![NPM Version](https://img.shields.io/npm/v/vite-plugin-sri4)
4
4
  [![codecov](https://codecov.io/gh/7a6163/vite-plugin-sri4/graph/badge.svg?token=GOVB4J3D19)](https://codecov.io/gh/7a6163/vite-plugin-sri4)
5
+ ![License](https://img.shields.io/npm/l/vite-plugin-sri4)
5
6
 
6
7
  A Vite plugin to generate Subresource Integrity (SRI) hashes for your assets during the build process. This plugin computes SRI hashes for JavaScript and CSS files and injects them as `integrity` and `crossorigin="anonymous"` attributes into your HTML, ensuring your resources have not been tampered with when loaded by browsers.
7
8
 
9
+ ## Table of Contents
10
+
11
+ - [Features](#features)
12
+ - [Installation](#installation)
13
+ - [Usage](#usage)
14
+ - [Plugin Options](#plugin-options)
15
+ - [Example Project](#example-project)
16
+ - [Best Practices](#best-practices)
17
+ - [Troubleshooting](#troubleshooting)
18
+ - [Contributing](#contributing)
19
+ - [License](#license)
20
+
8
21
  ## Features
9
22
 
10
23
  - **Automatic SRI Generation:** Computes SRI hashes for assets (chunks and files) using a configurable algorithm (default is `sha384`).
11
24
  - **HTML Injection:** Automatically injects `integrity` and `crossorigin` attributes into `<script>` and `<link>` tags in your HTML.
12
25
  - **CORS Support Check:** For external resources, a CORS check is performed to verify access via `Access-Control-Allow-Origin`.
13
26
  - **Bypass Domains:** Option to specify domains to bypass SRI injection.
27
+ - **Missing Asset Handling:** Configurable warning suppression for missing assets.
28
+ - **Robust Content Support:** Handles various content types including strings, Buffer, and Uint8Array.
14
29
 
15
30
  ## Installation
16
31
 
17
- If the plugin has been published to npm:
18
-
19
32
  ```bash
20
33
  npm install vite-plugin-sri4 --save-dev
21
34
  ```
22
35
 
23
- Alternatively, if you're developing locally, you can use npm link or install via a relative path.
24
-
25
36
  ## Usage
37
+
26
38
  Add the plugin to your Vite configuration by updating your vite.config.js or vite.config.ts file:
27
39
 
28
40
  ```javascript
@@ -36,23 +48,142 @@ export default defineConfig({
36
48
  // Optional. The security hash algorithm. Defaults to "sha384".
37
49
  algorithm: 'sha384',
38
50
  // Optional. Domains to bypass SRI injection.
39
- bypassDomains: ['example.com']
51
+ bypassDomains: ['example.com'],
52
+ // Optional. Suppress warnings for missing assets.
53
+ ignoreMissingAsset: false,
54
+ // Optional. Enable debug logging.
55
+ debug: false
40
56
  })
41
57
  ]
42
58
  });
43
59
  ```
44
60
 
45
- Now, when you run the build command:
61
+ ### Example HTML Output
46
62
 
47
- ```bash
48
- npm run build
63
+ Input:
64
+ ```html
65
+ <script src="app.js"></script>
66
+ <link rel="stylesheet" href="style.css">
49
67
  ```
50
68
 
51
- The plugin will process the generated bundles, compute SRI hashes, and inject the attributes into the HTML.
69
+ Output:
70
+ ```html
71
+ <script src="app.js" integrity="sha384-..." crossorigin="anonymous"></script>
72
+ <link rel="stylesheet" href="style.css" integrity="sha384-..." crossorigin="anonymous">
73
+ ```
52
74
 
53
75
  ## Plugin Options
54
76
 
55
- * algorithm (string):
56
- The hash algorithm used for computing SRI. Default is sha384. You may change it to other supported algorithms like sha256.
57
- * bypassDomains (Array<string>):
58
- 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).
77
+ * `algorithm` (string):
78
+ The hash algorithm used for computing SRI. Default is sha384. You may change it to other supported algorithms like sha256.
79
+ * `bypassDomains` (Array<string>):
80
+ 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).
81
+ * `ignoreMissingAsset` (boolean):
82
+ When true, suppresses warnings for assets that are not found in the bundle. Default is false.
83
+ * `debug` (boolean):
84
+ When true, enables detailed debug logging. Default is false.
85
+
86
+ ## Example Project
87
+
88
+ The plugin includes an example project in the `example` directory that demonstrates its usage with a simple Vite application. To try it:
89
+
90
+ 1. Clone the repository
91
+ 2. Install dependencies:
92
+ ```bash
93
+ npm install
94
+ cd example
95
+ npm install
96
+ ```
97
+ 3. Build the example:
98
+ ```bash
99
+ npm run build
100
+ ```
101
+ 4. Check the generated `dist/index.html` to see the SRI hashes in action
102
+
103
+ The example project shows:
104
+ - Basic setup with Vite
105
+ - SRI hash generation for JS and CSS files
106
+ - Handling of hashed filenames
107
+ - Static file handling
108
+
109
+ ## Best Practices
110
+
111
+ 1. **Hash Algorithm Selection**
112
+ - Use `sha384` (default) for a good balance of security and performance
113
+ - Consider `sha512` for maximum security
114
+ - Avoid `sha1` as it's considered cryptographically weak
115
+
116
+ 2. **CORS Configuration**
117
+ - Ensure your CDN or hosting service supports CORS
118
+ - Set appropriate `Access-Control-Allow-Origin` headers
119
+ - Use `bypassDomains` for trusted domains that don't support CORS
120
+
121
+ 3. **Performance Optimization**
122
+ - Enable `ignoreMissingAsset` in development for faster builds
123
+ - Use debug mode only when troubleshooting
124
+
125
+ 4. **Security Considerations**
126
+ - Always use HTTPS for external resources
127
+ - Regularly update the plugin for security fixes
128
+ - Keep your dependencies up to date
129
+
130
+ ## Troubleshooting
131
+
132
+ ### Common Issues
133
+
134
+ 1. **Missing Integrity Attributes**
135
+ - Check if the file is in your build output
136
+ - Verify the file path is correct
137
+ - Enable debug mode to see detailed logs
138
+
139
+ 2. **CORS Errors**
140
+ - Ensure the resource supports CORS
141
+ - Add the domain to `bypassDomains` if needed
142
+ - Check network tab for CORS headers
143
+
144
+ 3. **Build Performance**
145
+ - Use `ignoreMissingAsset` if you have many external resources
146
+ - Disable debug mode in production
147
+ - Consider using a CDN for external resources
148
+
149
+ ### Debug Mode
150
+
151
+ Enable debug mode to see detailed logs:
152
+
153
+ ```javascript
154
+ sri({
155
+ debug: true
156
+ })
157
+ ```
158
+
159
+ This will show:
160
+ - Asset processing steps
161
+ - SRI hash computation
162
+ - CORS checks
163
+ - Missing asset warnings
164
+
165
+ ## Contributing
166
+
167
+ We welcome contributions! Here's how you can help:
168
+
169
+ 1. Fork the repository
170
+ 2. Create your feature branch: `git checkout -b feature/my-feature`
171
+ 3. Commit your changes: `git commit -am 'Add some feature'`
172
+ 4. Push to the branch: `git push origin feature/my-feature`
173
+ 5. Submit a pull request
174
+
175
+ Please make sure to:
176
+ - Update the documentation
177
+ - Add tests for new features
178
+ - Follow the existing code style
179
+ - Update the CHANGELOG.md
180
+
181
+ ## License
182
+
183
+ This project is licensed under the MIT License - see the LICENSE file for details.
184
+
185
+ ## Support
186
+
187
+ - Create an issue for bug reports
188
+ - Star the project if you find it useful
189
+ - Follow the author for updates
package/dist/index.cjs CHANGED
@@ -9,7 +9,8 @@ const DEFAULT_OPTIONS = {
9
9
  algorithm: 'sha384',
10
10
  bypassDomains: [],
11
11
  crossorigin: 'anonymous',
12
- debug: false
12
+ debug: false,
13
+ ignoreMissingAsset: false
13
14
  };
14
15
 
15
16
  function log(message, options) {
@@ -20,10 +21,15 @@ function log(message, options) {
20
21
 
21
22
  function computeSri(content, algorithm = 'sha384') {
22
23
  try {
23
- const hash = node_crypto.createHash(algorithm)
24
- .update(content)
25
- .digest('base64');
26
- return `${algorithm}-${hash}`;
24
+ const hash = node_crypto.createHash(algorithm);
25
+ if (Buffer.isBuffer(content) || content instanceof Uint8Array) {
26
+ hash.update(content);
27
+ } else if (typeof content === 'string') {
28
+ hash.update(Buffer.from(content, 'utf-8'));
29
+ } else {
30
+ throw new Error('Invalid content type');
31
+ }
32
+ return `${algorithm}-${hash.digest('base64')}`;
27
33
  } catch (error) {
28
34
  console.error(`${LOG_PREFIX} Failed to compute SRI hash: ${error}`);
29
35
  return null;
@@ -49,12 +55,22 @@ async function externalResourceIsCorsEnabled(url, options) {
49
55
  function isBypassDomain(url, bypassDomains = []) {
50
56
  if (!bypassDomains.length) return false;
51
57
  try {
52
- const parsedUrl = url.startsWith('//')
53
- ? new URL(`http:${url}`)
54
- : new URL(url, 'http://dummy');
55
- return bypassDomains.some(
56
- (domain) => parsedUrl.hostname === domain ||
57
- parsedUrl.hostname.endsWith(`.${domain}`)
58
+ let hostname = url;
59
+
60
+ if (hostname.startsWith('http://')) {
61
+ hostname = hostname.slice(7);
62
+ } else if (hostname.startsWith('https://')) {
63
+ hostname = hostname.slice(8);
64
+ } else if (hostname.startsWith('//')) {
65
+ hostname = hostname.slice(2);
66
+ }
67
+
68
+ hostname = hostname.split('/')[0];
69
+
70
+ hostname = hostname.split(':')[0];
71
+
72
+ return bypassDomains.some(domain =>
73
+ hostname === domain || hostname.endsWith(`.${domain}`)
58
74
  );
59
75
  } catch (e) {
60
76
  return false;
@@ -66,23 +82,29 @@ function hasCrossOriginAttr(tag) {
66
82
  }
67
83
 
68
84
  function getBundleKey(url, base = '') {
69
- // 嘗試各種可能的路徑格式
70
- const possiblePaths = [
71
- url,
72
- url.replace(/^\//, ''),
73
- url.replace(/^\/static\//, ''),
74
- url.replace(/^static\//, ''),
75
- url.replace(base, ''),
76
- url.replace(base, '').replace(/^\//, '')
85
+ // Remove base prefix if exists
86
+ let cleanUrl = url.startsWith(base) ? url.slice(base.length) : url;
87
+ // Remove leading slash
88
+ cleanUrl = cleanUrl.startsWith('/') ? cleanUrl.slice(1) : cleanUrl;
89
+
90
+ // Try different path combinations
91
+ const paths = [
92
+ cleanUrl,
93
+ `static/${cleanUrl}`,
94
+ cleanUrl.replace(/^static\//, '')
77
95
  ];
78
96
 
79
- // 移除 hash 後的版本
80
- const withoutHash = url.replace(/-[a-zA-Z0-9]+\.([^.]+)$/, '.$1');
81
- if (withoutHash !== url) {
82
- possiblePaths.push(...getBundleKey(withoutHash, base));
97
+ // Remove hash part if exists and try again
98
+ const withoutHash = cleanUrl.replace(/-[a-zA-Z0-9]+\.([^.]+)$/, '.$1');
99
+ if (withoutHash !== cleanUrl) {
100
+ paths.push(...[
101
+ withoutHash,
102
+ `static/${withoutHash}`,
103
+ withoutHash.replace(/^static\//, '')
104
+ ]);
83
105
  }
84
106
 
85
- return [...new Set(possiblePaths)];
107
+ return [...new Set(paths)];
86
108
  }
87
109
 
88
110
  async function processTag(tag, url, options, bundle, base = '') {
@@ -128,6 +150,10 @@ async function processTag(tag, url, options, bundle, base = '') {
128
150
  // Handle local resources
129
151
  const possibleKeys = getBundleKey(url, base);
130
152
  let bundleItem = null;
153
+ let source;
154
+
155
+ log(`Looking for bundle keys:`, options);
156
+ possibleKeys.forEach(key => log(`- ${key}`, options));
131
157
 
132
158
  for (const key of possibleKeys) {
133
159
  if (bundle[key]) {
@@ -137,8 +163,29 @@ async function processTag(tag, url, options, bundle, base = '') {
137
163
  }
138
164
  }
139
165
 
140
- if (bundleItem) {
141
- const source = bundleItem.type === 'chunk' ? bundleItem.code : bundleItem.source;
166
+ if (!bundleItem) {
167
+ log(`Bundle item not found for ${url}`, options);
168
+ if (!options.ignoreMissingAsset) {
169
+ console.warn(`${LOG_PREFIX} Asset not found in bundle: ${url}`);
170
+ log(`Available bundle keys:`, options);
171
+ Object.keys(bundle).forEach(key => log(`- ${key}`, options));
172
+ } else {
173
+ log(`Ignoring missing asset due to ignoreMissingAsset option`, options);
174
+ }
175
+ return tag;
176
+ }
177
+
178
+ log(`Bundle item type: ${bundleItem.type}`, options);
179
+
180
+ try {
181
+ if (bundleItem.type === 'chunk') {
182
+ source = bundleItem.code;
183
+ log(`Processing chunk content of length: ${source.length}`, options);
184
+ } else {
185
+ source = bundleItem.source;
186
+ log(`Processing asset content of length: ${source.length}`, options);
187
+ }
188
+
142
189
  const integrity = computeSri(source, options.algorithm);
143
190
  if (integrity) {
144
191
  log(`Computing SRI for local resource ${url}: ${integrity}`, options);
@@ -148,9 +195,11 @@ async function processTag(tag, url, options, bundle, base = '') {
148
195
  log(`New tag: ${newTag}`, options);
149
196
  return newTag;
150
197
  }
151
- } else {
152
- log(`No bundle item found for ${url}`, options);
153
- log(`Available bundle keys: ${Object.keys(bundle).join(', ')}`, options);
198
+ } catch (error) {
199
+ log(`Error processing bundle item: ${error}`, options);
200
+ if (!options.ignoreMissingAsset) {
201
+ console.error(`${LOG_PREFIX} Failed to process asset: ${url}`, error);
202
+ }
154
203
  }
155
204
 
156
205
  return tag;
@@ -172,6 +221,7 @@ function sri(userOptions = {}) {
172
221
  base = config.base || '';
173
222
  log('Plugin configured in ' + (isBuild ? 'build' : 'dev') + ' mode', options);
174
223
  log(`Base URL: ${base}`, options);
224
+ log(`ignoreMissingAsset: ${options.ignoreMissingAsset}`, options);
175
225
  },
176
226
 
177
227
  async transformIndexHtml(html, ctx) {
package/dist/index.js CHANGED
@@ -7,7 +7,8 @@ const DEFAULT_OPTIONS = {
7
7
  algorithm: 'sha384',
8
8
  bypassDomains: [],
9
9
  crossorigin: 'anonymous',
10
- debug: false
10
+ debug: false,
11
+ ignoreMissingAsset: false
11
12
  };
12
13
 
13
14
  function log(message, options) {
@@ -18,10 +19,15 @@ function log(message, options) {
18
19
 
19
20
  function computeSri(content, algorithm = 'sha384') {
20
21
  try {
21
- const hash = createHash(algorithm)
22
- .update(content)
23
- .digest('base64');
24
- return `${algorithm}-${hash}`;
22
+ const hash = createHash(algorithm);
23
+ if (Buffer.isBuffer(content) || content instanceof Uint8Array) {
24
+ hash.update(content);
25
+ } else if (typeof content === 'string') {
26
+ hash.update(Buffer.from(content, 'utf-8'));
27
+ } else {
28
+ throw new Error('Invalid content type');
29
+ }
30
+ return `${algorithm}-${hash.digest('base64')}`;
25
31
  } catch (error) {
26
32
  console.error(`${LOG_PREFIX} Failed to compute SRI hash: ${error}`);
27
33
  return null;
@@ -47,12 +53,22 @@ async function externalResourceIsCorsEnabled(url, options) {
47
53
  function isBypassDomain(url, bypassDomains = []) {
48
54
  if (!bypassDomains.length) return false;
49
55
  try {
50
- const parsedUrl = url.startsWith('//')
51
- ? new URL(`http:${url}`)
52
- : new URL(url, 'http://dummy');
53
- return bypassDomains.some(
54
- (domain) => parsedUrl.hostname === domain ||
55
- parsedUrl.hostname.endsWith(`.${domain}`)
56
+ let hostname = url;
57
+
58
+ if (hostname.startsWith('http://')) {
59
+ hostname = hostname.slice(7);
60
+ } else if (hostname.startsWith('https://')) {
61
+ hostname = hostname.slice(8);
62
+ } else if (hostname.startsWith('//')) {
63
+ hostname = hostname.slice(2);
64
+ }
65
+
66
+ hostname = hostname.split('/')[0];
67
+
68
+ hostname = hostname.split(':')[0];
69
+
70
+ return bypassDomains.some(domain =>
71
+ hostname === domain || hostname.endsWith(`.${domain}`)
56
72
  );
57
73
  } catch (e) {
58
74
  return false;
@@ -64,23 +80,29 @@ function hasCrossOriginAttr(tag) {
64
80
  }
65
81
 
66
82
  function getBundleKey(url, base = '') {
67
- // 嘗試各種可能的路徑格式
68
- const possiblePaths = [
69
- url,
70
- url.replace(/^\//, ''),
71
- url.replace(/^\/static\//, ''),
72
- url.replace(/^static\//, ''),
73
- url.replace(base, ''),
74
- url.replace(base, '').replace(/^\//, '')
83
+ // Remove base prefix if exists
84
+ let cleanUrl = url.startsWith(base) ? url.slice(base.length) : url;
85
+ // Remove leading slash
86
+ cleanUrl = cleanUrl.startsWith('/') ? cleanUrl.slice(1) : cleanUrl;
87
+
88
+ // Try different path combinations
89
+ const paths = [
90
+ cleanUrl,
91
+ `static/${cleanUrl}`,
92
+ cleanUrl.replace(/^static\//, '')
75
93
  ];
76
94
 
77
- // 移除 hash 後的版本
78
- const withoutHash = url.replace(/-[a-zA-Z0-9]+\.([^.]+)$/, '.$1');
79
- if (withoutHash !== url) {
80
- possiblePaths.push(...getBundleKey(withoutHash, base));
95
+ // Remove hash part if exists and try again
96
+ const withoutHash = cleanUrl.replace(/-[a-zA-Z0-9]+\.([^.]+)$/, '.$1');
97
+ if (withoutHash !== cleanUrl) {
98
+ paths.push(...[
99
+ withoutHash,
100
+ `static/${withoutHash}`,
101
+ withoutHash.replace(/^static\//, '')
102
+ ]);
81
103
  }
82
104
 
83
- return [...new Set(possiblePaths)];
105
+ return [...new Set(paths)];
84
106
  }
85
107
 
86
108
  async function processTag(tag, url, options, bundle, base = '') {
@@ -126,6 +148,10 @@ async function processTag(tag, url, options, bundle, base = '') {
126
148
  // Handle local resources
127
149
  const possibleKeys = getBundleKey(url, base);
128
150
  let bundleItem = null;
151
+ let source;
152
+
153
+ log(`Looking for bundle keys:`, options);
154
+ possibleKeys.forEach(key => log(`- ${key}`, options));
129
155
 
130
156
  for (const key of possibleKeys) {
131
157
  if (bundle[key]) {
@@ -135,8 +161,29 @@ async function processTag(tag, url, options, bundle, base = '') {
135
161
  }
136
162
  }
137
163
 
138
- if (bundleItem) {
139
- const source = bundleItem.type === 'chunk' ? bundleItem.code : bundleItem.source;
164
+ if (!bundleItem) {
165
+ log(`Bundle item not found for ${url}`, options);
166
+ if (!options.ignoreMissingAsset) {
167
+ console.warn(`${LOG_PREFIX} Asset not found in bundle: ${url}`);
168
+ log(`Available bundle keys:`, options);
169
+ Object.keys(bundle).forEach(key => log(`- ${key}`, options));
170
+ } else {
171
+ log(`Ignoring missing asset due to ignoreMissingAsset option`, options);
172
+ }
173
+ return tag;
174
+ }
175
+
176
+ log(`Bundle item type: ${bundleItem.type}`, options);
177
+
178
+ try {
179
+ if (bundleItem.type === 'chunk') {
180
+ source = bundleItem.code;
181
+ log(`Processing chunk content of length: ${source.length}`, options);
182
+ } else {
183
+ source = bundleItem.source;
184
+ log(`Processing asset content of length: ${source.length}`, options);
185
+ }
186
+
140
187
  const integrity = computeSri(source, options.algorithm);
141
188
  if (integrity) {
142
189
  log(`Computing SRI for local resource ${url}: ${integrity}`, options);
@@ -146,9 +193,11 @@ async function processTag(tag, url, options, bundle, base = '') {
146
193
  log(`New tag: ${newTag}`, options);
147
194
  return newTag;
148
195
  }
149
- } else {
150
- log(`No bundle item found for ${url}`, options);
151
- log(`Available bundle keys: ${Object.keys(bundle).join(', ')}`, options);
196
+ } catch (error) {
197
+ log(`Error processing bundle item: ${error}`, options);
198
+ if (!options.ignoreMissingAsset) {
199
+ console.error(`${LOG_PREFIX} Failed to process asset: ${url}`, error);
200
+ }
152
201
  }
153
202
 
154
203
  return tag;
@@ -170,6 +219,7 @@ function sri(userOptions = {}) {
170
219
  base = config.base || '';
171
220
  log('Plugin configured in ' + (isBuild ? 'build' : 'dev') + ' mode', options);
172
221
  log(`Base URL: ${base}`, options);
222
+ log(`ignoreMissingAsset: ${options.ignoreMissingAsset}`, options);
173
223
  },
174
224
 
175
225
  async transformIndexHtml(html, ctx) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-sri4",
3
- "version": "1.8.3",
3
+ "version": "1.8.6",
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",