vite-plugin-sri4 1.6.0 → 1.8.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 +153 -91
- package/dist/index.js +153 -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: [],
|
|
@@ -12,7 +14,7 @@ const DEFAULT_OPTIONS = {
|
|
|
12
14
|
|
|
13
15
|
function log(message, options) {
|
|
14
16
|
if (options.debug) {
|
|
15
|
-
console.log(
|
|
17
|
+
console.log(`${LOG_PREFIX} ${message}`);
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -23,7 +25,7 @@ function computeSri(content, algorithm = 'sha384') {
|
|
|
23
25
|
.digest('base64');
|
|
24
26
|
return `${algorithm}-${hash}`;
|
|
25
27
|
} catch (error) {
|
|
26
|
-
console.error(
|
|
28
|
+
console.error(`${LOG_PREFIX} Failed to compute SRI hash: ${error}`);
|
|
27
29
|
return null;
|
|
28
30
|
}
|
|
29
31
|
}
|
|
@@ -31,8 +33,7 @@ function computeSri(content, algorithm = 'sha384') {
|
|
|
31
33
|
async function externalResourceIsCorsEnabled(url, options) {
|
|
32
34
|
try {
|
|
33
35
|
const response = await fetch(url, {
|
|
34
|
-
method: 'HEAD'
|
|
35
|
-
timeout: 5000
|
|
36
|
+
method: 'HEAD'
|
|
36
37
|
});
|
|
37
38
|
const acao = response.headers.get('access-control-allow-origin');
|
|
38
39
|
if (acao && (acao === '*' || acao.includes(options.domain || ''))) {
|
|
@@ -45,38 +46,12 @@ async function externalResourceIsCorsEnabled(url, options) {
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
async function replaceAsync(str, regex, asyncFn) {
|
|
49
|
-
const promises = [];
|
|
50
|
-
const matches = [];
|
|
51
|
-
|
|
52
|
-
str.replace(regex, (...args) => {
|
|
53
|
-
matches.push(args);
|
|
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
|
-
|
|
74
49
|
function isBypassDomain(url, bypassDomains = []) {
|
|
75
50
|
if (!bypassDomains.length) return false;
|
|
76
51
|
try {
|
|
77
52
|
const parsedUrl = url.startsWith('//')
|
|
78
|
-
? new URL(url
|
|
79
|
-
: new URL(url);
|
|
53
|
+
? new URL(`http:${url}`)
|
|
54
|
+
: new URL(url, 'http://dummy');
|
|
80
55
|
return bypassDomains.some(
|
|
81
56
|
(domain) => parsedUrl.hostname === domain ||
|
|
82
57
|
parsedUrl.hostname.endsWith(`.${domain}`)
|
|
@@ -90,92 +65,179 @@ function hasCrossOriginAttr(tag) {
|
|
|
90
65
|
return /(?:^|\s)crossorigin(?:=["']?[^"'\s>]*["']?)?(?:\s|>|$)/i.test(tag);
|
|
91
66
|
}
|
|
92
67
|
|
|
68
|
+
function getAllPossiblePaths(url) {
|
|
69
|
+
const paths = [
|
|
70
|
+
url,
|
|
71
|
+
url.startsWith('/') ? url.slice(1) : url,
|
|
72
|
+
url.startsWith('/static/') ? url.slice(8) : url,
|
|
73
|
+
!url.startsWith('/static/') ? `static/${url}` : url,
|
|
74
|
+
!url.startsWith('/static/') ? `/static/${url}` : url,
|
|
75
|
+
url.replace(/^\/static\//, ''),
|
|
76
|
+
url.replace(/^static\//, '')
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const withoutHash = url.replace(/-[a-zA-Z0-9]+\./, '.');
|
|
80
|
+
if (withoutHash !== url) {
|
|
81
|
+
paths.push(...getAllPossiblePaths(withoutHash));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return [...new Set(paths)];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function processTag(tag, url, options, sriMap) {
|
|
88
|
+
if (tag.includes('integrity=')) {
|
|
89
|
+
log(`Skip tag with existing integrity attribute: ${tag}`, options);
|
|
90
|
+
return tag;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
log(`Processing tag: ${tag}`, options);
|
|
94
|
+
log(`URL: ${url}`, options);
|
|
95
|
+
|
|
96
|
+
// Handle external resources
|
|
97
|
+
if (/^(https?:)?\/\//i.test(url)) {
|
|
98
|
+
if (isBypassDomain(url, options.bypassDomains)) {
|
|
99
|
+
log(`Skip SRI for bypass domain: ${url}`, options);
|
|
100
|
+
return tag;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const corsOk = await externalResourceIsCorsEnabled(url, options);
|
|
104
|
+
if (!corsOk) {
|
|
105
|
+
log(`External resource ${url} does not support CORS`, options);
|
|
106
|
+
return tag;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const response = await fetch(url);
|
|
111
|
+
const content = await response.arrayBuffer();
|
|
112
|
+
const hash = computeSri(Buffer.from(content), options.algorithm);
|
|
113
|
+
if (hash) {
|
|
114
|
+
log(`Computing SRI for external resource ${url}: ${hash}`, options);
|
|
115
|
+
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
116
|
+
const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
|
|
117
|
+
const newTag = tag.replace(/>$/, ` integrity="${hash}"${crossOriginAttr}>`);
|
|
118
|
+
log(`New tag: ${newTag}`, options);
|
|
119
|
+
return newTag;
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
log(`Failed to process external resource ${url}: ${error}`, options);
|
|
123
|
+
}
|
|
124
|
+
return tag;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Handle local resources
|
|
128
|
+
const possiblePaths = getAllPossiblePaths(url);
|
|
129
|
+
let integrity = null;
|
|
130
|
+
|
|
131
|
+
log(`Checking possible paths for ${url}:`, options);
|
|
132
|
+
for (const path of possiblePaths) {
|
|
133
|
+
log(`- Checking path: ${path}`, options);
|
|
134
|
+
if (sriMap.has(path)) {
|
|
135
|
+
integrity = sriMap.get(path);
|
|
136
|
+
log(`Found integrity for path ${path}: ${integrity}`, options);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (integrity) {
|
|
142
|
+
log(`Using precomputed SRI for ${url}: ${integrity}`, options);
|
|
143
|
+
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
144
|
+
const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
|
|
145
|
+
const newTag = tag.replace(/>$/, ` integrity="${integrity}"${crossOriginAttr}>`);
|
|
146
|
+
log(`New tag: ${newTag}`, options);
|
|
147
|
+
return newTag;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
log(`No SRI hash found for ${url}`, options);
|
|
151
|
+
log(`Available paths in sriMap: ${Array.from(sriMap.keys()).join(', ')}`, options);
|
|
152
|
+
return tag;
|
|
153
|
+
}
|
|
154
|
+
|
|
93
155
|
function sri(userOptions = {}) {
|
|
94
156
|
const options = { ...DEFAULT_OPTIONS, ...userOptions };
|
|
95
157
|
const sriMap = new Map();
|
|
158
|
+
let isBuild = false;
|
|
96
159
|
|
|
97
160
|
return {
|
|
98
161
|
name: 'vite-plugin-sri4',
|
|
99
162
|
apply: 'build',
|
|
163
|
+
enforce: 'post',
|
|
100
164
|
|
|
101
165
|
configResolved(config) {
|
|
102
166
|
options.domain = config.server?.host || '';
|
|
167
|
+
isBuild = config.command === 'build';
|
|
168
|
+
log('Plugin configured in ' + (isBuild ? 'build' : 'dev') + ' mode', options);
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
async renderChunk(code, chunk) {
|
|
172
|
+
if (!isBuild) return null;
|
|
173
|
+
|
|
174
|
+
const hash = computeSri(code, options.algorithm);
|
|
175
|
+
if (hash) {
|
|
176
|
+
for (const path of getAllPossiblePaths(chunk.fileName)) {
|
|
177
|
+
sriMap.set(path, hash);
|
|
178
|
+
log(`Stored SRI for path ${path}: ${hash}`, options);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
103
182
|
},
|
|
104
183
|
|
|
105
184
|
async generateBundle(_, bundle) {
|
|
185
|
+
if (!isBuild) return;
|
|
186
|
+
|
|
106
187
|
for (const fileName in bundle) {
|
|
107
188
|
const chunk = bundle[fileName];
|
|
108
|
-
if (chunk.type === '
|
|
109
|
-
const
|
|
110
|
-
if (
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
log(`Computed SRI for ${fileName}: ${hash}`, options);
|
|
189
|
+
if (chunk.type === 'asset' && !sriMap.has(fileName)) {
|
|
190
|
+
const hash = computeSri(chunk.source, options.algorithm);
|
|
191
|
+
if (hash) {
|
|
192
|
+
for (const path of getAllPossiblePaths(fileName)) {
|
|
193
|
+
sriMap.set(path, hash);
|
|
194
|
+
log(`Computing SRI for asset ${path}: ${hash}`, options);
|
|
115
195
|
}
|
|
116
196
|
}
|
|
117
197
|
}
|
|
118
198
|
}
|
|
199
|
+
|
|
200
|
+
log('Final sriMap contents:', options);
|
|
201
|
+
for (const [key, value] of sriMap.entries()) {
|
|
202
|
+
log(`${key} => ${value}`, options);
|
|
203
|
+
}
|
|
119
204
|
},
|
|
120
205
|
|
|
121
206
|
async transformIndexHtml(html) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const actualSrc = src || moduleSrc;
|
|
207
|
+
if (!isBuild || !html) {
|
|
208
|
+
log('Skipping HTML transform in dev mode or empty HTML', options);
|
|
209
|
+
return html;
|
|
210
|
+
}
|
|
127
211
|
|
|
128
|
-
|
|
129
|
-
|
|
212
|
+
try {
|
|
213
|
+
log('Starting HTML transformation', options);
|
|
214
|
+
log(`SRI Map size: ${sriMap.size}`, options);
|
|
215
|
+
|
|
216
|
+
// Process script tags
|
|
217
|
+
const scriptTags = html.match(/<script[^>]+src=["']([^"']+)["'][^>]*>/g) || [];
|
|
218
|
+
log(`Found ${scriptTags.length} script tags`, options);
|
|
219
|
+
for (const tag of scriptTags) {
|
|
220
|
+
const url = tag.match(/src=["']([^"']+)["']/)[1];
|
|
221
|
+
log(`Processing script: ${url}`, options);
|
|
222
|
+
const newTag = await processTag(tag, url, options, sriMap);
|
|
223
|
+
html = html.replace(tag, newTag);
|
|
130
224
|
}
|
|
131
225
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
226
|
+
// Process link tags
|
|
227
|
+
const linkTags = html.match(/<link[^>]+href=["']([^"']+)["'][^>]*>/g) || [];
|
|
228
|
+
log(`Found ${linkTags.length} link tags`, options);
|
|
229
|
+
for (const tag of linkTags) {
|
|
230
|
+
const url = tag.match(/href=["']([^"']+)["']/)[1];
|
|
231
|
+
log(`Processing link: ${url}`, options);
|
|
232
|
+
const newTag = await processTag(tag, url, options, sriMap);
|
|
233
|
+
html = html.replace(tag, newTag);
|
|
136
234
|
}
|
|
137
235
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const fileName = actualSrc.startsWith('/') ? actualSrc.slice(1) : actualSrc;
|
|
147
|
-
const integrity = sriMap.get(fileName);
|
|
148
|
-
|
|
149
|
-
if (integrity) {
|
|
150
|
-
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
151
|
-
if (!hasCrossOrigin) {
|
|
152
|
-
return tag.replace(
|
|
153
|
-
/>$/,
|
|
154
|
-
` integrity="${integrity}" crossorigin="${options.crossorigin}">`
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
return tag.replace(
|
|
158
|
-
/>$/,
|
|
159
|
-
` integrity="${integrity}">`
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return tag;
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
html = await replaceAsync(
|
|
167
|
-
html,
|
|
168
|
-
/(<script[^>]+(?:src="([^"]+)"[^>]*|type="module"[^>]*src="([^"]+)"[^>]*)>)/g,
|
|
169
|
-
processTag
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
html = await replaceAsync(
|
|
173
|
-
html,
|
|
174
|
-
/(<link[^>]+href="([^"]+)"[^>]*>)/g,
|
|
175
|
-
processTag
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
return html;
|
|
236
|
+
return html;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error(`${LOG_PREFIX} Failed to transform HTML: ${error}`);
|
|
239
|
+
return html;
|
|
240
|
+
}
|
|
179
241
|
}
|
|
180
242
|
};
|
|
181
243
|
}
|
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: [],
|
|
@@ -10,7 +12,7 @@ const DEFAULT_OPTIONS = {
|
|
|
10
12
|
|
|
11
13
|
function log(message, options) {
|
|
12
14
|
if (options.debug) {
|
|
13
|
-
console.log(
|
|
15
|
+
console.log(`${LOG_PREFIX} ${message}`);
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -21,7 +23,7 @@ function computeSri(content, algorithm = 'sha384') {
|
|
|
21
23
|
.digest('base64');
|
|
22
24
|
return `${algorithm}-${hash}`;
|
|
23
25
|
} catch (error) {
|
|
24
|
-
console.error(
|
|
26
|
+
console.error(`${LOG_PREFIX} Failed to compute SRI hash: ${error}`);
|
|
25
27
|
return null;
|
|
26
28
|
}
|
|
27
29
|
}
|
|
@@ -29,8 +31,7 @@ function computeSri(content, algorithm = 'sha384') {
|
|
|
29
31
|
async function externalResourceIsCorsEnabled(url, options) {
|
|
30
32
|
try {
|
|
31
33
|
const response = await fetch(url, {
|
|
32
|
-
method: 'HEAD'
|
|
33
|
-
timeout: 5000
|
|
34
|
+
method: 'HEAD'
|
|
34
35
|
});
|
|
35
36
|
const acao = response.headers.get('access-control-allow-origin');
|
|
36
37
|
if (acao && (acao === '*' || acao.includes(options.domain || ''))) {
|
|
@@ -43,38 +44,12 @@ async function externalResourceIsCorsEnabled(url, options) {
|
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
async function replaceAsync(str, regex, asyncFn) {
|
|
47
|
-
const promises = [];
|
|
48
|
-
const matches = [];
|
|
49
|
-
|
|
50
|
-
str.replace(regex, (...args) => {
|
|
51
|
-
matches.push(args);
|
|
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
|
-
|
|
72
47
|
function isBypassDomain(url, bypassDomains = []) {
|
|
73
48
|
if (!bypassDomains.length) return false;
|
|
74
49
|
try {
|
|
75
50
|
const parsedUrl = url.startsWith('//')
|
|
76
|
-
? new URL(url
|
|
77
|
-
: new URL(url);
|
|
51
|
+
? new URL(`http:${url}`)
|
|
52
|
+
: new URL(url, 'http://dummy');
|
|
78
53
|
return bypassDomains.some(
|
|
79
54
|
(domain) => parsedUrl.hostname === domain ||
|
|
80
55
|
parsedUrl.hostname.endsWith(`.${domain}`)
|
|
@@ -88,92 +63,179 @@ function hasCrossOriginAttr(tag) {
|
|
|
88
63
|
return /(?:^|\s)crossorigin(?:=["']?[^"'\s>]*["']?)?(?:\s|>|$)/i.test(tag);
|
|
89
64
|
}
|
|
90
65
|
|
|
66
|
+
function getAllPossiblePaths(url) {
|
|
67
|
+
const paths = [
|
|
68
|
+
url,
|
|
69
|
+
url.startsWith('/') ? url.slice(1) : url,
|
|
70
|
+
url.startsWith('/static/') ? url.slice(8) : url,
|
|
71
|
+
!url.startsWith('/static/') ? `static/${url}` : url,
|
|
72
|
+
!url.startsWith('/static/') ? `/static/${url}` : url,
|
|
73
|
+
url.replace(/^\/static\//, ''),
|
|
74
|
+
url.replace(/^static\//, '')
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const withoutHash = url.replace(/-[a-zA-Z0-9]+\./, '.');
|
|
78
|
+
if (withoutHash !== url) {
|
|
79
|
+
paths.push(...getAllPossiblePaths(withoutHash));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return [...new Set(paths)];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function processTag(tag, url, options, sriMap) {
|
|
86
|
+
if (tag.includes('integrity=')) {
|
|
87
|
+
log(`Skip tag with existing integrity attribute: ${tag}`, options);
|
|
88
|
+
return tag;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
log(`Processing tag: ${tag}`, options);
|
|
92
|
+
log(`URL: ${url}`, options);
|
|
93
|
+
|
|
94
|
+
// Handle external resources
|
|
95
|
+
if (/^(https?:)?\/\//i.test(url)) {
|
|
96
|
+
if (isBypassDomain(url, options.bypassDomains)) {
|
|
97
|
+
log(`Skip SRI for bypass domain: ${url}`, options);
|
|
98
|
+
return tag;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const corsOk = await externalResourceIsCorsEnabled(url, options);
|
|
102
|
+
if (!corsOk) {
|
|
103
|
+
log(`External resource ${url} does not support CORS`, options);
|
|
104
|
+
return tag;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const response = await fetch(url);
|
|
109
|
+
const content = await response.arrayBuffer();
|
|
110
|
+
const hash = computeSri(Buffer.from(content), options.algorithm);
|
|
111
|
+
if (hash) {
|
|
112
|
+
log(`Computing SRI for external resource ${url}: ${hash}`, options);
|
|
113
|
+
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
114
|
+
const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
|
|
115
|
+
const newTag = tag.replace(/>$/, ` integrity="${hash}"${crossOriginAttr}>`);
|
|
116
|
+
log(`New tag: ${newTag}`, options);
|
|
117
|
+
return newTag;
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
log(`Failed to process external resource ${url}: ${error}`, options);
|
|
121
|
+
}
|
|
122
|
+
return tag;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Handle local resources
|
|
126
|
+
const possiblePaths = getAllPossiblePaths(url);
|
|
127
|
+
let integrity = null;
|
|
128
|
+
|
|
129
|
+
log(`Checking possible paths for ${url}:`, options);
|
|
130
|
+
for (const path of possiblePaths) {
|
|
131
|
+
log(`- Checking path: ${path}`, options);
|
|
132
|
+
if (sriMap.has(path)) {
|
|
133
|
+
integrity = sriMap.get(path);
|
|
134
|
+
log(`Found integrity for path ${path}: ${integrity}`, options);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (integrity) {
|
|
140
|
+
log(`Using precomputed SRI for ${url}: ${integrity}`, options);
|
|
141
|
+
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
142
|
+
const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
|
|
143
|
+
const newTag = tag.replace(/>$/, ` integrity="${integrity}"${crossOriginAttr}>`);
|
|
144
|
+
log(`New tag: ${newTag}`, options);
|
|
145
|
+
return newTag;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
log(`No SRI hash found for ${url}`, options);
|
|
149
|
+
log(`Available paths in sriMap: ${Array.from(sriMap.keys()).join(', ')}`, options);
|
|
150
|
+
return tag;
|
|
151
|
+
}
|
|
152
|
+
|
|
91
153
|
function sri(userOptions = {}) {
|
|
92
154
|
const options = { ...DEFAULT_OPTIONS, ...userOptions };
|
|
93
155
|
const sriMap = new Map();
|
|
156
|
+
let isBuild = false;
|
|
94
157
|
|
|
95
158
|
return {
|
|
96
159
|
name: 'vite-plugin-sri4',
|
|
97
160
|
apply: 'build',
|
|
161
|
+
enforce: 'post',
|
|
98
162
|
|
|
99
163
|
configResolved(config) {
|
|
100
164
|
options.domain = config.server?.host || '';
|
|
165
|
+
isBuild = config.command === 'build';
|
|
166
|
+
log('Plugin configured in ' + (isBuild ? 'build' : 'dev') + ' mode', options);
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
async renderChunk(code, chunk) {
|
|
170
|
+
if (!isBuild) return null;
|
|
171
|
+
|
|
172
|
+
const hash = computeSri(code, options.algorithm);
|
|
173
|
+
if (hash) {
|
|
174
|
+
for (const path of getAllPossiblePaths(chunk.fileName)) {
|
|
175
|
+
sriMap.set(path, hash);
|
|
176
|
+
log(`Stored SRI for path ${path}: ${hash}`, options);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
101
180
|
},
|
|
102
181
|
|
|
103
182
|
async generateBundle(_, bundle) {
|
|
183
|
+
if (!isBuild) return;
|
|
184
|
+
|
|
104
185
|
for (const fileName in bundle) {
|
|
105
186
|
const chunk = bundle[fileName];
|
|
106
|
-
if (chunk.type === '
|
|
107
|
-
const
|
|
108
|
-
if (
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
log(`Computed SRI for ${fileName}: ${hash}`, options);
|
|
187
|
+
if (chunk.type === 'asset' && !sriMap.has(fileName)) {
|
|
188
|
+
const hash = computeSri(chunk.source, options.algorithm);
|
|
189
|
+
if (hash) {
|
|
190
|
+
for (const path of getAllPossiblePaths(fileName)) {
|
|
191
|
+
sriMap.set(path, hash);
|
|
192
|
+
log(`Computing SRI for asset ${path}: ${hash}`, options);
|
|
113
193
|
}
|
|
114
194
|
}
|
|
115
195
|
}
|
|
116
196
|
}
|
|
197
|
+
|
|
198
|
+
log('Final sriMap contents:', options);
|
|
199
|
+
for (const [key, value] of sriMap.entries()) {
|
|
200
|
+
log(`${key} => ${value}`, options);
|
|
201
|
+
}
|
|
117
202
|
},
|
|
118
203
|
|
|
119
204
|
async transformIndexHtml(html) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const actualSrc = src || moduleSrc;
|
|
205
|
+
if (!isBuild || !html) {
|
|
206
|
+
log('Skipping HTML transform in dev mode or empty HTML', options);
|
|
207
|
+
return html;
|
|
208
|
+
}
|
|
125
209
|
|
|
126
|
-
|
|
127
|
-
|
|
210
|
+
try {
|
|
211
|
+
log('Starting HTML transformation', options);
|
|
212
|
+
log(`SRI Map size: ${sriMap.size}`, options);
|
|
213
|
+
|
|
214
|
+
// Process script tags
|
|
215
|
+
const scriptTags = html.match(/<script[^>]+src=["']([^"']+)["'][^>]*>/g) || [];
|
|
216
|
+
log(`Found ${scriptTags.length} script tags`, options);
|
|
217
|
+
for (const tag of scriptTags) {
|
|
218
|
+
const url = tag.match(/src=["']([^"']+)["']/)[1];
|
|
219
|
+
log(`Processing script: ${url}`, options);
|
|
220
|
+
const newTag = await processTag(tag, url, options, sriMap);
|
|
221
|
+
html = html.replace(tag, newTag);
|
|
128
222
|
}
|
|
129
223
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
224
|
+
// Process link tags
|
|
225
|
+
const linkTags = html.match(/<link[^>]+href=["']([^"']+)["'][^>]*>/g) || [];
|
|
226
|
+
log(`Found ${linkTags.length} link tags`, options);
|
|
227
|
+
for (const tag of linkTags) {
|
|
228
|
+
const url = tag.match(/href=["']([^"']+)["']/)[1];
|
|
229
|
+
log(`Processing link: ${url}`, options);
|
|
230
|
+
const newTag = await processTag(tag, url, options, sriMap);
|
|
231
|
+
html = html.replace(tag, newTag);
|
|
134
232
|
}
|
|
135
233
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const fileName = actualSrc.startsWith('/') ? actualSrc.slice(1) : actualSrc;
|
|
145
|
-
const integrity = sriMap.get(fileName);
|
|
146
|
-
|
|
147
|
-
if (integrity) {
|
|
148
|
-
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
149
|
-
if (!hasCrossOrigin) {
|
|
150
|
-
return tag.replace(
|
|
151
|
-
/>$/,
|
|
152
|
-
` integrity="${integrity}" crossorigin="${options.crossorigin}">`
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
return tag.replace(
|
|
156
|
-
/>$/,
|
|
157
|
-
` integrity="${integrity}">`
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return tag;
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
html = await replaceAsync(
|
|
165
|
-
html,
|
|
166
|
-
/(<script[^>]+(?:src="([^"]+)"[^>]*|type="module"[^>]*src="([^"]+)"[^>]*)>)/g,
|
|
167
|
-
processTag
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
html = await replaceAsync(
|
|
171
|
-
html,
|
|
172
|
-
/(<link[^>]+href="([^"]+)"[^>]*>)/g,
|
|
173
|
-
processTag
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
return html;
|
|
234
|
+
return html;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error(`${LOG_PREFIX} Failed to transform HTML: ${error}`);
|
|
237
|
+
return html;
|
|
238
|
+
}
|
|
177
239
|
}
|
|
178
240
|
};
|
|
179
241
|
}
|