Skip to content

Instantly share code, notes, and snippets.

@boatilus
Created September 5, 2023 12:55
Show Gist options
  • Save boatilus/74bdffaccb9313c090f70cf5a7be6026 to your computer and use it in GitHub Desktop.
Save boatilus/74bdffaccb9313c090f70cf5a7be6026 to your computer and use it in GitHub Desktop.
const { parse, stringify } = require('css')
const { Compilation, sources } = require('webpack')
const pluginName = 'CssDedupeWebpackPlugin'
class CssDedupeWebpackPlugin {
apply(compiler) {
compiler.hooks.compilation.tap(pluginName, (compilation) => {
let buildManifest
let layoutCssChunkPath
compilation.hooks.processAssets.tap(
{
name: pluginName,
// The build manifest is created just before this phase.
stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE,
},
(assets) => {
const buildManifestSource = assets['app-build-manifest.json']
if (!buildManifestSource) {
return
}
buildManifest = JSON.parse(buildManifestSource.source())
const manifestLayoutPages = buildManifest.pages['/layout']
if (!manifestLayoutPages) {
return
}
const layoutCssChunks = manifestLayoutPages.filter((chunk) =>
chunk.endsWith('.css'),
)
if (layoutCssChunks.length < 1) {
console.error('no CSS chunk in layout')
return
}
if (layoutCssChunks.length > 1) {
console.error('multiple CSS chunks in layout; expected 1')
return
}
layoutCssChunkPath = layoutCssChunks[0]
console.info(`layout CSS chunk is: ${layoutCssChunkPath}`)
},
)
compilation.hooks.processAssets.tap(
{
name: pluginName,
stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE,
},
(assets) => {
const layoutCssChunk = assets[layoutCssChunkPath]
if (!layoutCssChunk) {
return
}
const layoutCssChunkSource = layoutCssChunk.source()
const { stylesheet: layoutStylesheet } = parse(layoutCssChunkSource)
const layoutClassnameRules = layoutStylesheet.rules
.filter((rule) => typeof rule.selectors !== 'undefined')
.flatMap((rule) => rule.selectors)
.filter((selector) => selector.startsWith('.'))
// Grab all CSS chunks not in the /layout page.
Object.entries(assets)
.filter(
([chunk]) =>
chunk.endsWith('.css') && !chunk.includes(layoutCssChunkPath),
)
.map(([path, source]) => {
const ast = parse(source.source())
// Remove any duplicate rules from the AST by the rule's selector.
ast.stylesheet.rules = ast.stylesheet.rules.filter((rule) => {
if (
typeof rule.selectors === 'undefined' ||
rule.selectors.length === 0
) {
return true
}
const selectors = rule.selectors.filter((selector) =>
layoutClassnameRules.includes(selector),
)
return selectors.length === 0
})
console.info(
`for ${path}, retained: ${ast.stylesheet.rules.length}`,
)
compilation.updateAsset(
path,
new sources.RawSource(stringify(ast)),
)
})
},
)
})
}
}
module.exports = CssDedupeWebpackPlugin
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment