vite-plugin-sri4 1.7.0 → 1.8.1
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 +113 -57
- package/dist/index.js +113 -57
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -9,28 +9,21 @@ const DEFAULT_OPTIONS = {
|
|
|
9
9
|
algorithm: 'sha384',
|
|
10
10
|
bypassDomains: [],
|
|
11
11
|
crossorigin: 'anonymous',
|
|
12
|
-
debug: false
|
|
12
|
+
debug: false,
|
|
13
|
+
inlineScripts: false
|
|
13
14
|
};
|
|
14
15
|
|
|
15
|
-
/**
|
|
16
|
-
* Log debug messages if debug mode is enabled
|
|
17
|
-
* @param {string} message - Message to log
|
|
18
|
-
* @param {Object} options - Plugin options
|
|
19
|
-
*/
|
|
20
16
|
function log(message, options) {
|
|
21
17
|
if (options.debug) {
|
|
22
18
|
console.log(`${LOG_PREFIX} ${message}`);
|
|
23
19
|
}
|
|
24
20
|
}
|
|
25
21
|
|
|
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
|
-
*/
|
|
32
22
|
function computeSri(content, algorithm = 'sha384') {
|
|
33
23
|
try {
|
|
24
|
+
if (typeof content === 'string') {
|
|
25
|
+
content = Buffer.from(content, 'utf-8');
|
|
26
|
+
}
|
|
34
27
|
const hash = node_crypto.createHash(algorithm)
|
|
35
28
|
.update(content)
|
|
36
29
|
.digest('base64');
|
|
@@ -41,17 +34,10 @@ function computeSri(content, algorithm = 'sha384') {
|
|
|
41
34
|
}
|
|
42
35
|
}
|
|
43
36
|
|
|
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
|
-
*/
|
|
50
37
|
async function externalResourceIsCorsEnabled(url, options) {
|
|
51
38
|
try {
|
|
52
39
|
const response = await fetch(url, {
|
|
53
|
-
method: 'HEAD'
|
|
54
|
-
timeout: 5000
|
|
40
|
+
method: 'HEAD'
|
|
55
41
|
});
|
|
56
42
|
const acao = response.headers.get('access-control-allow-origin');
|
|
57
43
|
if (acao && (acao === '*' || acao.includes(options.domain || ''))) {
|
|
@@ -64,50 +50,60 @@ async function externalResourceIsCorsEnabled(url, options) {
|
|
|
64
50
|
}
|
|
65
51
|
}
|
|
66
52
|
|
|
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
|
-
*/
|
|
73
53
|
function isBypassDomain(url, bypassDomains = []) {
|
|
74
54
|
if (!bypassDomains.length) return false;
|
|
75
55
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
56
|
+
let parsedUrl;
|
|
57
|
+
if (url.startsWith('//')) {
|
|
58
|
+
parsedUrl = new URL(`http:${url}`);
|
|
59
|
+
} else if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
60
|
+
parsedUrl = new URL(url);
|
|
61
|
+
} else {
|
|
62
|
+
parsedUrl = new URL(url, 'http://dummy');
|
|
63
|
+
}
|
|
64
|
+
|
|
79
65
|
return bypassDomains.some(
|
|
80
66
|
(domain) => parsedUrl.hostname === domain ||
|
|
81
67
|
parsedUrl.hostname.endsWith(`.${domain}`)
|
|
82
68
|
);
|
|
83
69
|
} catch (e) {
|
|
70
|
+
console.error(`${LOG_PREFIX} Failed to parse URL: ${url}`, e);
|
|
84
71
|
return false;
|
|
85
72
|
}
|
|
86
73
|
}
|
|
87
74
|
|
|
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
|
-
*/
|
|
93
75
|
function hasCrossOriginAttr(tag) {
|
|
94
76
|
return /(?:^|\s)crossorigin(?:=["']?[^"'\s>]*["']?)?(?:\s|>|$)/i.test(tag);
|
|
95
77
|
}
|
|
96
78
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
79
|
+
function getAllPossiblePaths(url) {
|
|
80
|
+
const paths = [
|
|
81
|
+
url,
|
|
82
|
+
url.startsWith('/') ? url.slice(1) : url,
|
|
83
|
+
url.startsWith('/static/') ? url.slice(8) : url,
|
|
84
|
+
!url.startsWith('/static/') ? `static/${url}` : url,
|
|
85
|
+
!url.startsWith('/static/') ? `/static/${url}` : url,
|
|
86
|
+
url.replace(/^\/static\//, ''),
|
|
87
|
+
url.replace(/^static\//, '')
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
const withoutHash = url.replace(/-[a-zA-Z0-9]+\./, '.');
|
|
91
|
+
if (withoutHash !== url) {
|
|
92
|
+
paths.push(...getAllPossiblePaths(withoutHash));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return [...new Set(paths)];
|
|
96
|
+
}
|
|
97
|
+
|
|
105
98
|
async function processTag(tag, url, options, sriMap) {
|
|
106
99
|
if (tag.includes('integrity=')) {
|
|
107
100
|
log(`Skip tag with existing integrity attribute: ${tag}`, options);
|
|
108
101
|
return tag;
|
|
109
102
|
}
|
|
110
103
|
|
|
104
|
+
log(`Processing tag: ${tag}`, options);
|
|
105
|
+
log(`URL: ${url}`, options);
|
|
106
|
+
|
|
111
107
|
// Handle external resources
|
|
112
108
|
if (/^(https?:)?\/\//i.test(url)) {
|
|
113
109
|
if (isBypassDomain(url, options.bypassDomains)) {
|
|
@@ -129,7 +125,9 @@ async function processTag(tag, url, options, sriMap) {
|
|
|
129
125
|
log(`Computing SRI for external resource ${url}: ${hash}`, options);
|
|
130
126
|
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
131
127
|
const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
|
|
132
|
-
|
|
128
|
+
const newTag = tag.replace(/>$/, ` integrity="${hash}"${crossOriginAttr}>`);
|
|
129
|
+
log(`New tag: ${newTag}`, options);
|
|
130
|
+
return newTag;
|
|
133
131
|
}
|
|
134
132
|
} catch (error) {
|
|
135
133
|
log(`Failed to process external resource ${url}: ${error}`, options);
|
|
@@ -138,24 +136,56 @@ async function processTag(tag, url, options, sriMap) {
|
|
|
138
136
|
}
|
|
139
137
|
|
|
140
138
|
// Handle local resources
|
|
141
|
-
const
|
|
142
|
-
|
|
139
|
+
const possiblePaths = getAllPossiblePaths(url);
|
|
140
|
+
let integrity = null;
|
|
141
|
+
|
|
142
|
+
log(`Checking possible paths for ${url}:`, options);
|
|
143
|
+
for (const path of possiblePaths) {
|
|
144
|
+
log(`- Checking path: ${path}`, options);
|
|
145
|
+
if (sriMap.has(path)) {
|
|
146
|
+
integrity = sriMap.get(path);
|
|
147
|
+
log(`Found integrity for path ${path}: ${integrity}`, options);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
143
152
|
if (integrity) {
|
|
144
|
-
log(`Using precomputed SRI for ${
|
|
153
|
+
log(`Using precomputed SRI for ${url}: ${integrity}`, options);
|
|
145
154
|
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
146
155
|
const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
|
|
147
|
-
|
|
156
|
+
const newTag = tag.replace(/>$/, ` integrity="${integrity}"${crossOriginAttr}>`);
|
|
157
|
+
log(`New tag: ${newTag}`, options);
|
|
158
|
+
return newTag;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
log(`No SRI hash found for ${url}`, options);
|
|
162
|
+
log(`Available paths in sriMap: ${Array.from(sriMap.keys()).join(', ')}`, options);
|
|
163
|
+
return tag;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function processInlineScript(tag, options) {
|
|
167
|
+
if (tag.includes('integrity=')) {
|
|
168
|
+
log(`Skip inline script with existing integrity attribute: ${tag}`, options);
|
|
169
|
+
return tag;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const content = tag.match(/<script[^>]*>([\s\S]*?)<\/script>/)?.[1]?.trim();
|
|
173
|
+
if (!content) {
|
|
174
|
+
log(`Skip empty inline script: ${tag}`, options);
|
|
175
|
+
return tag;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const hash = computeSri(content, options.algorithm);
|
|
179
|
+
if (hash) {
|
|
180
|
+
log(`Computing SRI for inline script: ${hash}`, options);
|
|
181
|
+
const newTag = tag.replace('>', ` integrity="${hash}">`);
|
|
182
|
+
log(`New inline script tag: ${newTag}`, options);
|
|
183
|
+
return newTag;
|
|
148
184
|
}
|
|
149
185
|
|
|
150
|
-
log(`No SRI hash found for ${fileName}`, options);
|
|
151
186
|
return tag;
|
|
152
187
|
}
|
|
153
188
|
|
|
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
|
-
*/
|
|
159
189
|
function sri(userOptions = {}) {
|
|
160
190
|
const options = { ...DEFAULT_OPTIONS, ...userOptions };
|
|
161
191
|
const sriMap = new Map();
|
|
@@ -177,8 +207,10 @@ function sri(userOptions = {}) {
|
|
|
177
207
|
|
|
178
208
|
const hash = computeSri(code, options.algorithm);
|
|
179
209
|
if (hash) {
|
|
180
|
-
|
|
181
|
-
|
|
210
|
+
for (const path of getAllPossiblePaths(chunk.fileName)) {
|
|
211
|
+
sriMap.set(path, hash);
|
|
212
|
+
log(`Stored SRI for path ${path}: ${hash}`, options);
|
|
213
|
+
}
|
|
182
214
|
}
|
|
183
215
|
return null;
|
|
184
216
|
},
|
|
@@ -191,11 +223,18 @@ function sri(userOptions = {}) {
|
|
|
191
223
|
if (chunk.type === 'asset' && !sriMap.has(fileName)) {
|
|
192
224
|
const hash = computeSri(chunk.source, options.algorithm);
|
|
193
225
|
if (hash) {
|
|
194
|
-
|
|
195
|
-
|
|
226
|
+
for (const path of getAllPossiblePaths(fileName)) {
|
|
227
|
+
sriMap.set(path, hash);
|
|
228
|
+
log(`Computing SRI for asset ${path}: ${hash}`, options);
|
|
229
|
+
}
|
|
196
230
|
}
|
|
197
231
|
}
|
|
198
232
|
}
|
|
233
|
+
|
|
234
|
+
log('Final sriMap contents:', options);
|
|
235
|
+
for (const [key, value] of sriMap.entries()) {
|
|
236
|
+
log(`${key} => ${value}`, options);
|
|
237
|
+
}
|
|
199
238
|
},
|
|
200
239
|
|
|
201
240
|
async transformIndexHtml(html) {
|
|
@@ -205,18 +244,35 @@ function sri(userOptions = {}) {
|
|
|
205
244
|
}
|
|
206
245
|
|
|
207
246
|
try {
|
|
247
|
+
log('Starting HTML transformation', options);
|
|
248
|
+
log(`SRI Map size: ${sriMap.size}`, options);
|
|
249
|
+
|
|
208
250
|
// Process script tags
|
|
209
251
|
const scriptTags = html.match(/<script[^>]+src=["']([^"']+)["'][^>]*>/g) || [];
|
|
252
|
+
log(`Found ${scriptTags.length} script tags`, options);
|
|
210
253
|
for (const tag of scriptTags) {
|
|
211
254
|
const url = tag.match(/src=["']([^"']+)["']/)[1];
|
|
255
|
+
log(`Processing script: ${url}`, options);
|
|
212
256
|
const newTag = await processTag(tag, url, options, sriMap);
|
|
213
257
|
html = html.replace(tag, newTag);
|
|
214
258
|
}
|
|
215
259
|
|
|
260
|
+
// Process inline scripts if enabled
|
|
261
|
+
if (options.inlineScripts) {
|
|
262
|
+
const inlineScripts = html.match(/<script[^>]*>([^<]+)<\/script>/g) || [];
|
|
263
|
+
log(`Found ${inlineScripts.length} inline script tags`, options);
|
|
264
|
+
for (const tag of inlineScripts) {
|
|
265
|
+
const newTag = await processInlineScript(tag, options);
|
|
266
|
+
html = html.replace(tag, newTag);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
216
270
|
// Process link tags
|
|
217
271
|
const linkTags = html.match(/<link[^>]+href=["']([^"']+)["'][^>]*>/g) || [];
|
|
272
|
+
log(`Found ${linkTags.length} link tags`, options);
|
|
218
273
|
for (const tag of linkTags) {
|
|
219
274
|
const url = tag.match(/href=["']([^"']+)["']/)[1];
|
|
275
|
+
log(`Processing link: ${url}`, options);
|
|
220
276
|
const newTag = await processTag(tag, url, options, sriMap);
|
|
221
277
|
html = html.replace(tag, newTag);
|
|
222
278
|
}
|
package/dist/index.js
CHANGED
|
@@ -7,28 +7,21 @@ const DEFAULT_OPTIONS = {
|
|
|
7
7
|
algorithm: 'sha384',
|
|
8
8
|
bypassDomains: [],
|
|
9
9
|
crossorigin: 'anonymous',
|
|
10
|
-
debug: false
|
|
10
|
+
debug: false,
|
|
11
|
+
inlineScripts: false
|
|
11
12
|
};
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
-
* Log debug messages if debug mode is enabled
|
|
15
|
-
* @param {string} message - Message to log
|
|
16
|
-
* @param {Object} options - Plugin options
|
|
17
|
-
*/
|
|
18
14
|
function log(message, options) {
|
|
19
15
|
if (options.debug) {
|
|
20
16
|
console.log(`${LOG_PREFIX} ${message}`);
|
|
21
17
|
}
|
|
22
18
|
}
|
|
23
19
|
|
|
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
|
-
*/
|
|
30
20
|
function computeSri(content, algorithm = 'sha384') {
|
|
31
21
|
try {
|
|
22
|
+
if (typeof content === 'string') {
|
|
23
|
+
content = Buffer.from(content, 'utf-8');
|
|
24
|
+
}
|
|
32
25
|
const hash = createHash(algorithm)
|
|
33
26
|
.update(content)
|
|
34
27
|
.digest('base64');
|
|
@@ -39,17 +32,10 @@ function computeSri(content, algorithm = 'sha384') {
|
|
|
39
32
|
}
|
|
40
33
|
}
|
|
41
34
|
|
|
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
|
-
*/
|
|
48
35
|
async function externalResourceIsCorsEnabled(url, options) {
|
|
49
36
|
try {
|
|
50
37
|
const response = await fetch(url, {
|
|
51
|
-
method: 'HEAD'
|
|
52
|
-
timeout: 5000
|
|
38
|
+
method: 'HEAD'
|
|
53
39
|
});
|
|
54
40
|
const acao = response.headers.get('access-control-allow-origin');
|
|
55
41
|
if (acao && (acao === '*' || acao.includes(options.domain || ''))) {
|
|
@@ -62,50 +48,60 @@ async function externalResourceIsCorsEnabled(url, options) {
|
|
|
62
48
|
}
|
|
63
49
|
}
|
|
64
50
|
|
|
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
|
-
*/
|
|
71
51
|
function isBypassDomain(url, bypassDomains = []) {
|
|
72
52
|
if (!bypassDomains.length) return false;
|
|
73
53
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
54
|
+
let parsedUrl;
|
|
55
|
+
if (url.startsWith('//')) {
|
|
56
|
+
parsedUrl = new URL(`http:${url}`);
|
|
57
|
+
} else if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
58
|
+
parsedUrl = new URL(url);
|
|
59
|
+
} else {
|
|
60
|
+
parsedUrl = new URL(url, 'http://dummy');
|
|
61
|
+
}
|
|
62
|
+
|
|
77
63
|
return bypassDomains.some(
|
|
78
64
|
(domain) => parsedUrl.hostname === domain ||
|
|
79
65
|
parsedUrl.hostname.endsWith(`.${domain}`)
|
|
80
66
|
);
|
|
81
67
|
} catch (e) {
|
|
68
|
+
console.error(`${LOG_PREFIX} Failed to parse URL: ${url}`, e);
|
|
82
69
|
return false;
|
|
83
70
|
}
|
|
84
71
|
}
|
|
85
72
|
|
|
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
|
-
*/
|
|
91
73
|
function hasCrossOriginAttr(tag) {
|
|
92
74
|
return /(?:^|\s)crossorigin(?:=["']?[^"'\s>]*["']?)?(?:\s|>|$)/i.test(tag);
|
|
93
75
|
}
|
|
94
76
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
77
|
+
function getAllPossiblePaths(url) {
|
|
78
|
+
const paths = [
|
|
79
|
+
url,
|
|
80
|
+
url.startsWith('/') ? url.slice(1) : url,
|
|
81
|
+
url.startsWith('/static/') ? url.slice(8) : url,
|
|
82
|
+
!url.startsWith('/static/') ? `static/${url}` : url,
|
|
83
|
+
!url.startsWith('/static/') ? `/static/${url}` : url,
|
|
84
|
+
url.replace(/^\/static\//, ''),
|
|
85
|
+
url.replace(/^static\//, '')
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const withoutHash = url.replace(/-[a-zA-Z0-9]+\./, '.');
|
|
89
|
+
if (withoutHash !== url) {
|
|
90
|
+
paths.push(...getAllPossiblePaths(withoutHash));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return [...new Set(paths)];
|
|
94
|
+
}
|
|
95
|
+
|
|
103
96
|
async function processTag(tag, url, options, sriMap) {
|
|
104
97
|
if (tag.includes('integrity=')) {
|
|
105
98
|
log(`Skip tag with existing integrity attribute: ${tag}`, options);
|
|
106
99
|
return tag;
|
|
107
100
|
}
|
|
108
101
|
|
|
102
|
+
log(`Processing tag: ${tag}`, options);
|
|
103
|
+
log(`URL: ${url}`, options);
|
|
104
|
+
|
|
109
105
|
// Handle external resources
|
|
110
106
|
if (/^(https?:)?\/\//i.test(url)) {
|
|
111
107
|
if (isBypassDomain(url, options.bypassDomains)) {
|
|
@@ -127,7 +123,9 @@ async function processTag(tag, url, options, sriMap) {
|
|
|
127
123
|
log(`Computing SRI for external resource ${url}: ${hash}`, options);
|
|
128
124
|
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
129
125
|
const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
|
|
130
|
-
|
|
126
|
+
const newTag = tag.replace(/>$/, ` integrity="${hash}"${crossOriginAttr}>`);
|
|
127
|
+
log(`New tag: ${newTag}`, options);
|
|
128
|
+
return newTag;
|
|
131
129
|
}
|
|
132
130
|
} catch (error) {
|
|
133
131
|
log(`Failed to process external resource ${url}: ${error}`, options);
|
|
@@ -136,24 +134,56 @@ async function processTag(tag, url, options, sriMap) {
|
|
|
136
134
|
}
|
|
137
135
|
|
|
138
136
|
// Handle local resources
|
|
139
|
-
const
|
|
140
|
-
|
|
137
|
+
const possiblePaths = getAllPossiblePaths(url);
|
|
138
|
+
let integrity = null;
|
|
139
|
+
|
|
140
|
+
log(`Checking possible paths for ${url}:`, options);
|
|
141
|
+
for (const path of possiblePaths) {
|
|
142
|
+
log(`- Checking path: ${path}`, options);
|
|
143
|
+
if (sriMap.has(path)) {
|
|
144
|
+
integrity = sriMap.get(path);
|
|
145
|
+
log(`Found integrity for path ${path}: ${integrity}`, options);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
141
150
|
if (integrity) {
|
|
142
|
-
log(`Using precomputed SRI for ${
|
|
151
|
+
log(`Using precomputed SRI for ${url}: ${integrity}`, options);
|
|
143
152
|
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
144
153
|
const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
|
|
145
|
-
|
|
154
|
+
const newTag = tag.replace(/>$/, ` integrity="${integrity}"${crossOriginAttr}>`);
|
|
155
|
+
log(`New tag: ${newTag}`, options);
|
|
156
|
+
return newTag;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
log(`No SRI hash found for ${url}`, options);
|
|
160
|
+
log(`Available paths in sriMap: ${Array.from(sriMap.keys()).join(', ')}`, options);
|
|
161
|
+
return tag;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function processInlineScript(tag, options) {
|
|
165
|
+
if (tag.includes('integrity=')) {
|
|
166
|
+
log(`Skip inline script with existing integrity attribute: ${tag}`, options);
|
|
167
|
+
return tag;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const content = tag.match(/<script[^>]*>([\s\S]*?)<\/script>/)?.[1]?.trim();
|
|
171
|
+
if (!content) {
|
|
172
|
+
log(`Skip empty inline script: ${tag}`, options);
|
|
173
|
+
return tag;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const hash = computeSri(content, options.algorithm);
|
|
177
|
+
if (hash) {
|
|
178
|
+
log(`Computing SRI for inline script: ${hash}`, options);
|
|
179
|
+
const newTag = tag.replace('>', ` integrity="${hash}">`);
|
|
180
|
+
log(`New inline script tag: ${newTag}`, options);
|
|
181
|
+
return newTag;
|
|
146
182
|
}
|
|
147
183
|
|
|
148
|
-
log(`No SRI hash found for ${fileName}`, options);
|
|
149
184
|
return tag;
|
|
150
185
|
}
|
|
151
186
|
|
|
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
|
-
*/
|
|
157
187
|
function sri(userOptions = {}) {
|
|
158
188
|
const options = { ...DEFAULT_OPTIONS, ...userOptions };
|
|
159
189
|
const sriMap = new Map();
|
|
@@ -175,8 +205,10 @@ function sri(userOptions = {}) {
|
|
|
175
205
|
|
|
176
206
|
const hash = computeSri(code, options.algorithm);
|
|
177
207
|
if (hash) {
|
|
178
|
-
|
|
179
|
-
|
|
208
|
+
for (const path of getAllPossiblePaths(chunk.fileName)) {
|
|
209
|
+
sriMap.set(path, hash);
|
|
210
|
+
log(`Stored SRI for path ${path}: ${hash}`, options);
|
|
211
|
+
}
|
|
180
212
|
}
|
|
181
213
|
return null;
|
|
182
214
|
},
|
|
@@ -189,11 +221,18 @@ function sri(userOptions = {}) {
|
|
|
189
221
|
if (chunk.type === 'asset' && !sriMap.has(fileName)) {
|
|
190
222
|
const hash = computeSri(chunk.source, options.algorithm);
|
|
191
223
|
if (hash) {
|
|
192
|
-
|
|
193
|
-
|
|
224
|
+
for (const path of getAllPossiblePaths(fileName)) {
|
|
225
|
+
sriMap.set(path, hash);
|
|
226
|
+
log(`Computing SRI for asset ${path}: ${hash}`, options);
|
|
227
|
+
}
|
|
194
228
|
}
|
|
195
229
|
}
|
|
196
230
|
}
|
|
231
|
+
|
|
232
|
+
log('Final sriMap contents:', options);
|
|
233
|
+
for (const [key, value] of sriMap.entries()) {
|
|
234
|
+
log(`${key} => ${value}`, options);
|
|
235
|
+
}
|
|
197
236
|
},
|
|
198
237
|
|
|
199
238
|
async transformIndexHtml(html) {
|
|
@@ -203,18 +242,35 @@ function sri(userOptions = {}) {
|
|
|
203
242
|
}
|
|
204
243
|
|
|
205
244
|
try {
|
|
245
|
+
log('Starting HTML transformation', options);
|
|
246
|
+
log(`SRI Map size: ${sriMap.size}`, options);
|
|
247
|
+
|
|
206
248
|
// Process script tags
|
|
207
249
|
const scriptTags = html.match(/<script[^>]+src=["']([^"']+)["'][^>]*>/g) || [];
|
|
250
|
+
log(`Found ${scriptTags.length} script tags`, options);
|
|
208
251
|
for (const tag of scriptTags) {
|
|
209
252
|
const url = tag.match(/src=["']([^"']+)["']/)[1];
|
|
253
|
+
log(`Processing script: ${url}`, options);
|
|
210
254
|
const newTag = await processTag(tag, url, options, sriMap);
|
|
211
255
|
html = html.replace(tag, newTag);
|
|
212
256
|
}
|
|
213
257
|
|
|
258
|
+
// Process inline scripts if enabled
|
|
259
|
+
if (options.inlineScripts) {
|
|
260
|
+
const inlineScripts = html.match(/<script[^>]*>([^<]+)<\/script>/g) || [];
|
|
261
|
+
log(`Found ${inlineScripts.length} inline script tags`, options);
|
|
262
|
+
for (const tag of inlineScripts) {
|
|
263
|
+
const newTag = await processInlineScript(tag, options);
|
|
264
|
+
html = html.replace(tag, newTag);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
214
268
|
// Process link tags
|
|
215
269
|
const linkTags = html.match(/<link[^>]+href=["']([^"']+)["'][^>]*>/g) || [];
|
|
270
|
+
log(`Found ${linkTags.length} link tags`, options);
|
|
216
271
|
for (const tag of linkTags) {
|
|
217
272
|
const url = tag.match(/href=["']([^"']+)["']/)[1];
|
|
273
|
+
log(`Processing link: ${url}`, options);
|
|
218
274
|
const newTag = await processTag(tag, url, options, sriMap);
|
|
219
275
|
html = html.replace(tag, newTag);
|
|
220
276
|
}
|