vite-plugin-sri4 1.8.2 → 1.8.3
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 +58 -142
- package/dist/index.js +58 -142
- 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,9 +20,6 @@ function log(message, options) {
|
|
|
21
20
|
|
|
22
21
|
function computeSri(content, algorithm = 'sha384') {
|
|
23
22
|
try {
|
|
24
|
-
if (typeof content === 'string') {
|
|
25
|
-
content = Buffer.from(content, 'utf-8');
|
|
26
|
-
}
|
|
27
23
|
const hash = node_crypto.createHash(algorithm)
|
|
28
24
|
.update(content)
|
|
29
25
|
.digest('base64');
|
|
@@ -53,21 +49,14 @@ async function externalResourceIsCorsEnabled(url, options) {
|
|
|
53
49
|
function isBypassDomain(url, bypassDomains = []) {
|
|
54
50
|
if (!bypassDomains.length) return false;
|
|
55
51
|
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
|
-
|
|
52
|
+
const parsedUrl = url.startsWith('//')
|
|
53
|
+
? new URL(`http:${url}`)
|
|
54
|
+
: new URL(url, 'http://dummy');
|
|
65
55
|
return bypassDomains.some(
|
|
66
56
|
(domain) => parsedUrl.hostname === domain ||
|
|
67
57
|
parsedUrl.hostname.endsWith(`.${domain}`)
|
|
68
58
|
);
|
|
69
59
|
} catch (e) {
|
|
70
|
-
console.error(`${LOG_PREFIX} Failed to parse URL: ${url}`, e);
|
|
71
60
|
return false;
|
|
72
61
|
}
|
|
73
62
|
}
|
|
@@ -76,38 +65,27 @@ function hasCrossOriginAttr(tag) {
|
|
|
76
65
|
return /(?:^|\s)crossorigin(?:=["']?[^"'\s>]*["']?)?(?:\s|>|$)/i.test(tag);
|
|
77
66
|
}
|
|
78
67
|
|
|
79
|
-
function
|
|
80
|
-
|
|
68
|
+
function getBundleKey(url, base = '') {
|
|
69
|
+
// 嘗試各種可能的路徑格式
|
|
70
|
+
const possiblePaths = [
|
|
81
71
|
url,
|
|
82
|
-
url.
|
|
83
|
-
url.startsWith('/static/') ? url.slice(8) : url,
|
|
84
|
-
!url.startsWith('/static/') ? `static/${url}` : url,
|
|
85
|
-
!url.startsWith('/static/') ? `/static/${url}` : url,
|
|
72
|
+
url.replace(/^\//, ''),
|
|
86
73
|
url.replace(/^\/static\//, ''),
|
|
87
|
-
url.replace(/^static\//, '')
|
|
74
|
+
url.replace(/^static\//, ''),
|
|
75
|
+
url.replace(base, ''),
|
|
76
|
+
url.replace(base, '').replace(/^\//, '')
|
|
88
77
|
];
|
|
89
78
|
|
|
90
|
-
//
|
|
91
|
-
const withoutHash = url.replace(/-[a-zA-Z0-9]
|
|
79
|
+
// 移除 hash 後的版本
|
|
80
|
+
const withoutHash = url.replace(/-[a-zA-Z0-9]+\.([^.]+)$/, '.$1');
|
|
92
81
|
if (withoutHash !== url) {
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Handle variations with and without /static/ prefix for hashed files
|
|
97
|
-
const hashedMatch = url.match(/^(?:\/static\/)?(.*?)-[a-zA-Z0-9]{8}(\..*?)$/);
|
|
98
|
-
if (hashedMatch) {
|
|
99
|
-
const [, base, ext] = hashedMatch;
|
|
100
|
-
paths.push(
|
|
101
|
-
`${base}${ext}`,
|
|
102
|
-
`/static/${base}${ext}`,
|
|
103
|
-
`static/${base}${ext}`
|
|
104
|
-
);
|
|
82
|
+
possiblePaths.push(...getBundleKey(withoutHash, base));
|
|
105
83
|
}
|
|
106
84
|
|
|
107
|
-
return [...new Set(
|
|
85
|
+
return [...new Set(possiblePaths)];
|
|
108
86
|
}
|
|
109
87
|
|
|
110
|
-
async function processTag(tag, url, options,
|
|
88
|
+
async function processTag(tag, url, options, bundle, base = '') {
|
|
111
89
|
if (tag.includes('integrity=')) {
|
|
112
90
|
log(`Skip tag with existing integrity attribute: ${tag}`, options);
|
|
113
91
|
return tag;
|
|
@@ -148,54 +126,31 @@ async function processTag(tag, url, options, sriMap) {
|
|
|
148
126
|
}
|
|
149
127
|
|
|
150
128
|
// Handle local resources
|
|
151
|
-
const
|
|
152
|
-
let
|
|
153
|
-
|
|
154
|
-
log(`Checking possible paths for ${url}:`, options);
|
|
155
|
-
log(`Possible paths:`, options);
|
|
156
|
-
possiblePaths.forEach(path => log(` - ${path}`, options));
|
|
129
|
+
const possibleKeys = getBundleKey(url, base);
|
|
130
|
+
let bundleItem = null;
|
|
157
131
|
|
|
158
|
-
for (const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
log(`Found integrity for path ${path}: ${integrity}`, options);
|
|
132
|
+
for (const key of possibleKeys) {
|
|
133
|
+
if (bundle[key]) {
|
|
134
|
+
bundleItem = bundle[key];
|
|
135
|
+
log(`Found bundle item for key: ${key}`, options);
|
|
163
136
|
break;
|
|
164
137
|
}
|
|
165
138
|
}
|
|
166
139
|
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async function processInlineScript(tag, options) {
|
|
182
|
-
if (tag.includes('integrity=')) {
|
|
183
|
-
log(`Skip inline script with existing integrity attribute: ${tag}`, options);
|
|
184
|
-
return tag;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const content = tag.match(/<script[^>]*>([\s\S]*?)<\/script>/)?.[1]?.trim();
|
|
188
|
-
if (!content) {
|
|
189
|
-
log(`Skip empty inline script: ${tag}`, options);
|
|
190
|
-
return tag;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const hash = computeSri(content, options.algorithm);
|
|
194
|
-
if (hash) {
|
|
195
|
-
log(`Computing SRI for inline script: ${hash}`, options);
|
|
196
|
-
const newTag = tag.replace('>', ` integrity="${hash}">`);
|
|
197
|
-
log(`New inline script tag: ${newTag}`, options);
|
|
198
|
-
return newTag;
|
|
140
|
+
if (bundleItem) {
|
|
141
|
+
const source = bundleItem.type === 'chunk' ? bundleItem.code : bundleItem.source;
|
|
142
|
+
const integrity = computeSri(source, options.algorithm);
|
|
143
|
+
if (integrity) {
|
|
144
|
+
log(`Computing SRI for local resource ${url}: ${integrity}`, options);
|
|
145
|
+
const hasCrossOrigin = hasCrossOriginAttr(tag);
|
|
146
|
+
const crossOriginAttr = hasCrossOrigin ? '' : ` crossorigin="${options.crossorigin}"`;
|
|
147
|
+
const newTag = tag.replace(/>$/, ` integrity="${integrity}"${crossOriginAttr}>`);
|
|
148
|
+
log(`New tag: ${newTag}`, options);
|
|
149
|
+
return newTag;
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
log(`No bundle item found for ${url}`, options);
|
|
153
|
+
log(`Available bundle keys: ${Object.keys(bundle).join(', ')}`, options);
|
|
199
154
|
}
|
|
200
155
|
|
|
201
156
|
return tag;
|
|
@@ -203,8 +158,8 @@ async function processInlineScript(tag, options) {
|
|
|
203
158
|
|
|
204
159
|
function sri(userOptions = {}) {
|
|
205
160
|
const options = { ...DEFAULT_OPTIONS, ...userOptions };
|
|
206
|
-
const sriMap = new Map();
|
|
207
161
|
let isBuild = false;
|
|
162
|
+
let base = '';
|
|
208
163
|
|
|
209
164
|
return {
|
|
210
165
|
name: 'vite-plugin-sri4',
|
|
@@ -214,45 +169,12 @@ function sri(userOptions = {}) {
|
|
|
214
169
|
configResolved(config) {
|
|
215
170
|
options.domain = config.server?.host || '';
|
|
216
171
|
isBuild = config.command === 'build';
|
|
172
|
+
base = config.base || '';
|
|
217
173
|
log('Plugin configured in ' + (isBuild ? 'build' : 'dev') + ' mode', options);
|
|
174
|
+
log(`Base URL: ${base}`, options);
|
|
218
175
|
},
|
|
219
176
|
|
|
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) {
|
|
177
|
+
async transformIndexHtml(html, ctx) {
|
|
256
178
|
if (!isBuild || !html) {
|
|
257
179
|
log('Skipping HTML transform in dev mode or empty HTML', options);
|
|
258
180
|
return html;
|
|
@@ -260,38 +182,32 @@ function sri(userOptions = {}) {
|
|
|
260
182
|
|
|
261
183
|
try {
|
|
262
184
|
log('Starting HTML transformation', options);
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
185
|
+
const bundle = ctx.bundle || {};
|
|
186
|
+
log(`Bundle size: ${Object.keys(bundle).length}`, options);
|
|
187
|
+
|
|
188
|
+
// Process script tags with and without quotes
|
|
189
|
+
const scriptTagRegex = /<script[^>]+src=(?:["']([^"']+)["']|([^ >]+))[^>]*>/g;
|
|
190
|
+
let match;
|
|
191
|
+
while ((match = scriptTagRegex.exec(html)) !== null) {
|
|
192
|
+
const [tag, quotedUrl, unquotedUrl] = match;
|
|
193
|
+
const url = quotedUrl || unquotedUrl;
|
|
270
194
|
log(`Processing script: ${url}`, options);
|
|
271
|
-
const newTag = await processTag(tag, url, options,
|
|
195
|
+
const newTag = await processTag(tag, url, options, bundle, base);
|
|
272
196
|
html = html.replace(tag, newTag);
|
|
273
197
|
}
|
|
274
198
|
|
|
275
|
-
// Process
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
199
|
+
// Process link tags with and without quotes
|
|
200
|
+
const linkTagRegex = /<link[^>]+href=(?:["']([^"']+)["']|([^ >]+))[^>]*>/g;
|
|
201
|
+
while ((match = linkTagRegex.exec(html)) !== null) {
|
|
202
|
+
const [tag, quotedUrl, unquotedUrl] = match;
|
|
203
|
+
const url = quotedUrl || unquotedUrl;
|
|
204
|
+
if (tag.includes('stylesheet') || tag.includes('modulepreload')) {
|
|
205
|
+
log(`Processing link: ${url}`, options);
|
|
206
|
+
const newTag = await processTag(tag, url, options, bundle, base);
|
|
281
207
|
html = html.replace(tag, newTag);
|
|
282
208
|
}
|
|
283
209
|
}
|
|
284
210
|
|
|
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
211
|
return html;
|
|
296
212
|
} catch (error) {
|
|
297
213
|
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,9 +18,6 @@ function log(message, options) {
|
|
|
19
18
|
|
|
20
19
|
function computeSri(content, algorithm = 'sha384') {
|
|
21
20
|
try {
|
|
22
|
-
if (typeof content === 'string') {
|
|
23
|
-
content = Buffer.from(content, 'utf-8');
|
|
24
|
-
}
|
|
25
21
|
const hash = createHash(algorithm)
|
|
26
22
|
.update(content)
|
|
27
23
|
.digest('base64');
|
|
@@ -51,21 +47,14 @@ async function externalResourceIsCorsEnabled(url, options) {
|
|
|
51
47
|
function isBypassDomain(url, bypassDomains = []) {
|
|
52
48
|
if (!bypassDomains.length) return false;
|
|
53
49
|
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
|
-
|
|
50
|
+
const parsedUrl = url.startsWith('//')
|
|
51
|
+
? new URL(`http:${url}`)
|
|
52
|
+
: new URL(url, 'http://dummy');
|
|
63
53
|
return bypassDomains.some(
|
|
64
54
|
(domain) => parsedUrl.hostname === domain ||
|
|
65
55
|
parsedUrl.hostname.endsWith(`.${domain}`)
|
|
66
56
|
);
|
|
67
57
|
} catch (e) {
|
|
68
|
-
console.error(`${LOG_PREFIX} Failed to parse URL: ${url}`, e);
|
|
69
58
|
return false;
|
|
70
59
|
}
|
|
71
60
|
}
|
|
@@ -74,38 +63,27 @@ function hasCrossOriginAttr(tag) {
|
|
|
74
63
|
return /(?:^|\s)crossorigin(?:=["']?[^"'\s>]*["']?)?(?:\s|>|$)/i.test(tag);
|
|
75
64
|
}
|
|
76
65
|
|
|
77
|
-
function
|
|
78
|
-
|
|
66
|
+
function getBundleKey(url, base = '') {
|
|
67
|
+
// 嘗試各種可能的路徑格式
|
|
68
|
+
const possiblePaths = [
|
|
79
69
|
url,
|
|
80
|
-
url.
|
|
81
|
-
url.startsWith('/static/') ? url.slice(8) : url,
|
|
82
|
-
!url.startsWith('/static/') ? `static/${url}` : url,
|
|
83
|
-
!url.startsWith('/static/') ? `/static/${url}` : url,
|
|
70
|
+
url.replace(/^\//, ''),
|
|
84
71
|
url.replace(/^\/static\//, ''),
|
|
85
|
-
url.replace(/^static\//, '')
|
|
72
|
+
url.replace(/^static\//, ''),
|
|
73
|
+
url.replace(base, ''),
|
|
74
|
+
url.replace(base, '').replace(/^\//, '')
|
|
86
75
|
];
|
|
87
76
|
|
|
88
|
-
//
|
|
89
|
-
const withoutHash = url.replace(/-[a-zA-Z0-9]
|
|
77
|
+
// 移除 hash 後的版本
|
|
78
|
+
const withoutHash = url.replace(/-[a-zA-Z0-9]+\.([^.]+)$/, '.$1');
|
|
90
79
|
if (withoutHash !== url) {
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Handle variations with and without /static/ prefix for hashed files
|
|
95
|
-
const hashedMatch = url.match(/^(?:\/static\/)?(.*?)-[a-zA-Z0-9]{8}(\..*?)$/);
|
|
96
|
-
if (hashedMatch) {
|
|
97
|
-
const [, base, ext] = hashedMatch;
|
|
98
|
-
paths.push(
|
|
99
|
-
`${base}${ext}`,
|
|
100
|
-
`/static/${base}${ext}`,
|
|
101
|
-
`static/${base}${ext}`
|
|
102
|
-
);
|
|
80
|
+
possiblePaths.push(...getBundleKey(withoutHash, base));
|
|
103
81
|
}
|
|
104
82
|
|
|
105
|
-
return [...new Set(
|
|
83
|
+
return [...new Set(possiblePaths)];
|
|
106
84
|
}
|
|
107
85
|
|
|
108
|
-
async function processTag(tag, url, options,
|
|
86
|
+
async function processTag(tag, url, options, bundle, base = '') {
|
|
109
87
|
if (tag.includes('integrity=')) {
|
|
110
88
|
log(`Skip tag with existing integrity attribute: ${tag}`, options);
|
|
111
89
|
return tag;
|
|
@@ -146,54 +124,31 @@ async function processTag(tag, url, options, sriMap) {
|
|
|
146
124
|
}
|
|
147
125
|
|
|
148
126
|
// Handle local resources
|
|
149
|
-
const
|
|
150
|
-
let
|
|
151
|
-
|
|
152
|
-
log(`Checking possible paths for ${url}:`, options);
|
|
153
|
-
log(`Possible paths:`, options);
|
|
154
|
-
possiblePaths.forEach(path => log(` - ${path}`, options));
|
|
127
|
+
const possibleKeys = getBundleKey(url, base);
|
|
128
|
+
let bundleItem = null;
|
|
155
129
|
|
|
156
|
-
for (const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
log(`Found integrity for path ${path}: ${integrity}`, options);
|
|
130
|
+
for (const key of possibleKeys) {
|
|
131
|
+
if (bundle[key]) {
|
|
132
|
+
bundleItem = bundle[key];
|
|
133
|
+
log(`Found bundle item for key: ${key}`, options);
|
|
161
134
|
break;
|
|
162
135
|
}
|
|
163
136
|
}
|
|
164
137
|
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async function processInlineScript(tag, options) {
|
|
180
|
-
if (tag.includes('integrity=')) {
|
|
181
|
-
log(`Skip inline script with existing integrity attribute: ${tag}`, options);
|
|
182
|
-
return tag;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const content = tag.match(/<script[^>]*>([\s\S]*?)<\/script>/)?.[1]?.trim();
|
|
186
|
-
if (!content) {
|
|
187
|
-
log(`Skip empty inline script: ${tag}`, options);
|
|
188
|
-
return tag;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const hash = computeSri(content, options.algorithm);
|
|
192
|
-
if (hash) {
|
|
193
|
-
log(`Computing SRI for inline script: ${hash}`, options);
|
|
194
|
-
const newTag = tag.replace('>', ` integrity="${hash}">`);
|
|
195
|
-
log(`New inline script tag: ${newTag}`, options);
|
|
196
|
-
return newTag;
|
|
138
|
+
if (bundleItem) {
|
|
139
|
+
const source = bundleItem.type === 'chunk' ? bundleItem.code : bundleItem.source;
|
|
140
|
+
const integrity = computeSri(source, options.algorithm);
|
|
141
|
+
if (integrity) {
|
|
142
|
+
log(`Computing SRI for local resource ${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
|
+
} else {
|
|
150
|
+
log(`No bundle item found for ${url}`, options);
|
|
151
|
+
log(`Available bundle keys: ${Object.keys(bundle).join(', ')}`, options);
|
|
197
152
|
}
|
|
198
153
|
|
|
199
154
|
return tag;
|
|
@@ -201,8 +156,8 @@ async function processInlineScript(tag, options) {
|
|
|
201
156
|
|
|
202
157
|
function sri(userOptions = {}) {
|
|
203
158
|
const options = { ...DEFAULT_OPTIONS, ...userOptions };
|
|
204
|
-
const sriMap = new Map();
|
|
205
159
|
let isBuild = false;
|
|
160
|
+
let base = '';
|
|
206
161
|
|
|
207
162
|
return {
|
|
208
163
|
name: 'vite-plugin-sri4',
|
|
@@ -212,45 +167,12 @@ function sri(userOptions = {}) {
|
|
|
212
167
|
configResolved(config) {
|
|
213
168
|
options.domain = config.server?.host || '';
|
|
214
169
|
isBuild = config.command === 'build';
|
|
170
|
+
base = config.base || '';
|
|
215
171
|
log('Plugin configured in ' + (isBuild ? 'build' : 'dev') + ' mode', options);
|
|
172
|
+
log(`Base URL: ${base}`, options);
|
|
216
173
|
},
|
|
217
174
|
|
|
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) {
|
|
175
|
+
async transformIndexHtml(html, ctx) {
|
|
254
176
|
if (!isBuild || !html) {
|
|
255
177
|
log('Skipping HTML transform in dev mode or empty HTML', options);
|
|
256
178
|
return html;
|
|
@@ -258,38 +180,32 @@ function sri(userOptions = {}) {
|
|
|
258
180
|
|
|
259
181
|
try {
|
|
260
182
|
log('Starting HTML transformation', options);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
183
|
+
const bundle = ctx.bundle || {};
|
|
184
|
+
log(`Bundle size: ${Object.keys(bundle).length}`, options);
|
|
185
|
+
|
|
186
|
+
// Process script tags with and without quotes
|
|
187
|
+
const scriptTagRegex = /<script[^>]+src=(?:["']([^"']+)["']|([^ >]+))[^>]*>/g;
|
|
188
|
+
let match;
|
|
189
|
+
while ((match = scriptTagRegex.exec(html)) !== null) {
|
|
190
|
+
const [tag, quotedUrl, unquotedUrl] = match;
|
|
191
|
+
const url = quotedUrl || unquotedUrl;
|
|
268
192
|
log(`Processing script: ${url}`, options);
|
|
269
|
-
const newTag = await processTag(tag, url, options,
|
|
193
|
+
const newTag = await processTag(tag, url, options, bundle, base);
|
|
270
194
|
html = html.replace(tag, newTag);
|
|
271
195
|
}
|
|
272
196
|
|
|
273
|
-
// Process
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
197
|
+
// Process link tags with and without quotes
|
|
198
|
+
const linkTagRegex = /<link[^>]+href=(?:["']([^"']+)["']|([^ >]+))[^>]*>/g;
|
|
199
|
+
while ((match = linkTagRegex.exec(html)) !== null) {
|
|
200
|
+
const [tag, quotedUrl, unquotedUrl] = match;
|
|
201
|
+
const url = quotedUrl || unquotedUrl;
|
|
202
|
+
if (tag.includes('stylesheet') || tag.includes('modulepreload')) {
|
|
203
|
+
log(`Processing link: ${url}`, options);
|
|
204
|
+
const newTag = await processTag(tag, url, options, bundle, base);
|
|
279
205
|
html = html.replace(tag, newTag);
|
|
280
206
|
}
|
|
281
207
|
}
|
|
282
208
|
|
|
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
209
|
return html;
|
|
294
210
|
} catch (error) {
|
|
295
211
|
console.error(`${LOG_PREFIX} Failed to transform HTML: ${error}`);
|