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.
- package/README.md +144 -13
- package/dist/index.cjs +154 -127
- package/dist/index.js +154 -127
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,27 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
[](https://codecov.io/gh/7a6163/vite-plugin-sri4)
|
|
5
|
+

|
|
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
|
-
|
|
61
|
+
### Example HTML Output
|
|
46
62
|
|
|
47
|
-
|
|
48
|
-
|
|
63
|
+
Input:
|
|
64
|
+
```html
|
|
65
|
+
<script src="app.js"></script>
|
|
66
|
+
<link rel="stylesheet" href="style.css">
|
|
49
67
|
```
|
|
50
68
|
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
}
|