-
Notifications
You must be signed in to change notification settings - Fork 244
Expand file tree
/
Copy pathplugin-webAccessibleResources.ts
More file actions
296 lines (266 loc) · 11.4 KB
/
plugin-webAccessibleResources.ts
File metadata and controls
296 lines (266 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
import { OutputChunk } from 'rollup'
import {
Manifest as ViteManifest,
ResolvedConfig,
version as ViteVersion,
} from 'vite'
import { compileFileResources } from './compileFileResources'
import { contentScripts } from './contentScripts'
import { DYNAMIC_RESOURCE } from './defineManifest'
import {
getMatchPatternOrigin,
isResourceByMatch,
parseJsonAsset,
_debug,
} from './helpers'
import {
WebAccessibleResourceById,
WebAccessibleResourceByMatch,
} from './manifest'
import { getOptions } from './plugin-optionsProvider'
import type { CrxPluginFn, Browser } from './types'
const debug = _debug('web-acc-res')
export const pluginWebAccessibleResources: CrxPluginFn = () => {
let config: ResolvedConfig
let injectCss: boolean
let browser: Browser
let userWantsViteManifest: boolean | string | undefined
return [
{
name: 'crx:web-accessible-resources',
apply: 'serve',
enforce: 'post',
async config(config) {
const opts = await getOptions(config)
browser = opts.browser || 'chrome'
},
renderCrxManifest(manifest) {
// set default value for web_accessible_resources
manifest.web_accessible_resources =
manifest.web_accessible_resources ?? []
// remove dynamic resources placeholder
manifest.web_accessible_resources = manifest.web_accessible_resources
.map(({ resources, ...rest }) => ({
resources: resources.filter((r) => r !== DYNAMIC_RESOURCE),
...rest,
}))
.filter(({ resources }) => resources.length)
// during development don't specific resources
const war: WebAccessibleResourceByMatch = {
// all web origins can access
matches: ['<all_urls>'],
// all resources are web accessible
resources: ['**/*', '*'],
// change the extension origin on every reload
use_dynamic_url: false,
}
if (browser === 'firefox') {
// not allowed in FF b/c FF does this by default
delete war.use_dynamic_url
}
manifest.web_accessible_resources.push(war)
return manifest
},
},
{
name: 'crx:web-accessible-resources',
apply: 'build',
enforce: 'post',
async config({ build, ...config }, { command }) {
const opts = await getOptions(config)
const contentScripts = opts.contentScripts || {}
browser = opts.browser || 'chrome'
injectCss = contentScripts.injectCss ?? true
// Save the user's original manifest setting
userWantsViteManifest = build?.manifest
// Only force manifest generation if we're building - we need it to derive content script resources
return { ...config, build: { ...build, manifest: command === 'build' } }
},
configResolved(_config) {
config = _config
},
async renderCrxManifest(manifest, bundle) {
const { web_accessible_resources: _war = [] } = manifest
const dynamicScriptMatches = new Set<string>()
let dynamicScriptDynamicUrl = false
const web_accessible_resources: typeof _war = []
for (const r of _war) {
const i = r.resources.indexOf(DYNAMIC_RESOURCE)
if (i > -1 && isResourceByMatch(r)) {
r.resources = [...r.resources]
r.resources.splice(i, 1)
for (const p of r.matches) dynamicScriptMatches.add(p)
dynamicScriptDynamicUrl = r.use_dynamic_url ?? false
}
if (r.resources.length > 0) web_accessible_resources.push(r)
}
if (dynamicScriptMatches.size === 0) {
dynamicScriptMatches.add('http://*/*')
dynamicScriptMatches.add('https://*/*')
}
// derive content script resources from vite file manifest
if (contentScripts.size > 0) {
// Vite 5 changed the manifest.json location to .vite/manifest.json.
// In order to support both Vite <=4 and Vite 5, we need to check the Vite version and determine the path accordingly.
const viteMajorVersion = parseInt(ViteVersion.split('.')[0])
const manifestPath =
viteMajorVersion > 4 ? '.vite/manifest.json' : 'manifest.json'
const viteManifest = parseJsonAsset<ViteManifest>(
bundle,
manifestPath,
)
const viteFiles = new Map()
for (const [, file] of Object.entries(viteManifest))
viteFiles.set(file.file, file)
if (viteFiles.size === 0) return null
const bundleChunks = new Map<string, OutputChunk>()
for (const chunk of Object.values(bundle))
if (chunk.type === 'chunk') bundleChunks.set(chunk.fileName, chunk)
const moduleScriptResources = new Map<
string,
WebAccessibleResourceByMatch | WebAccessibleResourceById
>()
// multiple entries for each content script, dedupe by key === id
for (const [
key,
{ id, fileName, matches, type, isDynamicScript = false },
] of contentScripts)
if (key === id)
if (isDynamicScript || matches.length)
if (typeof fileName === 'undefined') {
throw new Error(
`Content script filename is undefined for "${id}"`,
)
} else {
const { assets, css, imports } = compileFileResources(
fileName,
{ chunks: bundleChunks, files: viteFiles, config },
)
// update content script resources for use by css plugin
contentScripts.get(key)!.css = [...css]
// loader files import the entry via chrome.runtime.getURL(),
// so the entry file must be web accessible.
// When no loader is emitted (simple scripts with no imports/exports),
// the entry is listed directly in content_scripts and injected by
// Chrome itself — it does not need to be web accessible.
// See: https://developer.chrome.com/docs/extensions/reference/manifest/web-accessible-resources
// "Content scripts themselves do not need to be allowed."
const script = contentScripts.get(key)!
const hasLoader = !!script.loaderName
if ((type === 'loader' && hasLoader) || isDynamicScript)
imports.add(fileName)
const resource:
| WebAccessibleResourceById
| WebAccessibleResourceByMatch = {
matches: isDynamicScript
? [...dynamicScriptMatches]
: matches,
resources: [...assets, ...imports],
use_dynamic_url: isDynamicScript
? dynamicScriptDynamicUrl
: false,
}
if (isDynamicScript || !injectCss) {
resource.resources.push(...css)
}
if (resource.resources.length)
if (type === 'module') {
// add conditionally after loaders and iife's
moduleScriptResources.set(fileName, resource)
} else {
resource.matches = resource.matches.map(
getMatchPatternOrigin,
)
web_accessible_resources.push(resource)
}
}
// now we know loader and iife resources, can handle modules
for (const r of web_accessible_resources)
if (isResourceByMatch(r))
// remove imported module scripts
for (const res of r.resources) moduleScriptResources.delete(res)
// add remaining top-level module imports (could be executed main world scripts)
web_accessible_resources.push(...moduleScriptResources.values())
}
/* ---------- COMBINE REDUNDANT RESOURCES ---------- */
const hashedResources = new Map<string, Set<string>>()
const combinedResources: typeof web_accessible_resources = []
for (const r of web_accessible_resources)
if (isResourceByMatch(r)) {
const { matches, resources, use_dynamic_url = false } = r
const key = JSON.stringify([use_dynamic_url, matches.sort()])
const combined = hashedResources.get(key) ?? new Set()
for (const res of resources) combined.add(res)
hashedResources.set(key, combined)
} else {
combinedResources.push(r)
}
for (const [key, resources] of hashedResources)
if (resources.size > 0) {
const [use_dynamic_url, matches]: [boolean, string[]] =
JSON.parse(key)
combinedResources.push({
matches,
resources: [...resources],
use_dynamic_url,
})
}
/* ------------- BROWSER COMPATIBILITY ------------- */
if (browser === 'firefox') {
for (const war of combinedResources) {
delete war.use_dynamic_url
}
}
/* -------------- INCLUDE SOURCEMAPS --------------- */
for (const war of combinedResources) {
const resourcesWithMaps = []
for (const res of war.resources) {
resourcesWithMaps.push(res)
if (bundle[res]?.type === 'chunk') {
const chunk = bundle[res] as OutputChunk & {
// OutputChunk.sourcemapFileName available in Vite >=5 (Rollup 3.29).
sourcemapFileName: string
}
if (chunk.map) {
const sourcemapFileName =
chunk.sourcemapFileName || `${chunk.fileName}.map`
// Vite 3 doesn't include source map files in the bundle object.
// To support both Vite 3 and Vite >=4, check the Vite version and fall back to checking the
// Vite config.
const viteMajorVersion = parseInt(ViteVersion.split('.')[0])
if (
sourcemapFileName in bundle ||
(viteMajorVersion < 4 && config.build.sourcemap == true)
) {
resourcesWithMaps.push(sourcemapFileName)
}
}
}
}
war.resources = resourcesWithMaps
}
/* --------------- CLEAN UP MANIFEST --------------- */
if (combinedResources.length === 0)
delete manifest.web_accessible_resources
else manifest.web_accessible_resources = combinedResources
// If the user didn't explicitly set build.manifest to true (i.e. it is disabled
// or left at its default), remove the Vite manifest from the bundle to keep the distribution clean
if (!userWantsViteManifest) {
// Vite 5+ uses .vite/manifest.json, older versions use manifest.json
const viteMajorVersion = parseInt(ViteVersion.split('.')[0])
const manifestPath =
viteMajorVersion > 4 ? '.vite/manifest.json' : 'manifest.json'
if (bundle[manifestPath]) {
debug(
'Removing Vite manifest: %s (userWantsViteManifest=%s)',
manifestPath,
userWantsViteManifest,
)
delete bundle[manifestPath]
}
}
return manifest
},
},
]
}