vite-plugin-sri4 1.8.5 → 1.8.7

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.
Files changed (4) hide show
  1. package/README.md +144 -13
  2. package/dist/index.cjs +154 -127
  3. package/dist/index.js +154 -127
  4. package/package.json +1 -1
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) {
@@ -46,7 +47,7 @@ async function externalResourceIsCorsEnabled(url, options) {
46
47
  }
47
48
  return false;
48
49
  } catch (error) {
49
- log(`Failed to fetch CORS headers from ${url}: ${error}`, options);
50
+ console.error(`${LOG_PREFIX} Failed to fetch CORS headers from ${url}`, error);
50
51
  return false;
51
52
  }
52
53
  }
@@ -54,12 +55,22 @@ async function externalResourceIsCorsEnabled(url, options) {
54
55
  function isBypassDomain(url, bypassDomains = []) {
55
56
  if (!bypassDomains.length) return false;
56
57
  try {
57
- const parsedUrl = url.startsWith('//')
58
- ? new URL(`http:${url}`)
59
- : new URL(url, 'http://dummy');
60
- return bypassDomains.some(
61
- (domain) => parsedUrl.hostname === domain ||
62
- 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}`)
63
74
  );
64
75
  } catch (e) {
65
76
  return false;
@@ -96,99 +107,12 @@ function getBundleKey(url, base = '') {
96
107
  return [...new Set(paths)];
97
108
  }
98
109
 
99
- async function processTag(tag, url, options, bundle, base = '') {
100
- if (tag.includes('integrity=')) {
101
- log(`Skip tag with existing integrity attribute: ${tag}`, options);
102
- return tag;
103
- }
104
-
105
- log(`Processing tag: ${tag}`, options);
106
- log(`URL: ${url}`, options);
107
-
108
- // Handle external resources
109
- if (/^(https?:)?\/\//i.test(url)) {
110
- if (isBypassDomain(url, options.bypassDomains)) {
111
- log(`Skip SRI for bypass domain: ${url}`, options);
112
- return tag;
113
- }
114
-
115
- const corsOk = await externalResourceIsCorsEnabled(url, options);
116
- if (!corsOk) {
117
- log(`External resource ${url} does not support CORS`, options);
118
- return tag;
119
- }
120
-
121
- try {
122
- const response = await fetch(url);
123
- const content = await response.arrayBuffer();
124
- const hash = computeSri(Buffer.from(content), options.algorithm);
125
- if (hash) {
126
- log(`Computing SRI for external resource ${url}: ${hash}`, options);
127
- const hasCrossOrigin = hasCrossOriginAttr(tag);
128
- const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
129
- const newTag = tag.replace(/>$/, ` integrity="${hash}"${crossOriginAttr}>`);
130
- log(`New tag: ${newTag}`, options);
131
- return newTag;
132
- }
133
- } catch (error) {
134
- log(`Failed to process external resource ${url}: ${error}`, options);
135
- }
136
- return tag;
137
- }
138
-
139
- // Handle local resources
140
- const possibleKeys = getBundleKey(url, base);
141
- let bundleItem = null;
142
- let source;
143
-
144
- log(`Looking for bundle keys:`, options);
145
- possibleKeys.forEach(key => log(`- ${key}`, options));
146
-
147
- for (const key of possibleKeys) {
148
- if (bundle[key]) {
149
- bundleItem = bundle[key];
150
- log(`Found bundle item for key: ${key}`, options);
151
- break;
152
- }
153
- }
154
-
155
- if (!bundleItem) {
156
- log(`Available bundle keys:`, options);
157
- Object.keys(bundle).forEach(key => log(`- ${key}`, options));
158
- return tag;
159
- }
160
-
161
- log(`Bundle item type: ${bundleItem.type}`, options);
162
-
163
- try {
164
- if (bundleItem.type === 'chunk') {
165
- source = bundleItem.code;
166
- log(`Processing chunk content of length: ${source.length}`, options);
167
- } else {
168
- source = bundleItem.source;
169
- log(`Processing asset content of length: ${source.length}`, options);
170
- }
171
-
172
- const integrity = computeSri(source, options.algorithm);
173
- if (integrity) {
174
- log(`Computing SRI for local resource ${url}: ${integrity}`, options);
175
- const hasCrossOrigin = hasCrossOriginAttr(tag);
176
- const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
177
- const newTag = tag.replace(/>$/, ` integrity="${integrity}"${crossOriginAttr}>`);
178
- log(`New tag: ${newTag}`, options);
179
- return newTag;
180
- }
181
- } catch (error) {
182
- log(`Error processing bundle item: ${error}`, options);
183
- }
184
-
185
- return tag;
186
- }
187
-
188
110
  function sri(userOptions = {}) {
189
111
  const options = { ...DEFAULT_OPTIONS, ...userOptions };
190
112
  let isBuild = false;
191
113
  let base = '';
114
+ const htmlFiles = new Map(); // Store HTML file info for processing
115
+ const sriCache = new Map(); // Cache SRI hashes
192
116
 
193
117
  return {
194
118
  name: 'vite-plugin-sri4',
@@ -200,48 +124,151 @@ function sri(userOptions = {}) {
200
124
  isBuild = config.command === 'build';
201
125
  base = config.base || '';
202
126
  log('Plugin configured in ' + (isBuild ? 'build' : 'dev') + ' mode', options);
203
- log(`Base URL: ${base}`, options);
204
127
  },
205
128
 
206
129
  async transformIndexHtml(html, ctx) {
207
130
  if (!isBuild || !html) {
208
- log('Skipping HTML transform in dev mode or empty HTML', options);
209
131
  return html;
210
132
  }
211
133
 
212
- try {
213
- log('Starting HTML transformation', options);
214
- const bundle = ctx.bundle || {};
215
- log(`Bundle size: ${Object.keys(bundle).length}`, options);
216
-
217
- // Process script tags with and without quotes
218
- const scriptTagRegex = /<script[^>]+src=(?:["']([^"']+)["']|([^ >]+))[^>]*>/g;
219
- let match;
220
- while ((match = scriptTagRegex.exec(html)) !== null) {
221
- const [tag, quotedUrl, unquotedUrl] = match;
222
- const url = quotedUrl || unquotedUrl;
223
- log(`Processing script: ${url}`, options);
224
- const newTag = await processTag(tag, url, options, bundle, base);
225
- html = html.replace(tag, newTag);
134
+ // Store HTML file info for later processing
135
+ const resourceTags = [];
136
+
137
+ // Find script tags
138
+ const scriptTagRegex = /<script[^>]+src=(?:["']([^"']+)["']|([^ >]+))[^>]*>/g;
139
+ let match;
140
+ while ((match = scriptTagRegex.exec(html)) !== null) {
141
+ const [tag, quotedUrl, unquotedUrl] = match;
142
+ resourceTags.push({
143
+ tag,
144
+ url: quotedUrl || unquotedUrl,
145
+ type: 'script'
146
+ });
147
+ }
148
+
149
+ // Find link tags
150
+ const linkTagRegex = /<link[^>]+href=(?:["']([^"']+)["']|([^ >]+))[^>]*>/g;
151
+ while ((match = linkTagRegex.exec(html)) !== null) {
152
+ const [tag, quotedUrl, unquotedUrl] = match;
153
+ if (tag.includes('stylesheet') || tag.includes('modulepreload')) {
154
+ resourceTags.push({
155
+ tag,
156
+ url: quotedUrl || unquotedUrl,
157
+ type: 'link'
158
+ });
226
159
  }
160
+ }
161
+
162
+ // Store HTML file info
163
+ htmlFiles.set(ctx.filename, {
164
+ content: html,
165
+ resources: resourceTags
166
+ });
167
+
168
+ return html;
169
+ },
227
170
 
228
- // Process link tags with and without quotes
229
- const linkTagRegex = /<link[^>]+href=(?:["']([^"']+)["']|([^ >]+))[^>]*>/g;
230
- while ((match = linkTagRegex.exec(html)) !== null) {
231
- const [tag, quotedUrl, unquotedUrl] = match;
232
- const url = quotedUrl || unquotedUrl;
233
- if (tag.includes('stylesheet') || tag.includes('modulepreload')) {
234
- log(`Processing link: ${url}`, options);
235
- const newTag = await processTag(tag, url, options, bundle, base);
236
- html = html.replace(tag, newTag);
171
+ async writeBundle(options, bundle) {
172
+ for (const [filename, htmlInfo] of htmlFiles) {
173
+ let content = htmlInfo.content;
174
+
175
+ // Process all resources in parallel
176
+ const updates = await Promise.all(
177
+ htmlInfo.resources.map(async ({ tag, url, type }) => {
178
+ if (tag.includes('integrity=')) {
179
+ return null;
180
+ }
181
+
182
+ // Handle external resources
183
+ if (/^(https?:)?\/\//i.test(url)) {
184
+ if (isBypassDomain(url, options.bypassDomains)) {
185
+ return null;
186
+ }
187
+
188
+ // Check cache first
189
+ if (sriCache.has(url)) {
190
+ return {
191
+ tag,
192
+ newTag: sriCache.get(url)
193
+ };
194
+ }
195
+
196
+ const corsOk = await externalResourceIsCorsEnabled(url, options);
197
+ if (!corsOk) {
198
+ return null;
199
+ }
200
+
201
+ try {
202
+ const response = await fetch(url);
203
+ const content = await response.arrayBuffer();
204
+ const hash = computeSri(Buffer.from(content), options.algorithm);
205
+ if (hash) {
206
+ const hasCrossOrigin = hasCrossOriginAttr(tag);
207
+ const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
208
+ const newTag = tag.replace(/>$/, ` integrity="${hash}"${crossOriginAttr}>`);
209
+ sriCache.set(url, newTag);
210
+ return { tag, newTag };
211
+ }
212
+ } catch (error) {
213
+ log(`Failed to process external resource ${url}: ${error}`, options);
214
+ }
215
+ return null;
216
+ }
217
+
218
+ // Handle local resources
219
+ const possibleKeys = getBundleKey(url, base);
220
+ let bundleItem = null;
221
+
222
+ for (const key of possibleKeys) {
223
+ if (bundle[key]) {
224
+ bundleItem = bundle[key];
225
+ break;
226
+ }
227
+ }
228
+
229
+ if (!bundleItem) {
230
+ if (!options.ignoreMissingAsset) {
231
+ console.warn(`${LOG_PREFIX} Asset not found in bundle: ${url}`);
232
+ }
233
+ return null;
234
+ }
235
+
236
+ try {
237
+ const source = bundleItem.type === 'chunk' ? bundleItem.code : bundleItem.source;
238
+ const integrity = computeSri(source, options.algorithm);
239
+
240
+ if (integrity) {
241
+ const hasCrossOrigin = hasCrossOriginAttr(tag);
242
+ const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
243
+ const newTag = tag.replace(/>$/, ` integrity="${integrity}"${crossOriginAttr}>`);
244
+ return { tag, newTag };
245
+ }
246
+ } catch (error) {
247
+ if (!options.ignoreMissingAsset) {
248
+ console.error(`${LOG_PREFIX} Failed to process asset: ${url}`, error);
249
+ }
250
+ }
251
+
252
+ return null;
253
+ })
254
+ );
255
+
256
+ // Apply all updates to the HTML content
257
+ updates.forEach(update => {
258
+ if (update) {
259
+ content = content.replace(update.tag, update.newTag);
237
260
  }
238
- }
261
+ });
239
262
 
240
- return html;
241
- } catch (error) {
242
- console.error(`${LOG_PREFIX} Failed to transform HTML: ${error}`);
243
- return html;
263
+ // Write the modified content back to the bundle
264
+ if (bundle[filename]) {
265
+ bundle[filename].source = content;
266
+ }
244
267
  }
268
+
269
+ // Clear the caches
270
+ htmlFiles.clear();
271
+ sriCache.clear();
245
272
  }
246
273
  };
247
274
  }
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) {
@@ -44,7 +45,7 @@ async function externalResourceIsCorsEnabled(url, options) {
44
45
  }
45
46
  return false;
46
47
  } catch (error) {
47
- log(`Failed to fetch CORS headers from ${url}: ${error}`, options);
48
+ console.error(`${LOG_PREFIX} Failed to fetch CORS headers from ${url}`, error);
48
49
  return false;
49
50
  }
50
51
  }
@@ -52,12 +53,22 @@ async function externalResourceIsCorsEnabled(url, options) {
52
53
  function isBypassDomain(url, bypassDomains = []) {
53
54
  if (!bypassDomains.length) return false;
54
55
  try {
55
- const parsedUrl = url.startsWith('//')
56
- ? new URL(`http:${url}`)
57
- : new URL(url, 'http://dummy');
58
- return bypassDomains.some(
59
- (domain) => parsedUrl.hostname === domain ||
60
- 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}`)
61
72
  );
62
73
  } catch (e) {
63
74
  return false;
@@ -94,99 +105,12 @@ function getBundleKey(url, base = '') {
94
105
  return [...new Set(paths)];
95
106
  }
96
107
 
97
- async function processTag(tag, url, options, bundle, base = '') {
98
- if (tag.includes('integrity=')) {
99
- log(`Skip tag with existing integrity attribute: ${tag}`, options);
100
- return tag;
101
- }
102
-
103
- log(`Processing tag: ${tag}`, options);
104
- log(`URL: ${url}`, options);
105
-
106
- // Handle external resources
107
- if (/^(https?:)?\/\//i.test(url)) {
108
- if (isBypassDomain(url, options.bypassDomains)) {
109
- log(`Skip SRI for bypass domain: ${url}`, options);
110
- return tag;
111
- }
112
-
113
- const corsOk = await externalResourceIsCorsEnabled(url, options);
114
- if (!corsOk) {
115
- log(`External resource ${url} does not support CORS`, options);
116
- return tag;
117
- }
118
-
119
- try {
120
- const response = await fetch(url);
121
- const content = await response.arrayBuffer();
122
- const hash = computeSri(Buffer.from(content), options.algorithm);
123
- if (hash) {
124
- log(`Computing SRI for external resource ${url}: ${hash}`, options);
125
- const hasCrossOrigin = hasCrossOriginAttr(tag);
126
- const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
127
- const newTag = tag.replace(/>$/, ` integrity="${hash}"${crossOriginAttr}>`);
128
- log(`New tag: ${newTag}`, options);
129
- return newTag;
130
- }
131
- } catch (error) {
132
- log(`Failed to process external resource ${url}: ${error}`, options);
133
- }
134
- return tag;
135
- }
136
-
137
- // Handle local resources
138
- const possibleKeys = getBundleKey(url, base);
139
- let bundleItem = null;
140
- let source;
141
-
142
- log(`Looking for bundle keys:`, options);
143
- possibleKeys.forEach(key => log(`- ${key}`, options));
144
-
145
- for (const key of possibleKeys) {
146
- if (bundle[key]) {
147
- bundleItem = bundle[key];
148
- log(`Found bundle item for key: ${key}`, options);
149
- break;
150
- }
151
- }
152
-
153
- if (!bundleItem) {
154
- log(`Available bundle keys:`, options);
155
- Object.keys(bundle).forEach(key => log(`- ${key}`, options));
156
- return tag;
157
- }
158
-
159
- log(`Bundle item type: ${bundleItem.type}`, options);
160
-
161
- try {
162
- if (bundleItem.type === 'chunk') {
163
- source = bundleItem.code;
164
- log(`Processing chunk content of length: ${source.length}`, options);
165
- } else {
166
- source = bundleItem.source;
167
- log(`Processing asset content of length: ${source.length}`, options);
168
- }
169
-
170
- const integrity = computeSri(source, options.algorithm);
171
- if (integrity) {
172
- log(`Computing SRI for local resource ${url}: ${integrity}`, options);
173
- const hasCrossOrigin = hasCrossOriginAttr(tag);
174
- const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
175
- const newTag = tag.replace(/>$/, ` integrity="${integrity}"${crossOriginAttr}>`);
176
- log(`New tag: ${newTag}`, options);
177
- return newTag;
178
- }
179
- } catch (error) {
180
- log(`Error processing bundle item: ${error}`, options);
181
- }
182
-
183
- return tag;
184
- }
185
-
186
108
  function sri(userOptions = {}) {
187
109
  const options = { ...DEFAULT_OPTIONS, ...userOptions };
188
110
  let isBuild = false;
189
111
  let base = '';
112
+ const htmlFiles = new Map(); // Store HTML file info for processing
113
+ const sriCache = new Map(); // Cache SRI hashes
190
114
 
191
115
  return {
192
116
  name: 'vite-plugin-sri4',
@@ -198,48 +122,151 @@ function sri(userOptions = {}) {
198
122
  isBuild = config.command === 'build';
199
123
  base = config.base || '';
200
124
  log('Plugin configured in ' + (isBuild ? 'build' : 'dev') + ' mode', options);
201
- log(`Base URL: ${base}`, options);
202
125
  },
203
126
 
204
127
  async transformIndexHtml(html, ctx) {
205
128
  if (!isBuild || !html) {
206
- log('Skipping HTML transform in dev mode or empty HTML', options);
207
129
  return html;
208
130
  }
209
131
 
210
- try {
211
- log('Starting HTML transformation', options);
212
- const bundle = ctx.bundle || {};
213
- log(`Bundle size: ${Object.keys(bundle).length}`, options);
214
-
215
- // Process script tags with and without quotes
216
- const scriptTagRegex = /<script[^>]+src=(?:["']([^"']+)["']|([^ >]+))[^>]*>/g;
217
- let match;
218
- while ((match = scriptTagRegex.exec(html)) !== null) {
219
- const [tag, quotedUrl, unquotedUrl] = match;
220
- const url = quotedUrl || unquotedUrl;
221
- log(`Processing script: ${url}`, options);
222
- const newTag = await processTag(tag, url, options, bundle, base);
223
- html = html.replace(tag, newTag);
132
+ // Store HTML file info for later processing
133
+ const resourceTags = [];
134
+
135
+ // Find script tags
136
+ const scriptTagRegex = /<script[^>]+src=(?:["']([^"']+)["']|([^ >]+))[^>]*>/g;
137
+ let match;
138
+ while ((match = scriptTagRegex.exec(html)) !== null) {
139
+ const [tag, quotedUrl, unquotedUrl] = match;
140
+ resourceTags.push({
141
+ tag,
142
+ url: quotedUrl || unquotedUrl,
143
+ type: 'script'
144
+ });
145
+ }
146
+
147
+ // Find link tags
148
+ const linkTagRegex = /<link[^>]+href=(?:["']([^"']+)["']|([^ >]+))[^>]*>/g;
149
+ while ((match = linkTagRegex.exec(html)) !== null) {
150
+ const [tag, quotedUrl, unquotedUrl] = match;
151
+ if (tag.includes('stylesheet') || tag.includes('modulepreload')) {
152
+ resourceTags.push({
153
+ tag,
154
+ url: quotedUrl || unquotedUrl,
155
+ type: 'link'
156
+ });
224
157
  }
158
+ }
159
+
160
+ // Store HTML file info
161
+ htmlFiles.set(ctx.filename, {
162
+ content: html,
163
+ resources: resourceTags
164
+ });
165
+
166
+ return html;
167
+ },
225
168
 
226
- // Process link tags with and without quotes
227
- const linkTagRegex = /<link[^>]+href=(?:["']([^"']+)["']|([^ >]+))[^>]*>/g;
228
- while ((match = linkTagRegex.exec(html)) !== null) {
229
- const [tag, quotedUrl, unquotedUrl] = match;
230
- const url = quotedUrl || unquotedUrl;
231
- if (tag.includes('stylesheet') || tag.includes('modulepreload')) {
232
- log(`Processing link: ${url}`, options);
233
- const newTag = await processTag(tag, url, options, bundle, base);
234
- html = html.replace(tag, newTag);
169
+ async writeBundle(options, bundle) {
170
+ for (const [filename, htmlInfo] of htmlFiles) {
171
+ let content = htmlInfo.content;
172
+
173
+ // Process all resources in parallel
174
+ const updates = await Promise.all(
175
+ htmlInfo.resources.map(async ({ tag, url, type }) => {
176
+ if (tag.includes('integrity=')) {
177
+ return null;
178
+ }
179
+
180
+ // Handle external resources
181
+ if (/^(https?:)?\/\//i.test(url)) {
182
+ if (isBypassDomain(url, options.bypassDomains)) {
183
+ return null;
184
+ }
185
+
186
+ // Check cache first
187
+ if (sriCache.has(url)) {
188
+ return {
189
+ tag,
190
+ newTag: sriCache.get(url)
191
+ };
192
+ }
193
+
194
+ const corsOk = await externalResourceIsCorsEnabled(url, options);
195
+ if (!corsOk) {
196
+ return null;
197
+ }
198
+
199
+ try {
200
+ const response = await fetch(url);
201
+ const content = await response.arrayBuffer();
202
+ const hash = computeSri(Buffer.from(content), options.algorithm);
203
+ if (hash) {
204
+ const hasCrossOrigin = hasCrossOriginAttr(tag);
205
+ const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
206
+ const newTag = tag.replace(/>$/, ` integrity="${hash}"${crossOriginAttr}>`);
207
+ sriCache.set(url, newTag);
208
+ return { tag, newTag };
209
+ }
210
+ } catch (error) {
211
+ log(`Failed to process external resource ${url}: ${error}`, options);
212
+ }
213
+ return null;
214
+ }
215
+
216
+ // Handle local resources
217
+ const possibleKeys = getBundleKey(url, base);
218
+ let bundleItem = null;
219
+
220
+ for (const key of possibleKeys) {
221
+ if (bundle[key]) {
222
+ bundleItem = bundle[key];
223
+ break;
224
+ }
225
+ }
226
+
227
+ if (!bundleItem) {
228
+ if (!options.ignoreMissingAsset) {
229
+ console.warn(`${LOG_PREFIX} Asset not found in bundle: ${url}`);
230
+ }
231
+ return null;
232
+ }
233
+
234
+ try {
235
+ const source = bundleItem.type === 'chunk' ? bundleItem.code : bundleItem.source;
236
+ const integrity = computeSri(source, options.algorithm);
237
+
238
+ if (integrity) {
239
+ const hasCrossOrigin = hasCrossOriginAttr(tag);
240
+ const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
241
+ const newTag = tag.replace(/>$/, ` integrity="${integrity}"${crossOriginAttr}>`);
242
+ return { tag, newTag };
243
+ }
244
+ } catch (error) {
245
+ if (!options.ignoreMissingAsset) {
246
+ console.error(`${LOG_PREFIX} Failed to process asset: ${url}`, error);
247
+ }
248
+ }
249
+
250
+ return null;
251
+ })
252
+ );
253
+
254
+ // Apply all updates to the HTML content
255
+ updates.forEach(update => {
256
+ if (update) {
257
+ content = content.replace(update.tag, update.newTag);
235
258
  }
236
- }
259
+ });
237
260
 
238
- return html;
239
- } catch (error) {
240
- console.error(`${LOG_PREFIX} Failed to transform HTML: ${error}`);
241
- return html;
261
+ // Write the modified content back to the bundle
262
+ if (bundle[filename]) {
263
+ bundle[filename].source = content;
264
+ }
242
265
  }
266
+
267
+ // Clear the caches
268
+ htmlFiles.clear();
269
+ sriCache.clear();
243
270
  }
244
271
  };
245
272
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-sri4",
3
- "version": "1.8.5",
3
+ "version": "1.8.7",
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",