vite-plugin-sri4 1.5.0 → 1.7.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/dist/index.cjs +140 -91
- package/dist/index.js +140 -91
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
var node_crypto = require('node:crypto');
|
|
4
4
|
var fetch = require('cross-fetch');
|
|
5
5
|
|
|
6
|
+
const LOG_PREFIX = '[vite-plugin-sri4]';
|
|
7
|
+
|
|
6
8
|
const DEFAULT_OPTIONS = {
|
|
7
9
|
algorithm: 'sha384',
|
|
8
10
|
bypassDomains: [],
|
|
@@ -10,12 +12,23 @@ const DEFAULT_OPTIONS = {
|
|
|
10
12
|
debug: false
|
|
11
13
|
};
|
|
12
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Log debug messages if debug mode is enabled
|
|
17
|
+
* @param {string} message - Message to log
|
|
18
|
+
* @param {Object} options - Plugin options
|
|
19
|
+
*/
|
|
13
20
|
function log(message, options) {
|
|
14
21
|
if (options.debug) {
|
|
15
|
-
console.log(
|
|
22
|
+
console.log(`${LOG_PREFIX} ${message}`);
|
|
16
23
|
}
|
|
17
24
|
}
|
|
18
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Compute SRI hash for given content
|
|
28
|
+
* @param {string|Buffer} content - Content to hash
|
|
29
|
+
* @param {string} algorithm - Hash algorithm to use
|
|
30
|
+
* @returns {string|null} SRI hash string or null if failed
|
|
31
|
+
*/
|
|
19
32
|
function computeSri(content, algorithm = 'sha384') {
|
|
20
33
|
try {
|
|
21
34
|
const hash = node_crypto.createHash(algorithm)
|
|
@@ -23,11 +36,17 @@ function computeSri(content, algorithm = 'sha384') {
|
|
|
23
36
|
.digest('base64');
|
|
24
37
|
return `${algorithm}-${hash}`;
|
|
25
38
|
} catch (error) {
|
|
26
|
-
console.error(
|
|
39
|
+
console.error(`${LOG_PREFIX} Failed to compute SRI hash: ${error}`);
|
|
27
40
|
return null;
|
|
28
41
|
}
|
|
29
42
|
}
|
|
30
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Check if external resource has CORS enabled
|
|
46
|
+
* @param {string} url - URL to check
|
|
47
|
+
* @param {Object} options - Plugin options
|
|
48
|
+
* @returns {Promise<boolean>} Whether CORS is enabled
|
|
49
|
+
*/
|
|
31
50
|
async function externalResourceIsCorsEnabled(url, options) {
|
|
32
51
|
try {
|
|
33
52
|
const response = await fetch(url, {
|
|
@@ -45,38 +64,18 @@ async function externalResourceIsCorsEnabled(url, options) {
|
|
|
45
64
|
}
|
|
46
65
|
}
|
|
47
66
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
promises.push(asyncFn(...args));
|
|
55
|
-
return '';
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const results = await Promise.all(promises);
|
|
59
|
-
|
|
60
|
-
let lastIndex = 0;
|
|
61
|
-
let result = '';
|
|
62
|
-
|
|
63
|
-
for (let i = 0; i < matches.length; i++) {
|
|
64
|
-
const match = matches[i][0];
|
|
65
|
-
const index = str.indexOf(match, lastIndex);
|
|
66
|
-
result += str.slice(lastIndex, index) + results[i];
|
|
67
|
-
lastIndex = index + match.length;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
result += str.slice(lastIndex);
|
|
71
|
-
return result;
|
|
72
|
-
}
|
|
73
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Check if URL belongs to bypass domains
|
|
69
|
+
* @param {string} url - URL to check
|
|
70
|
+
* @param {string[]} bypassDomains - List of domains to bypass
|
|
71
|
+
* @returns {boolean} Whether URL belongs to bypass domains
|
|
72
|
+
*/
|
|
74
73
|
function isBypassDomain(url, bypassDomains = []) {
|
|
75
74
|
if (!bypassDomains.length) return false;
|
|
76
75
|
try {
|
|
77
76
|
const parsedUrl = url.startsWith('//')
|
|
78
|
-
? new URL(url
|
|
79
|
-
: new URL(url);
|
|
77
|
+
? new URL(`http:${url}`)
|
|
78
|
+
: new URL(url, 'http://dummy');
|
|
80
79
|
return bypassDomains.some(
|
|
81
80
|
(domain) => parsedUrl.hostname === domain ||
|
|
82
81
|
parsedUrl.hostname.endsWith(`.${domain}`)
|
|
@@ -86,97 +85,147 @@ function isBypassDomain(url, bypassDomains = []) {
|
|
|
86
85
|
}
|
|
87
86
|
}
|
|
88
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Check if tag already has crossorigin attribute
|
|
90
|
+
* @param {string} tag - HTML tag to check
|
|
91
|
+
* @returns {boolean} Whether tag has crossorigin attribute
|
|
92
|
+
*/
|
|
89
93
|
function hasCrossOriginAttr(tag) {
|
|
90
94
|
return /(?:^|\s)crossorigin(?:=["']?[^"'\s>]*["']?)?(?:\s|>|$)/i.test(tag);
|
|
91
95
|
}
|
|
92
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Process individual HTML tags and add SRI attributes
|
|
99
|
+
* @param {string} tag - HTML tag to process
|
|
100
|
+
* @param {string} url - Resource URL
|
|
101
|
+
* @param {Object} options - Plugin options
|
|
102
|
+
* @param {Map} sriMap - Map of file names to SRI hashes
|
|
103
|
+
* @returns {Promise<string>} Processed HTML tag
|
|
104
|
+
*/
|
|
105
|
+
async function processTag(tag, url, options, sriMap) {
|
|
106
|
+
if (tag.includes('integrity=')) {
|
|
107
|
+
log(`Skip tag with existing integrity attribute: ${tag}`, options);
|
|
108
|
+
return tag;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Handle external resources
|
|
112
|
+
if (/^(https?:)?\/\//i.test(url)) {
|
|
113
|
+
if (isBypassDomain(url, options.bypassDomains)) {
|
|
114
|
+
log(`Skip SRI for bypass domain: ${url}`, options);
|
|
115
|
+
return tag;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const corsOk = await externalResourceIsCorsEnabled(url, options);
|
|
119
|
+
if (!corsOk) {
|
|
120
|
+
log(`External resource ${url} does not support CORS`, options);
|
|
121
|
+
return tag;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const response = await fetch(url);
|
|
126
|
+
const content = await response.arrayBuffer();
|
|
127
|
+
const hash = computeSri(Buffer.from(content), options.algorithm);
|
|
128
|
+
if (hash) {
|
|
129
|
+
log(`Computing SRI for external resource ${url}: ${hash}`, options);
|
|
130
|
+
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
131
|
+
const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
|
|
132
|
+
return tag.replace(/>$/, ` integrity="${hash}"${crossOriginAttr}>`);
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
log(`Failed to process external resource ${url}: ${error}`, options);
|
|
136
|
+
}
|
|
137
|
+
return tag;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Handle local resources
|
|
141
|
+
const fileName = url.startsWith('/') ? url.slice(1) : url;
|
|
142
|
+
const integrity = sriMap.get(fileName);
|
|
143
|
+
if (integrity) {
|
|
144
|
+
log(`Using precomputed SRI for ${fileName}: ${integrity}`, options);
|
|
145
|
+
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
146
|
+
const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
|
|
147
|
+
return tag.replace(/>$/, ` integrity="${integrity}"${crossOriginAttr}>`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
log(`No SRI hash found for ${fileName}`, options);
|
|
151
|
+
return tag;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Vite plugin for adding Subresource Integrity (SRI) hashes to assets
|
|
156
|
+
* @param {Object} userOptions - Plugin options
|
|
157
|
+
* @returns {import('vite').Plugin} Vite plugin object
|
|
158
|
+
*/
|
|
93
159
|
function sri(userOptions = {}) {
|
|
94
160
|
const options = { ...DEFAULT_OPTIONS, ...userOptions };
|
|
95
161
|
const sriMap = new Map();
|
|
162
|
+
let isBuild = false;
|
|
96
163
|
|
|
97
164
|
return {
|
|
98
165
|
name: 'vite-plugin-sri4',
|
|
99
|
-
enforce: 'post',
|
|
100
166
|
apply: 'build',
|
|
167
|
+
enforce: 'post',
|
|
101
168
|
|
|
102
169
|
configResolved(config) {
|
|
103
170
|
options.domain = config.server?.host || '';
|
|
171
|
+
isBuild = config.command === 'build';
|
|
172
|
+
log('Plugin configured in ' + (isBuild ? 'build' : 'dev') + ' mode', options);
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
async renderChunk(code, chunk) {
|
|
176
|
+
if (!isBuild) return null;
|
|
177
|
+
|
|
178
|
+
const hash = computeSri(code, options.algorithm);
|
|
179
|
+
if (hash) {
|
|
180
|
+
sriMap.set(chunk.fileName, hash);
|
|
181
|
+
log(`Computing SRI for chunk ${chunk.fileName}: ${hash}`, options);
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
104
184
|
},
|
|
105
185
|
|
|
106
186
|
async generateBundle(_, bundle) {
|
|
187
|
+
if (!isBuild) return;
|
|
188
|
+
|
|
107
189
|
for (const fileName in bundle) {
|
|
108
190
|
const chunk = bundle[fileName];
|
|
109
|
-
if (chunk.type === '
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
sriMap.set(fileName, hash);
|
|
115
|
-
log(`Computed SRI for ${fileName}: ${hash}`, options);
|
|
116
|
-
}
|
|
191
|
+
if (chunk.type === 'asset' && !sriMap.has(fileName)) {
|
|
192
|
+
const hash = computeSri(chunk.source, options.algorithm);
|
|
193
|
+
if (hash) {
|
|
194
|
+
sriMap.set(fileName, hash);
|
|
195
|
+
log(`Computing SRI for asset ${fileName}: ${hash}`, options);
|
|
117
196
|
}
|
|
118
197
|
}
|
|
119
198
|
}
|
|
120
199
|
},
|
|
121
200
|
|
|
122
201
|
async transformIndexHtml(html) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const actualSrc = src || moduleSrc;
|
|
128
|
-
|
|
129
|
-
if (hasIntegrity(tag)) {
|
|
130
|
-
return tag;
|
|
131
|
-
}
|
|
202
|
+
if (!isBuild || !html) {
|
|
203
|
+
log('Skipping HTML transform in dev mode or empty HTML', options);
|
|
204
|
+
return html;
|
|
205
|
+
}
|
|
132
206
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
207
|
+
try {
|
|
208
|
+
// Process script tags
|
|
209
|
+
const scriptTags = html.match(/<script[^>]+src=["']([^"']+)["'][^>]*>/g) || [];
|
|
210
|
+
for (const tag of scriptTags) {
|
|
211
|
+
const url = tag.match(/src=["']([^"']+)["']/)[1];
|
|
212
|
+
const newTag = await processTag(tag, url, options, sriMap);
|
|
213
|
+
html = html.replace(tag, newTag);
|
|
137
214
|
}
|
|
138
215
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const fileName = actualSrc.startsWith('/') ? actualSrc.slice(1) : actualSrc;
|
|
148
|
-
const integrity = sriMap.get(fileName);
|
|
149
|
-
|
|
150
|
-
if (integrity) {
|
|
151
|
-
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
152
|
-
if (!hasCrossOrigin) {
|
|
153
|
-
return tag.replace(
|
|
154
|
-
/>$/,
|
|
155
|
-
` integrity="${integrity}" crossorigin="${options.crossorigin}">`
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
return tag.replace(
|
|
159
|
-
/>$/,
|
|
160
|
-
` integrity="${integrity}">`
|
|
161
|
-
);
|
|
216
|
+
// Process link tags
|
|
217
|
+
const linkTags = html.match(/<link[^>]+href=["']([^"']+)["'][^>]*>/g) || [];
|
|
218
|
+
for (const tag of linkTags) {
|
|
219
|
+
const url = tag.match(/href=["']([^"']+)["']/)[1];
|
|
220
|
+
const newTag = await processTag(tag, url, options, sriMap);
|
|
221
|
+
html = html.replace(tag, newTag);
|
|
162
222
|
}
|
|
163
223
|
|
|
164
|
-
return
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
/(<script[^>]+(?:src="([^"]+)"[^>]*|type="module"[^>]*src="([^"]+)"[^>]*)>)/g,
|
|
170
|
-
processTag
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
html = await replaceAsync(
|
|
174
|
-
html,
|
|
175
|
-
/(<link[^>]+href="([^"]+)"[^>]*>)/g,
|
|
176
|
-
processTag
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
return html;
|
|
224
|
+
return html;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error(`${LOG_PREFIX} Failed to transform HTML: ${error}`);
|
|
227
|
+
return html;
|
|
228
|
+
}
|
|
180
229
|
}
|
|
181
230
|
};
|
|
182
231
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import fetch from 'cross-fetch';
|
|
3
3
|
|
|
4
|
+
const LOG_PREFIX = '[vite-plugin-sri4]';
|
|
5
|
+
|
|
4
6
|
const DEFAULT_OPTIONS = {
|
|
5
7
|
algorithm: 'sha384',
|
|
6
8
|
bypassDomains: [],
|
|
@@ -8,12 +10,23 @@ const DEFAULT_OPTIONS = {
|
|
|
8
10
|
debug: false
|
|
9
11
|
};
|
|
10
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Log debug messages if debug mode is enabled
|
|
15
|
+
* @param {string} message - Message to log
|
|
16
|
+
* @param {Object} options - Plugin options
|
|
17
|
+
*/
|
|
11
18
|
function log(message, options) {
|
|
12
19
|
if (options.debug) {
|
|
13
|
-
console.log(
|
|
20
|
+
console.log(`${LOG_PREFIX} ${message}`);
|
|
14
21
|
}
|
|
15
22
|
}
|
|
16
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Compute SRI hash for given content
|
|
26
|
+
* @param {string|Buffer} content - Content to hash
|
|
27
|
+
* @param {string} algorithm - Hash algorithm to use
|
|
28
|
+
* @returns {string|null} SRI hash string or null if failed
|
|
29
|
+
*/
|
|
17
30
|
function computeSri(content, algorithm = 'sha384') {
|
|
18
31
|
try {
|
|
19
32
|
const hash = createHash(algorithm)
|
|
@@ -21,11 +34,17 @@ function computeSri(content, algorithm = 'sha384') {
|
|
|
21
34
|
.digest('base64');
|
|
22
35
|
return `${algorithm}-${hash}`;
|
|
23
36
|
} catch (error) {
|
|
24
|
-
console.error(
|
|
37
|
+
console.error(`${LOG_PREFIX} Failed to compute SRI hash: ${error}`);
|
|
25
38
|
return null;
|
|
26
39
|
}
|
|
27
40
|
}
|
|
28
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Check if external resource has CORS enabled
|
|
44
|
+
* @param {string} url - URL to check
|
|
45
|
+
* @param {Object} options - Plugin options
|
|
46
|
+
* @returns {Promise<boolean>} Whether CORS is enabled
|
|
47
|
+
*/
|
|
29
48
|
async function externalResourceIsCorsEnabled(url, options) {
|
|
30
49
|
try {
|
|
31
50
|
const response = await fetch(url, {
|
|
@@ -43,38 +62,18 @@ async function externalResourceIsCorsEnabled(url, options) {
|
|
|
43
62
|
}
|
|
44
63
|
}
|
|
45
64
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
promises.push(asyncFn(...args));
|
|
53
|
-
return '';
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const results = await Promise.all(promises);
|
|
57
|
-
|
|
58
|
-
let lastIndex = 0;
|
|
59
|
-
let result = '';
|
|
60
|
-
|
|
61
|
-
for (let i = 0; i < matches.length; i++) {
|
|
62
|
-
const match = matches[i][0];
|
|
63
|
-
const index = str.indexOf(match, lastIndex);
|
|
64
|
-
result += str.slice(lastIndex, index) + results[i];
|
|
65
|
-
lastIndex = index + match.length;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
result += str.slice(lastIndex);
|
|
69
|
-
return result;
|
|
70
|
-
}
|
|
71
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Check if URL belongs to bypass domains
|
|
67
|
+
* @param {string} url - URL to check
|
|
68
|
+
* @param {string[]} bypassDomains - List of domains to bypass
|
|
69
|
+
* @returns {boolean} Whether URL belongs to bypass domains
|
|
70
|
+
*/
|
|
72
71
|
function isBypassDomain(url, bypassDomains = []) {
|
|
73
72
|
if (!bypassDomains.length) return false;
|
|
74
73
|
try {
|
|
75
74
|
const parsedUrl = url.startsWith('//')
|
|
76
|
-
? new URL(url
|
|
77
|
-
: new URL(url);
|
|
75
|
+
? new URL(`http:${url}`)
|
|
76
|
+
: new URL(url, 'http://dummy');
|
|
78
77
|
return bypassDomains.some(
|
|
79
78
|
(domain) => parsedUrl.hostname === domain ||
|
|
80
79
|
parsedUrl.hostname.endsWith(`.${domain}`)
|
|
@@ -84,97 +83,147 @@ function isBypassDomain(url, bypassDomains = []) {
|
|
|
84
83
|
}
|
|
85
84
|
}
|
|
86
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Check if tag already has crossorigin attribute
|
|
88
|
+
* @param {string} tag - HTML tag to check
|
|
89
|
+
* @returns {boolean} Whether tag has crossorigin attribute
|
|
90
|
+
*/
|
|
87
91
|
function hasCrossOriginAttr(tag) {
|
|
88
92
|
return /(?:^|\s)crossorigin(?:=["']?[^"'\s>]*["']?)?(?:\s|>|$)/i.test(tag);
|
|
89
93
|
}
|
|
90
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Process individual HTML tags and add SRI attributes
|
|
97
|
+
* @param {string} tag - HTML tag to process
|
|
98
|
+
* @param {string} url - Resource URL
|
|
99
|
+
* @param {Object} options - Plugin options
|
|
100
|
+
* @param {Map} sriMap - Map of file names to SRI hashes
|
|
101
|
+
* @returns {Promise<string>} Processed HTML tag
|
|
102
|
+
*/
|
|
103
|
+
async function processTag(tag, url, options, sriMap) {
|
|
104
|
+
if (tag.includes('integrity=')) {
|
|
105
|
+
log(`Skip tag with existing integrity attribute: ${tag}`, options);
|
|
106
|
+
return tag;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Handle external resources
|
|
110
|
+
if (/^(https?:)?\/\//i.test(url)) {
|
|
111
|
+
if (isBypassDomain(url, options.bypassDomains)) {
|
|
112
|
+
log(`Skip SRI for bypass domain: ${url}`, options);
|
|
113
|
+
return tag;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const corsOk = await externalResourceIsCorsEnabled(url, options);
|
|
117
|
+
if (!corsOk) {
|
|
118
|
+
log(`External resource ${url} does not support CORS`, options);
|
|
119
|
+
return tag;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const response = await fetch(url);
|
|
124
|
+
const content = await response.arrayBuffer();
|
|
125
|
+
const hash = computeSri(Buffer.from(content), options.algorithm);
|
|
126
|
+
if (hash) {
|
|
127
|
+
log(`Computing SRI for external resource ${url}: ${hash}`, options);
|
|
128
|
+
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
129
|
+
const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
|
|
130
|
+
return tag.replace(/>$/, ` integrity="${hash}"${crossOriginAttr}>`);
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
log(`Failed to process external resource ${url}: ${error}`, options);
|
|
134
|
+
}
|
|
135
|
+
return tag;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Handle local resources
|
|
139
|
+
const fileName = url.startsWith('/') ? url.slice(1) : url;
|
|
140
|
+
const integrity = sriMap.get(fileName);
|
|
141
|
+
if (integrity) {
|
|
142
|
+
log(`Using precomputed SRI for ${fileName}: ${integrity}`, options);
|
|
143
|
+
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
144
|
+
const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
|
|
145
|
+
return tag.replace(/>$/, ` integrity="${integrity}"${crossOriginAttr}>`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
log(`No SRI hash found for ${fileName}`, options);
|
|
149
|
+
return tag;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Vite plugin for adding Subresource Integrity (SRI) hashes to assets
|
|
154
|
+
* @param {Object} userOptions - Plugin options
|
|
155
|
+
* @returns {import('vite').Plugin} Vite plugin object
|
|
156
|
+
*/
|
|
91
157
|
function sri(userOptions = {}) {
|
|
92
158
|
const options = { ...DEFAULT_OPTIONS, ...userOptions };
|
|
93
159
|
const sriMap = new Map();
|
|
160
|
+
let isBuild = false;
|
|
94
161
|
|
|
95
162
|
return {
|
|
96
163
|
name: 'vite-plugin-sri4',
|
|
97
|
-
enforce: 'post',
|
|
98
164
|
apply: 'build',
|
|
165
|
+
enforce: 'post',
|
|
99
166
|
|
|
100
167
|
configResolved(config) {
|
|
101
168
|
options.domain = config.server?.host || '';
|
|
169
|
+
isBuild = config.command === 'build';
|
|
170
|
+
log('Plugin configured in ' + (isBuild ? 'build' : 'dev') + ' mode', options);
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
async renderChunk(code, chunk) {
|
|
174
|
+
if (!isBuild) return null;
|
|
175
|
+
|
|
176
|
+
const hash = computeSri(code, options.algorithm);
|
|
177
|
+
if (hash) {
|
|
178
|
+
sriMap.set(chunk.fileName, hash);
|
|
179
|
+
log(`Computing SRI for chunk ${chunk.fileName}: ${hash}`, options);
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
102
182
|
},
|
|
103
183
|
|
|
104
184
|
async generateBundle(_, bundle) {
|
|
185
|
+
if (!isBuild) return;
|
|
186
|
+
|
|
105
187
|
for (const fileName in bundle) {
|
|
106
188
|
const chunk = bundle[fileName];
|
|
107
|
-
if (chunk.type === '
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
sriMap.set(fileName, hash);
|
|
113
|
-
log(`Computed SRI for ${fileName}: ${hash}`, options);
|
|
114
|
-
}
|
|
189
|
+
if (chunk.type === 'asset' && !sriMap.has(fileName)) {
|
|
190
|
+
const hash = computeSri(chunk.source, options.algorithm);
|
|
191
|
+
if (hash) {
|
|
192
|
+
sriMap.set(fileName, hash);
|
|
193
|
+
log(`Computing SRI for asset ${fileName}: ${hash}`, options);
|
|
115
194
|
}
|
|
116
195
|
}
|
|
117
196
|
}
|
|
118
197
|
},
|
|
119
198
|
|
|
120
199
|
async transformIndexHtml(html) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const actualSrc = src || moduleSrc;
|
|
126
|
-
|
|
127
|
-
if (hasIntegrity(tag)) {
|
|
128
|
-
return tag;
|
|
129
|
-
}
|
|
200
|
+
if (!isBuild || !html) {
|
|
201
|
+
log('Skipping HTML transform in dev mode or empty HTML', options);
|
|
202
|
+
return html;
|
|
203
|
+
}
|
|
130
204
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
205
|
+
try {
|
|
206
|
+
// Process script tags
|
|
207
|
+
const scriptTags = html.match(/<script[^>]+src=["']([^"']+)["'][^>]*>/g) || [];
|
|
208
|
+
for (const tag of scriptTags) {
|
|
209
|
+
const url = tag.match(/src=["']([^"']+)["']/)[1];
|
|
210
|
+
const newTag = await processTag(tag, url, options, sriMap);
|
|
211
|
+
html = html.replace(tag, newTag);
|
|
135
212
|
}
|
|
136
213
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const fileName = actualSrc.startsWith('/') ? actualSrc.slice(1) : actualSrc;
|
|
146
|
-
const integrity = sriMap.get(fileName);
|
|
147
|
-
|
|
148
|
-
if (integrity) {
|
|
149
|
-
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
150
|
-
if (!hasCrossOrigin) {
|
|
151
|
-
return tag.replace(
|
|
152
|
-
/>$/,
|
|
153
|
-
` integrity="${integrity}" crossorigin="${options.crossorigin}">`
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
return tag.replace(
|
|
157
|
-
/>$/,
|
|
158
|
-
` integrity="${integrity}">`
|
|
159
|
-
);
|
|
214
|
+
// Process link tags
|
|
215
|
+
const linkTags = html.match(/<link[^>]+href=["']([^"']+)["'][^>]*>/g) || [];
|
|
216
|
+
for (const tag of linkTags) {
|
|
217
|
+
const url = tag.match(/href=["']([^"']+)["']/)[1];
|
|
218
|
+
const newTag = await processTag(tag, url, options, sriMap);
|
|
219
|
+
html = html.replace(tag, newTag);
|
|
160
220
|
}
|
|
161
221
|
|
|
162
|
-
return
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
/(<script[^>]+(?:src="([^"]+)"[^>]*|type="module"[^>]*src="([^"]+)"[^>]*)>)/g,
|
|
168
|
-
processTag
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
html = await replaceAsync(
|
|
172
|
-
html,
|
|
173
|
-
/(<link[^>]+href="([^"]+)"[^>]*>)/g,
|
|
174
|
-
processTag
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
return html;
|
|
222
|
+
return html;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error(`${LOG_PREFIX} Failed to transform HTML: ${error}`);
|
|
225
|
+
return html;
|
|
226
|
+
}
|
|
178
227
|
}
|
|
179
228
|
};
|
|
180
229
|
}
|