vite-plugin-sri4 1.8.2 → 1.8.5
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 +88 -143
- package/dist/index.js +88 -143
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -9,8 +9,7 @@ const DEFAULT_OPTIONS = {
|
|
|
9
9
|
algorithm: 'sha384',
|
|
10
10
|
bypassDomains: [],
|
|
11
11
|
crossorigin: 'anonymous',
|
|
12
|
-
debug: false
|
|
13
|
-
inlineScripts: false
|
|
12
|
+
debug: false
|
|
14
13
|
};
|
|
15
14
|
|
|
16
15
|
function log(message, options) {
|
|
@@ -21,13 +20,15 @@ function log(message, options) {
|
|
|
21
20
|
|
|
22
21
|
function computeSri(content, algorithm = 'sha384') {
|
|
23
22
|
try {
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const hash = node_crypto.createHash(algorithm);
|
|
24
|
+
if (Buffer.isBuffer(content) || content instanceof Uint8Array) {
|
|
25
|
+
hash.update(content);
|
|
26
|
+
} else if (typeof content === 'string') {
|
|
27
|
+
hash.update(Buffer.from(content, 'utf-8'));
|
|
28
|
+
} else {
|
|
29
|
+
throw new Error('Invalid content type');
|
|
26
30
|
}
|
|
27
|
-
|
|
28
|
-
.update(content)
|
|
29
|
-
.digest('base64');
|
|
30
|
-
return `${algorithm}-${hash}`;
|
|
31
|
+
return `${algorithm}-${hash.digest('base64')}`;
|
|
31
32
|
} catch (error) {
|
|
32
33
|
console.error(`${LOG_PREFIX} Failed to compute SRI hash: ${error}`);
|
|
33
34
|
return null;
|
|
@@ -53,21 +54,14 @@ async function externalResourceIsCorsEnabled(url, options) {
|
|
|
53
54
|
function isBypassDomain(url, bypassDomains = []) {
|
|
54
55
|
if (!bypassDomains.length) return false;
|
|
55
56
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
57
|
+
const parsedUrl = url.startsWith('//')
|
|
58
|
+
? new URL(`http:${url}`)
|
|
59
|
+
: new URL(url, 'http://dummy');
|
|
65
60
|
return bypassDomains.some(
|
|
66
61
|
(domain) => parsedUrl.hostname === domain ||
|
|
67
62
|
parsedUrl.hostname.endsWith(`.${domain}`)
|
|
68
63
|
);
|
|
69
64
|
} catch (e) {
|
|
70
|
-
console.error(`${LOG_PREFIX} Failed to parse URL: ${url}`, e);
|
|
71
65
|
return false;
|
|
72
66
|
}
|
|
73
67
|
}
|
|
@@ -76,38 +70,33 @@ function hasCrossOriginAttr(tag) {
|
|
|
76
70
|
return /(?:^|\s)crossorigin(?:=["']?[^"'\s>]*["']?)?(?:\s|>|$)/i.test(tag);
|
|
77
71
|
}
|
|
78
72
|
|
|
79
|
-
function
|
|
73
|
+
function getBundleKey(url, base = '') {
|
|
74
|
+
// Remove base prefix if exists
|
|
75
|
+
let cleanUrl = url.startsWith(base) ? url.slice(base.length) : url;
|
|
76
|
+
// Remove leading slash
|
|
77
|
+
cleanUrl = cleanUrl.startsWith('/') ? cleanUrl.slice(1) : cleanUrl;
|
|
78
|
+
|
|
79
|
+
// Try different path combinations
|
|
80
80
|
const paths = [
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
!url.startsWith('/static/') ? `static/${url}` : url,
|
|
85
|
-
!url.startsWith('/static/') ? `/static/${url}` : url,
|
|
86
|
-
url.replace(/^\/static\//, ''),
|
|
87
|
-
url.replace(/^static\//, '')
|
|
81
|
+
cleanUrl,
|
|
82
|
+
`static/${cleanUrl}`,
|
|
83
|
+
cleanUrl.replace(/^static\//, '')
|
|
88
84
|
];
|
|
89
85
|
|
|
90
|
-
//
|
|
91
|
-
const withoutHash =
|
|
92
|
-
if (withoutHash !==
|
|
93
|
-
paths.push(...
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (hashedMatch) {
|
|
99
|
-
const [, base, ext] = hashedMatch;
|
|
100
|
-
paths.push(
|
|
101
|
-
`${base}${ext}`,
|
|
102
|
-
`/static/${base}${ext}`,
|
|
103
|
-
`static/${base}${ext}`
|
|
104
|
-
);
|
|
86
|
+
// Remove hash part if exists and try again
|
|
87
|
+
const withoutHash = cleanUrl.replace(/-[a-zA-Z0-9]+\.([^.]+)$/, '.$1');
|
|
88
|
+
if (withoutHash !== cleanUrl) {
|
|
89
|
+
paths.push(...[
|
|
90
|
+
withoutHash,
|
|
91
|
+
`static/${withoutHash}`,
|
|
92
|
+
withoutHash.replace(/^static\//, '')
|
|
93
|
+
]);
|
|
105
94
|
}
|
|
106
95
|
|
|
107
96
|
return [...new Set(paths)];
|
|
108
97
|
}
|
|
109
98
|
|
|
110
|
-
async function processTag(tag, url, options,
|
|
99
|
+
async function processTag(tag, url, options, bundle, base = '') {
|
|
111
100
|
if (tag.includes('integrity=')) {
|
|
112
101
|
log(`Skip tag with existing integrity attribute: ${tag}`, options);
|
|
113
102
|
return tag;
|
|
@@ -148,54 +137,49 @@ async function processTag(tag, url, options, sriMap) {
|
|
|
148
137
|
}
|
|
149
138
|
|
|
150
139
|
// Handle local resources
|
|
151
|
-
const
|
|
152
|
-
let
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
log(`
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
for (const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
log(`Found integrity for path ${path}: ${integrity}`, options);
|
|
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);
|
|
163
151
|
break;
|
|
164
152
|
}
|
|
165
153
|
}
|
|
166
154
|
|
|
167
|
-
if (
|
|
168
|
-
log(`
|
|
169
|
-
|
|
170
|
-
const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
|
|
171
|
-
const newTag = tag.replace(/>$/, ` integrity="${integrity}"${crossOriginAttr}>`);
|
|
172
|
-
log(`New tag: ${newTag}`, options);
|
|
173
|
-
return newTag;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
log(`No SRI hash found for ${url}`, options);
|
|
177
|
-
log(`Available paths in sriMap: ${Array.from(sriMap.keys()).join(', ')}`, options);
|
|
178
|
-
return tag;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async function processInlineScript(tag, options) {
|
|
182
|
-
if (tag.includes('integrity=')) {
|
|
183
|
-
log(`Skip inline script with existing integrity attribute: ${tag}`, options);
|
|
155
|
+
if (!bundleItem) {
|
|
156
|
+
log(`Available bundle keys:`, options);
|
|
157
|
+
Object.keys(bundle).forEach(key => log(`- ${key}`, options));
|
|
184
158
|
return tag;
|
|
185
159
|
}
|
|
186
160
|
|
|
187
|
-
|
|
188
|
-
if (!content) {
|
|
189
|
-
log(`Skip empty inline script: ${tag}`, options);
|
|
190
|
-
return tag;
|
|
191
|
-
}
|
|
161
|
+
log(`Bundle item type: ${bundleItem.type}`, options);
|
|
192
162
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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);
|
|
199
183
|
}
|
|
200
184
|
|
|
201
185
|
return tag;
|
|
@@ -203,8 +187,8 @@ async function processInlineScript(tag, options) {
|
|
|
203
187
|
|
|
204
188
|
function sri(userOptions = {}) {
|
|
205
189
|
const options = { ...DEFAULT_OPTIONS, ...userOptions };
|
|
206
|
-
const sriMap = new Map();
|
|
207
190
|
let isBuild = false;
|
|
191
|
+
let base = '';
|
|
208
192
|
|
|
209
193
|
return {
|
|
210
194
|
name: 'vite-plugin-sri4',
|
|
@@ -214,45 +198,12 @@ function sri(userOptions = {}) {
|
|
|
214
198
|
configResolved(config) {
|
|
215
199
|
options.domain = config.server?.host || '';
|
|
216
200
|
isBuild = config.command === 'build';
|
|
201
|
+
base = config.base || '';
|
|
217
202
|
log('Plugin configured in ' + (isBuild ? 'build' : 'dev') + ' mode', options);
|
|
203
|
+
log(`Base URL: ${base}`, options);
|
|
218
204
|
},
|
|
219
205
|
|
|
220
|
-
async
|
|
221
|
-
if (!isBuild) return null;
|
|
222
|
-
|
|
223
|
-
const hash = computeSri(code, options.algorithm);
|
|
224
|
-
if (hash) {
|
|
225
|
-
for (const path of getAllPossiblePaths(chunk.fileName)) {
|
|
226
|
-
sriMap.set(path, hash);
|
|
227
|
-
log(`Stored SRI for path ${path}: ${hash}`, options);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return null;
|
|
231
|
-
},
|
|
232
|
-
|
|
233
|
-
async generateBundle(_, bundle) {
|
|
234
|
-
if (!isBuild) return;
|
|
235
|
-
|
|
236
|
-
for (const fileName in bundle) {
|
|
237
|
-
const chunk = bundle[fileName];
|
|
238
|
-
if (chunk.type === 'asset' && !sriMap.has(fileName)) {
|
|
239
|
-
const hash = computeSri(chunk.source, options.algorithm);
|
|
240
|
-
if (hash) {
|
|
241
|
-
for (const path of getAllPossiblePaths(fileName)) {
|
|
242
|
-
sriMap.set(path, hash);
|
|
243
|
-
log(`Computing SRI for asset ${path}: ${hash}`, options);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
log('Final sriMap contents:', options);
|
|
250
|
-
for (const [key, value] of sriMap.entries()) {
|
|
251
|
-
log(`${key} => ${value}`, options);
|
|
252
|
-
}
|
|
253
|
-
},
|
|
254
|
-
|
|
255
|
-
async transformIndexHtml(html) {
|
|
206
|
+
async transformIndexHtml(html, ctx) {
|
|
256
207
|
if (!isBuild || !html) {
|
|
257
208
|
log('Skipping HTML transform in dev mode or empty HTML', options);
|
|
258
209
|
return html;
|
|
@@ -260,38 +211,32 @@ function sri(userOptions = {}) {
|
|
|
260
211
|
|
|
261
212
|
try {
|
|
262
213
|
log('Starting HTML transformation', options);
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
214
|
+
const bundle = ctx.bundle || {};
|
|
215
|
+
log(`Bundle size: ${Object.keys(bundle).length}`, options);
|
|
216
|
+
|
|
217
|
+
// Process script tags with and without quotes
|
|
218
|
+
const scriptTagRegex = /<script[^>]+src=(?:["']([^"']+)["']|([^ >]+))[^>]*>/g;
|
|
219
|
+
let match;
|
|
220
|
+
while ((match = scriptTagRegex.exec(html)) !== null) {
|
|
221
|
+
const [tag, quotedUrl, unquotedUrl] = match;
|
|
222
|
+
const url = quotedUrl || unquotedUrl;
|
|
270
223
|
log(`Processing script: ${url}`, options);
|
|
271
|
-
const newTag = await processTag(tag, url, options,
|
|
224
|
+
const newTag = await processTag(tag, url, options, bundle, base);
|
|
272
225
|
html = html.replace(tag, newTag);
|
|
273
226
|
}
|
|
274
227
|
|
|
275
|
-
// Process
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
228
|
+
// Process link tags with and without quotes
|
|
229
|
+
const linkTagRegex = /<link[^>]+href=(?:["']([^"']+)["']|([^ >]+))[^>]*>/g;
|
|
230
|
+
while ((match = linkTagRegex.exec(html)) !== null) {
|
|
231
|
+
const [tag, quotedUrl, unquotedUrl] = match;
|
|
232
|
+
const url = quotedUrl || unquotedUrl;
|
|
233
|
+
if (tag.includes('stylesheet') || tag.includes('modulepreload')) {
|
|
234
|
+
log(`Processing link: ${url}`, options);
|
|
235
|
+
const newTag = await processTag(tag, url, options, bundle, base);
|
|
281
236
|
html = html.replace(tag, newTag);
|
|
282
237
|
}
|
|
283
238
|
}
|
|
284
239
|
|
|
285
|
-
// Process link tags
|
|
286
|
-
const linkTags = html.match(/<link[^>]+href=["']([^"']+)["'][^>]*>/g) || [];
|
|
287
|
-
log(`Found ${linkTags.length} link tags`, options);
|
|
288
|
-
for (const tag of linkTags) {
|
|
289
|
-
const url = tag.match(/href=["']([^"']+)["']/)[1];
|
|
290
|
-
log(`Processing link: ${url}`, options);
|
|
291
|
-
const newTag = await processTag(tag, url, options, sriMap);
|
|
292
|
-
html = html.replace(tag, newTag);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
240
|
return html;
|
|
296
241
|
} catch (error) {
|
|
297
242
|
console.error(`${LOG_PREFIX} Failed to transform HTML: ${error}`);
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,7 @@ const DEFAULT_OPTIONS = {
|
|
|
7
7
|
algorithm: 'sha384',
|
|
8
8
|
bypassDomains: [],
|
|
9
9
|
crossorigin: 'anonymous',
|
|
10
|
-
debug: false
|
|
11
|
-
inlineScripts: false
|
|
10
|
+
debug: false
|
|
12
11
|
};
|
|
13
12
|
|
|
14
13
|
function log(message, options) {
|
|
@@ -19,13 +18,15 @@ function log(message, options) {
|
|
|
19
18
|
|
|
20
19
|
function computeSri(content, algorithm = 'sha384') {
|
|
21
20
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const hash = createHash(algorithm);
|
|
22
|
+
if (Buffer.isBuffer(content) || content instanceof Uint8Array) {
|
|
23
|
+
hash.update(content);
|
|
24
|
+
} else if (typeof content === 'string') {
|
|
25
|
+
hash.update(Buffer.from(content, 'utf-8'));
|
|
26
|
+
} else {
|
|
27
|
+
throw new Error('Invalid content type');
|
|
24
28
|
}
|
|
25
|
-
|
|
26
|
-
.update(content)
|
|
27
|
-
.digest('base64');
|
|
28
|
-
return `${algorithm}-${hash}`;
|
|
29
|
+
return `${algorithm}-${hash.digest('base64')}`;
|
|
29
30
|
} catch (error) {
|
|
30
31
|
console.error(`${LOG_PREFIX} Failed to compute SRI hash: ${error}`);
|
|
31
32
|
return null;
|
|
@@ -51,21 +52,14 @@ async function externalResourceIsCorsEnabled(url, options) {
|
|
|
51
52
|
function isBypassDomain(url, bypassDomains = []) {
|
|
52
53
|
if (!bypassDomains.length) return false;
|
|
53
54
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
55
|
+
const parsedUrl = url.startsWith('//')
|
|
56
|
+
? new URL(`http:${url}`)
|
|
57
|
+
: new URL(url, 'http://dummy');
|
|
63
58
|
return bypassDomains.some(
|
|
64
59
|
(domain) => parsedUrl.hostname === domain ||
|
|
65
60
|
parsedUrl.hostname.endsWith(`.${domain}`)
|
|
66
61
|
);
|
|
67
62
|
} catch (e) {
|
|
68
|
-
console.error(`${LOG_PREFIX} Failed to parse URL: ${url}`, e);
|
|
69
63
|
return false;
|
|
70
64
|
}
|
|
71
65
|
}
|
|
@@ -74,38 +68,33 @@ function hasCrossOriginAttr(tag) {
|
|
|
74
68
|
return /(?:^|\s)crossorigin(?:=["']?[^"'\s>]*["']?)?(?:\s|>|$)/i.test(tag);
|
|
75
69
|
}
|
|
76
70
|
|
|
77
|
-
function
|
|
71
|
+
function getBundleKey(url, base = '') {
|
|
72
|
+
// Remove base prefix if exists
|
|
73
|
+
let cleanUrl = url.startsWith(base) ? url.slice(base.length) : url;
|
|
74
|
+
// Remove leading slash
|
|
75
|
+
cleanUrl = cleanUrl.startsWith('/') ? cleanUrl.slice(1) : cleanUrl;
|
|
76
|
+
|
|
77
|
+
// Try different path combinations
|
|
78
78
|
const paths = [
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
!url.startsWith('/static/') ? `static/${url}` : url,
|
|
83
|
-
!url.startsWith('/static/') ? `/static/${url}` : url,
|
|
84
|
-
url.replace(/^\/static\//, ''),
|
|
85
|
-
url.replace(/^static\//, '')
|
|
79
|
+
cleanUrl,
|
|
80
|
+
`static/${cleanUrl}`,
|
|
81
|
+
cleanUrl.replace(/^static\//, '')
|
|
86
82
|
];
|
|
87
83
|
|
|
88
|
-
//
|
|
89
|
-
const withoutHash =
|
|
90
|
-
if (withoutHash !==
|
|
91
|
-
paths.push(...
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (hashedMatch) {
|
|
97
|
-
const [, base, ext] = hashedMatch;
|
|
98
|
-
paths.push(
|
|
99
|
-
`${base}${ext}`,
|
|
100
|
-
`/static/${base}${ext}`,
|
|
101
|
-
`static/${base}${ext}`
|
|
102
|
-
);
|
|
84
|
+
// Remove hash part if exists and try again
|
|
85
|
+
const withoutHash = cleanUrl.replace(/-[a-zA-Z0-9]+\.([^.]+)$/, '.$1');
|
|
86
|
+
if (withoutHash !== cleanUrl) {
|
|
87
|
+
paths.push(...[
|
|
88
|
+
withoutHash,
|
|
89
|
+
`static/${withoutHash}`,
|
|
90
|
+
withoutHash.replace(/^static\//, '')
|
|
91
|
+
]);
|
|
103
92
|
}
|
|
104
93
|
|
|
105
94
|
return [...new Set(paths)];
|
|
106
95
|
}
|
|
107
96
|
|
|
108
|
-
async function processTag(tag, url, options,
|
|
97
|
+
async function processTag(tag, url, options, bundle, base = '') {
|
|
109
98
|
if (tag.includes('integrity=')) {
|
|
110
99
|
log(`Skip tag with existing integrity attribute: ${tag}`, options);
|
|
111
100
|
return tag;
|
|
@@ -146,54 +135,49 @@ async function processTag(tag, url, options, sriMap) {
|
|
|
146
135
|
}
|
|
147
136
|
|
|
148
137
|
// Handle local resources
|
|
149
|
-
const
|
|
150
|
-
let
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
log(`
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
for (const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
log(`Found integrity for path ${path}: ${integrity}`, options);
|
|
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);
|
|
161
149
|
break;
|
|
162
150
|
}
|
|
163
151
|
}
|
|
164
152
|
|
|
165
|
-
if (
|
|
166
|
-
log(`
|
|
167
|
-
|
|
168
|
-
const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
|
|
169
|
-
const newTag = tag.replace(/>$/, ` integrity="${integrity}"${crossOriginAttr}>`);
|
|
170
|
-
log(`New tag: ${newTag}`, options);
|
|
171
|
-
return newTag;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
log(`No SRI hash found for ${url}`, options);
|
|
175
|
-
log(`Available paths in sriMap: ${Array.from(sriMap.keys()).join(', ')}`, options);
|
|
176
|
-
return tag;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async function processInlineScript(tag, options) {
|
|
180
|
-
if (tag.includes('integrity=')) {
|
|
181
|
-
log(`Skip inline script with existing integrity attribute: ${tag}`, options);
|
|
153
|
+
if (!bundleItem) {
|
|
154
|
+
log(`Available bundle keys:`, options);
|
|
155
|
+
Object.keys(bundle).forEach(key => log(`- ${key}`, options));
|
|
182
156
|
return tag;
|
|
183
157
|
}
|
|
184
158
|
|
|
185
|
-
|
|
186
|
-
if (!content) {
|
|
187
|
-
log(`Skip empty inline script: ${tag}`, options);
|
|
188
|
-
return tag;
|
|
189
|
-
}
|
|
159
|
+
log(`Bundle item type: ${bundleItem.type}`, options);
|
|
190
160
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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);
|
|
197
181
|
}
|
|
198
182
|
|
|
199
183
|
return tag;
|
|
@@ -201,8 +185,8 @@ async function processInlineScript(tag, options) {
|
|
|
201
185
|
|
|
202
186
|
function sri(userOptions = {}) {
|
|
203
187
|
const options = { ...DEFAULT_OPTIONS, ...userOptions };
|
|
204
|
-
const sriMap = new Map();
|
|
205
188
|
let isBuild = false;
|
|
189
|
+
let base = '';
|
|
206
190
|
|
|
207
191
|
return {
|
|
208
192
|
name: 'vite-plugin-sri4',
|
|
@@ -212,45 +196,12 @@ function sri(userOptions = {}) {
|
|
|
212
196
|
configResolved(config) {
|
|
213
197
|
options.domain = config.server?.host || '';
|
|
214
198
|
isBuild = config.command === 'build';
|
|
199
|
+
base = config.base || '';
|
|
215
200
|
log('Plugin configured in ' + (isBuild ? 'build' : 'dev') + ' mode', options);
|
|
201
|
+
log(`Base URL: ${base}`, options);
|
|
216
202
|
},
|
|
217
203
|
|
|
218
|
-
async
|
|
219
|
-
if (!isBuild) return null;
|
|
220
|
-
|
|
221
|
-
const hash = computeSri(code, options.algorithm);
|
|
222
|
-
if (hash) {
|
|
223
|
-
for (const path of getAllPossiblePaths(chunk.fileName)) {
|
|
224
|
-
sriMap.set(path, hash);
|
|
225
|
-
log(`Stored SRI for path ${path}: ${hash}`, options);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return null;
|
|
229
|
-
},
|
|
230
|
-
|
|
231
|
-
async generateBundle(_, bundle) {
|
|
232
|
-
if (!isBuild) return;
|
|
233
|
-
|
|
234
|
-
for (const fileName in bundle) {
|
|
235
|
-
const chunk = bundle[fileName];
|
|
236
|
-
if (chunk.type === 'asset' && !sriMap.has(fileName)) {
|
|
237
|
-
const hash = computeSri(chunk.source, options.algorithm);
|
|
238
|
-
if (hash) {
|
|
239
|
-
for (const path of getAllPossiblePaths(fileName)) {
|
|
240
|
-
sriMap.set(path, hash);
|
|
241
|
-
log(`Computing SRI for asset ${path}: ${hash}`, options);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
log('Final sriMap contents:', options);
|
|
248
|
-
for (const [key, value] of sriMap.entries()) {
|
|
249
|
-
log(`${key} => ${value}`, options);
|
|
250
|
-
}
|
|
251
|
-
},
|
|
252
|
-
|
|
253
|
-
async transformIndexHtml(html) {
|
|
204
|
+
async transformIndexHtml(html, ctx) {
|
|
254
205
|
if (!isBuild || !html) {
|
|
255
206
|
log('Skipping HTML transform in dev mode or empty HTML', options);
|
|
256
207
|
return html;
|
|
@@ -258,38 +209,32 @@ function sri(userOptions = {}) {
|
|
|
258
209
|
|
|
259
210
|
try {
|
|
260
211
|
log('Starting HTML transformation', options);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
212
|
+
const bundle = ctx.bundle || {};
|
|
213
|
+
log(`Bundle size: ${Object.keys(bundle).length}`, options);
|
|
214
|
+
|
|
215
|
+
// Process script tags with and without quotes
|
|
216
|
+
const scriptTagRegex = /<script[^>]+src=(?:["']([^"']+)["']|([^ >]+))[^>]*>/g;
|
|
217
|
+
let match;
|
|
218
|
+
while ((match = scriptTagRegex.exec(html)) !== null) {
|
|
219
|
+
const [tag, quotedUrl, unquotedUrl] = match;
|
|
220
|
+
const url = quotedUrl || unquotedUrl;
|
|
268
221
|
log(`Processing script: ${url}`, options);
|
|
269
|
-
const newTag = await processTag(tag, url, options,
|
|
222
|
+
const newTag = await processTag(tag, url, options, bundle, base);
|
|
270
223
|
html = html.replace(tag, newTag);
|
|
271
224
|
}
|
|
272
225
|
|
|
273
|
-
// Process
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
226
|
+
// Process link tags with and without quotes
|
|
227
|
+
const linkTagRegex = /<link[^>]+href=(?:["']([^"']+)["']|([^ >]+))[^>]*>/g;
|
|
228
|
+
while ((match = linkTagRegex.exec(html)) !== null) {
|
|
229
|
+
const [tag, quotedUrl, unquotedUrl] = match;
|
|
230
|
+
const url = quotedUrl || unquotedUrl;
|
|
231
|
+
if (tag.includes('stylesheet') || tag.includes('modulepreload')) {
|
|
232
|
+
log(`Processing link: ${url}`, options);
|
|
233
|
+
const newTag = await processTag(tag, url, options, bundle, base);
|
|
279
234
|
html = html.replace(tag, newTag);
|
|
280
235
|
}
|
|
281
236
|
}
|
|
282
237
|
|
|
283
|
-
// Process link tags
|
|
284
|
-
const linkTags = html.match(/<link[^>]+href=["']([^"']+)["'][^>]*>/g) || [];
|
|
285
|
-
log(`Found ${linkTags.length} link tags`, options);
|
|
286
|
-
for (const tag of linkTags) {
|
|
287
|
-
const url = tag.match(/href=["']([^"']+)["']/)[1];
|
|
288
|
-
log(`Processing link: ${url}`, options);
|
|
289
|
-
const newTag = await processTag(tag, url, options, sriMap);
|
|
290
|
-
html = html.replace(tag, newTag);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
238
|
return html;
|
|
294
239
|
} catch (error) {
|
|
295
240
|
console.error(`${LOG_PREFIX} Failed to transform HTML: ${error}`);
|