webpack-target-webextension
Version:
WebExtension plugin for Webpack. Supports code-splitting and dynamic import.
136 lines (126 loc) • 5.84 kB
JavaScript
// @ts-check
const createEagerlyLoadChunksRuntimeModule = require('./RuntimeModules/EagerlyLoadChunks.js')
module.exports = class WebExtensionServiceWorkerEntryPlugin {
/**
* @param {import('../../index.d.ts').BackgroundOptions} options
* @param {boolean} backgroundOutputEmitted
*/
constructor(options, backgroundOutputEmitted) {
this.options = options
this.backgroundOutputEmitted = backgroundOutputEmitted
}
/** @param {import('webpack').Compiler} compiler */
apply(compiler) {
const { javascript, sources } = compiler.webpack
const entry = this.options.serviceWorkerEntry
if (entry === undefined) return
if (!(compiler.options.output.module || compiler.options.experiments.outputModule)) {
const hook = compiler.hooks.entryOption
// Set chunkLoading to import-scripts
hook.tap(WebExtensionServiceWorkerEntryPlugin.name, (context, entries) => {
if (typeof entries === 'function') {
if (this.options.noDynamicEntryWarning) return
console.warn(`[webpack-extension-target] Dynamic entry points not supported yet.
You must manually set the chuck loading of entry point ${entry} to "import-scripts".
See https://webpack.js.org/configuration/entry-context/#entry-descriptor
Set background.noDynamicEntryWarning to true to disable this warning.
`)
}
/** @type {import('@rspack/core').EntryDescriptionNormalized} */
const selectedEntry = /** @type {any} */ (entries)[entry]
if (!selectedEntry) throw new Error(`[webpack-extension-target] There is no entry called ${entry}.`)
selectedEntry.chunkLoading = 'import-scripts'
})
}
// Set all lazy chunks to eagerly loaded
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1198822
if (this.options.eagerChunkLoading !== false) {
compiler.hooks.compilation.tap(WebExtensionServiceWorkerEntryPlugin.name, (compilation) => {
compilation.hooks.afterOptimizeChunkIds?.tap(WebExtensionServiceWorkerEntryPlugin.name, () => {
const entryPoint = compilation.entrypoints.get(entry)
if (!entryPoint) return
const entryChunk = entryPoint.getEntrypointChunk()
const reachableChunks = getReachableChunks(entryPoint, new Set(entryPoint.chunks))
const reachableChunkIds = new Set([...reachableChunks].map((x) => x.id))
for (const id of getInitialChunkIds(entryChunk, compilation.chunkGraph, chunkHasJs)) {
reachableChunkIds.delete(id)
}
if (reachableChunkIds.size) {
const EagerlyLoadChunksRuntimeModule = createEagerlyLoadChunksRuntimeModule(compiler.webpack)
compilation.hooks.additionalTreeRuntimeRequirements.tap(
EagerlyLoadChunksRuntimeModule.name,
(chunk, set) => {
if (chunk.id !== entryChunk.id) return
set.add(compiler.webpack.RuntimeGlobals.ensureChunkHandlers)
compilation.addRuntimeModule(entryChunk, new EagerlyLoadChunksRuntimeModule([...reachableChunkIds]))
},
)
}
})
})
}
if (
this.options.tryCatchWrapper !== false &&
compiler.options.output.chunkFormat !== 'module' &&
!this.backgroundOutputEmitted
) {
compiler.hooks.compilation.tap(WebExtensionServiceWorkerEntryPlugin.name, (compilation) => {
const hooks = javascript.JavascriptModulesPlugin.getCompilationHooks(compilation)
if (hooks.renderContent) {
// rspack don't have it
hooks.renderContent.tap(WebExtensionServiceWorkerEntryPlugin.name, (source, context) => {
const entryPoint = compilation.entrypoints.get(entry)
const entryChunk = entryPoint?.getEntrypointChunk()
if (entryChunk !== context.chunk) return source
return new sources.ConcatSource(
'/******/ try { // If the initial code of the serviceWorkerEntry throws, the console cannot be opened.\n',
source,
'\n/******/ } catch (e) {\n/******/ Promise.reject(e);\n/******/ }',
)
})
}
})
}
}
}
// webpack/lib/javascript/StartupHelpers.js
/**
* @param {import('webpack').Chunk} chunk the chunk
* @param {import('webpack').ChunkGraph} chunkGraph the chunk graph
* @param {function(import('webpack').Chunk, import('webpack').ChunkGraph): boolean} filterFn filter function
* @returns {Set<number | string>} initially fulfilled chunk ids
*/
function getInitialChunkIds(chunk, chunkGraph, filterFn) {
const initialChunkIds = new Set(chunk.ids)
for (const c of chunk.getAllInitialChunks()) {
if (c === chunk || filterFn(c, chunkGraph) || !c.ids) continue
for (const id of c.ids) initialChunkIds.add(id)
}
return initialChunkIds
}
// webpack/lib/javascript/JavascriptModulesPlugin.js
/**
* @param {import('webpack').Chunk} chunk a chunk
* @param {import('webpack').ChunkGraph} chunkGraph the chunk graph
* @returns {boolean} true, when a JS file is needed for this chunk
*/
function chunkHasJs(chunk, chunkGraph) {
if (chunkGraph.getNumberOfEntryModules(chunk) > 0) return true
return !!chunkGraph.getChunkModulesIterableBySourceType(chunk, 'javascript')
}
/**
* @param {import('webpack').ChunkGroup} chunkGroup
* @param {Set<import('webpack').Chunk>} reachableChunks
* @param {Set<import('webpack').ChunkGroup>} visitedChunkGroups
*/
function getReachableChunks(chunkGroup, reachableChunks = new Set(), visitedChunkGroups = new Set()) {
for (const x of chunkGroup.getChildren()) {
if (visitedChunkGroups.has(x)) continue
else {
visitedChunkGroups.add(x)
x.chunks.forEach((x) => reachableChunks.add(x))
getReachableChunks(x, reachableChunks, visitedChunkGroups)
}
}
return reachableChunks
}