Created
April 22, 2021 15:07
-
-
Save kadamwhite/8372f122eae1a801c6e66ba740c6c3a2 to your computer and use it in GitHub Desktop.
A Webpack plugin to remove media queries matching certain patterns from a generated CSS file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This file defines the project-level Webpack production configuration. It | |
* combines configs from all relevant plugins and themes into one single | |
* array (a "multi-configuration" Webpack setup) and runs those builds in | |
* a single pass. | |
*/ | |
const { basename, extname } = require( 'path' ); | |
const postcss = require( 'postcss' ); | |
const filtermq = require( 'postcss-filter-mq' ); | |
const webpack = require( 'webpack' ); | |
const { RawSource } = webpack.sources || require( 'webpack-sources' ); | |
/** | |
* Peel apart media query-specific CSS into different sheets. | |
*/ | |
class ExcludeMediaQueriesPlugin { | |
static name = 'ExcludeMediaQueriesPlugin'; | |
constructor( options = {} ) { | |
this.options = options; | |
} | |
/** | |
* Get the value of a passed plugin option, or fall back to a default. | |
* | |
* @param {string} option Name of option to retrieve. | |
* @param {*} fallback Default value to return if no option was provided. | |
* @returns {*} Value of option, or fallback value. | |
*/ | |
getOption( option, fallback ) { | |
if ( this.options[ option ] !== undefined ) { | |
return this.options[ option ]; | |
} | |
return fallback; | |
} | |
/** | |
* Match whether to apply the plugin to a specific file. | |
* | |
* @param {Function|RegExp|string} fileName A function, RE, or string to use to test file names. | |
* @returns {boolean} Whether the provided file should be processed by this plugin. | |
*/ | |
matchFile( fileName ) { | |
if ( typeof this.options.file === 'function' ) { | |
return this.options.file( fileName ); | |
} | |
if ( this.options.file instanceof RegExp ) { | |
return this.options.file.test( fileName ); | |
} | |
return this.options.file === fileName; | |
} | |
/** | |
* Convert an input file path to the desired output file path. | |
* | |
* @param {string} fileName A (hopefully absolute) file system path. | |
* @returns {string} The filename to which to output. | |
*/ | |
getOutputFilePath( fileName ) { | |
if ( typeof this.options.outputFile === 'function' ) { | |
return this.options.outputFile( fileName ); | |
} | |
if ( typeof this.options.outputFile === 'string' ) { | |
// Assume abs path. | |
return this.options.outputFile; | |
} | |
// Replace. | |
return fileName; | |
} | |
/** | |
* Given a string of CSS, return an array of numeric-value media query objects. | |
* | |
* @param {string} content Rendered CSS stylesheet content. | |
* @returns {object[]} Array of { query, value, unit } objects. | |
*/ | |
getMediaQueriesFromStylesheet( content ) { | |
// This works best when the CSS is minified. | |
return content | |
.split( /{|}/ ) | |
.filter( ( str ) => /@media/.test( str ) ) | |
// Match anything of the format `@media (condition: {numeric value}{unit?})`. | |
// Does not currently support multi-condition media queries. | |
.map( ( css ) => css.match( /\(\s*(\S+)\s*:\s*(\d+)(\D*?)\s*\)/ ) ) | |
.reduce( | |
( uniqueQueries, match ) => { | |
if ( match ) { | |
const [ , query, value, unit ] = match; | |
const matchingQuery = uniqueQueries.find( ( mq ) => ( | |
mq.query === query && mq.value === +value && mq.unit === unit | |
) ); | |
if ( ! matchingQuery ) { | |
return uniqueQueries.concat( { | |
query, | |
unit, | |
value: +value, | |
} ); | |
} | |
} | |
return uniqueQueries; | |
}, | |
[] | |
); | |
} | |
/** | |
* Given a string of CSS and, optionally, a `match` option which will filter | |
* the list of queries which get matched by the filtermq plugin, and return | |
* a regular expression we can pass to filtermq. | |
* | |
* @param {string} css Full CSS of output file to filter. | |
* @returns {RegExp} Regular expression for matching `@media` rules. | |
*/ | |
getMatchingQueryPattern( css ) { | |
let mqs = this.getMediaQueriesFromStylesheet( css ); | |
if ( typeof this.options.match === 'function' ) { | |
mqs = mqs.filter( this.options.match ); | |
} | |
return new RegExp( | |
mqs.map( ( { query, value, unit } ) => `${ query }\\s*:\\s*${ value }${ unit }` ).join( '|' ), | |
'i' | |
); | |
} | |
/** | |
* Instantiate and return the filtermq plugin instance. | |
* | |
* @param {string} css Input CSS. | |
* @returns {postcss.Transformer} FilterMQ PostCSS plugin object (Transformer). | |
*/ | |
getFilterPlugin( css ) { | |
return filtermq( { | |
regex: this.getMatchingQueryPattern( css ), | |
keepBaseRules: this.getOption( 'keepBaseRules', true ), | |
invert: this.getOption( 'invert', true ), | |
} ); | |
} | |
/** | |
* Bind plugin logic to the Webpack compilation cycle. | |
* | |
* @param {webpack.Compiler} compiler Webpack Compiler to which to apply the plugin. | |
*/ | |
apply( compiler ) { | |
compiler.hooks.emit.tapPromise( | |
ExcludeMediaQueriesPlugin.name, | |
( compilation ) => { | |
// Figure out which asset in the compilation we are reading in. | |
const fileName = Object.keys( compilation.assets ) | |
.find( ( fileName ) => this.matchFile( fileName ) ); | |
const outputFileName = this.getOutputFilePath( fileName ); | |
const css = compilation.assets[ fileName ].source(); | |
return postcss( [ this.getFilterPlugin( css ) ] ) | |
.process( css, { | |
from: fileName, | |
to: outputFileName, | |
} ) | |
.then( ( result ) => { | |
const source = new RawSource( result.css ); | |
if ( outputFileName === fileName ) { | |
compilation.updateAsset( outputFileName, source ); | |
} else { | |
compilation.emitAsset( outputFileName, source, { | |
name: basename( outputFileName ), | |
immutable: true, | |
} ); | |
// Spoof a chunk to make the file get output with the correct | |
// "name" in any generated manifest. | |
const name = this.getOption( 'name', basename( outputFileName ).replace( extname( outputFileName ), '' ) ); | |
const newFileChunk = compilation.addChunk( name ); | |
newFileChunk.chunkReason = ExcludeMediaQueriesPlugin.name; | |
newFileChunk.files = [ outputFileName ]; | |
newFileChunk.id = name; | |
newFileChunk.ids = [ name ]; | |
} | |
} ); | |
} | |
); | |
} | |
} | |
module.exports = ExcludeMediaQueriesPlugin; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Add an instance of the plugin to your Webpack config's plugin array | |
// like so: | |
[ | |
new ExcludeMediaQueriesPlugin( { | |
name: 'amp', | |
file: /main.css/, | |
match: ( { query, value, unit } ) => { | |
if ( query === 'min-width' && unit === 'px' ) { | |
return value > 640; | |
} | |
return false; | |
}, | |
outputFile: ( name ) => name.replace( 'main.css', 'mobile-only.css' ), | |
} ), | |
] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment