SassNinja / media-query-plugin

Webpack plugin for media query extraction.
MIT License
205 stars 27 forks source link

htmlWebpackPlugin.files.extracted.css is always empty #15

Closed wzrdtales closed 5 years ago

wzrdtales commented 5 years ago

Hey there,

I am figuring out how to use your plugin. But I cannot get it working. It is not extracting anything at all. I finally decided to try to inject the css links manually in the template with a forEach on htmlWebpackPlugin.files.extracted.css. And the surprise, htmlWebpackPlugin.files.extracted.css is not defined at all. I am not exactly sure what is going wrong, so now I finally reach out here for help :)

So first of all my environment:

package.json

{
    "@babel/core": "^7.4.3",
    "@babel/plugin-proposal-class-properties": "^7.4.0",
    "@babel/plugin-proposal-decorators": "^7.4.0",
    "@babel/plugin-syntax-dynamic-import": "^7.2.0",
    "@babel/preset-env": "^7.4.3",
    "@babel/register": "^7.4.0",
    "aurelia-cli": "^1.0.0-beta.15",
    "aurelia-loader-nodejs": "^1.0.1",
    "aurelia-pal-nodejs": "^1.2.0",
    "aurelia-testing": "^1.0.0",
    "aurelia-tools": "^2.0.0",
    "aurelia-webpack-plugin": "^3.0.0",
    "autoprefixer": "^9.5.0",
    "babel-core": "7.0.0-bridge.0",
    "babel-eslint": "^10.0.1",
    "babel-jest": "^23.6.0",
    "babel-loader": "^8.0.5",
    "babel-preset-es2015": "^6.24.1",
    "copy-webpack-plugin": "^4.5.2",
    "critters-webpack-plugin": "^2.3.0",
    "css-loader": "^1.0.0",
    "cssnano": "^4.1.10",
    "del": "^3.0.0",
    "duplicate-package-checker-webpack-plugin": "^3.0.0",
    "eslint": "^5.16.0",
    "expose-loader": "^0.7.5",
    "file-loader": "^2.0.0",
    "gulp": "^4.0.0",
    "gulp-rename": "^1.4.0",
    "hammerjs": "^2.0.8",
    "html-loader": "^0.5.5",
    "html-webpack-plugin": "^3.2.0",
    "istanbul-instrumenter-loader": "^3.0.1",
    "jest": "^23.6.0",
    "jest-cli": "^23.6.0",
    "jquery": "^3.3.1",
    "json-loader": "^0.5.7",
    "link-media-html-webpack-plugin": "^2.0.0",
    "media-query-plugin": "^1.3.0",
    "media-query-splitting-plugin": "^1.1.13",
    "mini-css-extract-plugin": "^0.4.3",
    "minimatch": "^3.0.4",
    "moment": "^2.24.0",
    "nps": "^5.9.5",
    "nps-utils": "^1.7.0",
    "opn": "^5.5.0",
    "plugin-error": "^1.0.1",
    "postcss-color-function": "^4.1.0",
    "postcss-font-magician": "^2.2.1",
    "postcss-loader": "latest",
    "request": "^2.88.0",
    "request-promise": "^4.2.4",
    "slick-carousel": "^1.8.1",
    "style-loader": "^0.23.1",
    "through2": "^2.0.3",
    "url-loader": "^1.1.1",
    "vinyl-fs": "^3.0.3",
    "webpack": "^4.29.6",
    "webpack-bundle-analyzer": "^3.1.0",
    "webpack-cli": "^3.3.0",
    "webpack-dev-server": "^3.2.1"
}

from the config

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const Critters = require('critters-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const MediaQueryPlugin = require('media-query-plugin');
const DuplicatePackageCheckerPlugin = require('duplicate-package-checker-webpack-plugin');
const project = require('./aurelia_project/aurelia.json');
const {
  AureliaPlugin,
  ModuleDependenciesPlugin
} = require('aurelia-webpack-plugin');
const { ProvidePlugin } = require('webpack');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

// config helpers:
const ensureArray = config =>
  (config && (Array.isArray(config) ? config : [config])) || [];
const when = (condition, config, negativeConfig) =>
  condition ? ensureArray(config) : ensureArray(negativeConfig);

// primary config:
const title = 'Something';
const outDir = path.resolve(__dirname, project.platform.output);
const srcDir = path.resolve(__dirname, 'src');
const nodeModulesDir = path.resolve(__dirname, 'node_modules');
const baseUrl = '/';

const cssRules = [
  { loader: 'css-loader' },
  MediaQueryPlugin.loader,
  {
    loader: 'postcss-loader',
    options: {
      plugins: () => [
        require('autoprefixer')({ browsers: ['last 2 versions'] }),
        require('postcss-color-function'),
        require('cssnano')({ preset: 'default' })
      ]
    }
  }
]

module.exports = ({
  production,
  server,
  extractCss,
  coverage,
  analyze,
  karma
} = {}) => ({
  resolve: {
    extensions: ['.js'],
    modules: [srcDir, nodeModulesDir],
    // Enforce single aurelia-binding, to avoid v1/v2 duplication due to
    // out-of-date dependencies on 3rd party aurelia plugins
    alias: {
      'aurelia-binding': path.resolve(__dirname, 'node_modules/aurelia-binding')
    }
  },
  entry: {
    app: ['aurelia-bootstrapper']
  },
  mode: production ? 'production' : 'development',
  output: {
    path: outDir,
    publicPath: baseUrl,
    filename: production
      ? '[name].[chunkhash].bundle.js'
      : '[name].[hash].bundle.js',
    sourceMapFilename: production
      ? '[name].[chunkhash].bundle.map'
      : '[name].[hash].bundle.map',
    chunkFilename: production
      ? '[name].[chunkhash].chunk.js'
      : '[name].[hash].chunk.js'
  },
  optimization: {
    runtimeChunk: true, // separates the runtime chunk, required for long term cacheability
    // moduleIds is the replacement for HashedModuleIdsPlugin and NamedModulesPlugin deprecated in https://github.com/webpack/webpack/releases/tag/v4.16.0
    // changes module id's to use hashes be based on the relative path of the module, required for long term cacheability
    moduleIds: 'hashed',
    // Use splitChunks to breakdown the App/Aurelia bundle down into smaller chunks
    // https://webpack.js.org/plugins/split-chunks-plugin/
    splitChunks: {
      hidePathInfo: true, // prevents the path from being used in the filename when using maxSize
      chunks: 'initial',
      // sizes are compared against source before minification
      maxInitialRequests: Infinity, // Default is 3, make this unlimited if using HTTP/2
      maxAsyncRequests: Infinity, // Default is 5, make this unlimited if using HTTP/2
      minSize: 10000, // chunk is only created if it would be bigger than minSize, adjust as required
      maxSize: 40000, // splits chunks if bigger than 40k, adjust as required (maxSize added in webpack v4.15)
      cacheGroups: {
        default: false, // Disable the built-in groups default & vendors (vendors is redefined below)
        // You can insert additional cacheGroup entries here if you want to split out specific modules
        // This is required in order to split out vendor css from the app css when using --extractCss
        // For example to separate font-awesome and bootstrap:
        // fontawesome: { // separates font-awesome css from the app css (font-awesome is only css/fonts)
        //   name: 'vendor.font-awesome',
        //   test:  /[\\/]node_modules[\\/]font-awesome[\\/]/,
        //   priority: 100,
        //   enforce: true
        // },
        // bootstrap: { // separates bootstrap js from vendors and also bootstrap css from app css
        //   name: 'vendor.font-awesome',
        //   test:  /[\\/]node_modules[\\/]bootstrap[\\/]/,
        //   priority: 90,
        //   enforce: true
        // },

        // This is the HTTP/2 optimised cacheGroup configuration
        // generic 'initial/sync' vendor node module splits: separates out larger modules
        vendorSplit: {
          // each node module as separate chunk file if module is bigger than minSize
          test: /[\\/]node_modules[\\/]/,
          name(module) {
            // Extract the name of the package from the path segment after node_modules
            const packageName = module.context.match(
              /[\\/]node_modules[\\/](.*?)([\\/]|$)/
            )[1];
            return `vendor.${packageName.replace('@', '')}`;
          },
          priority: 20
        },
        vendors: {
          // picks up everything else being used from node_modules that is less than minSize
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 19,
          enforce: true // create chunk regardless of the size of the chunk
        },
        // generic 'async' vendor node module splits: separates out larger modules
        vendorAsyncSplit: {
          // vendor async chunks, create each asynchronously used node module as separate chunk file if module is bigger than minSize
          test: /[\\/]node_modules[\\/]/,
          name(module) {
            // Extract the name of the package from the path segment after node_modules
            const packageName = module.context.match(
              /[\\/]node_modules[\\/](.*?)([\\/]|$)/
            )[1];
            return `vendor.async.${packageName.replace('@', '')}`;
          },
          chunks: 'async',
          priority: 10,
          reuseExistingChunk: true,
          minSize: 5000 // only create if 5k or larger
        },
        vendorsAsync: {
          // vendors async chunk, remaining asynchronously used node modules as single chunk file
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors.async',
          chunks: 'async',
          priority: 9,
          reuseExistingChunk: true,
          enforce: true // create chunk regardless of the size of the chunk
        },
        // generic 'async' common module splits: separates out larger modules
        commonAsync: {
          // common async chunks, each asynchronously used module a separate chunk file if module is bigger than minSize
          name(module) {
            // Extract the name of the module from last path component. 'src/modulename/' results in 'modulename'
            const moduleName = module.context.match(/[^\\/]+(?=\/$|$)/)[0];
            return `common.async.${moduleName.replace('@', '')}`;
          },
          minChunks: 2, // Minimum number of chunks that must share a module before splitting
          chunks: 'async',
          priority: 1,
          reuseExistingChunk: true,
          minSize: 5000 // only create if 5k or larger
        },
        commonsAsync: {
          // commons async chunk, remaining asynchronously used modules as single chunk file
          name: 'commons.async',
          minChunks: 2, // Minimum number of chunks that must share a module before splitting
          chunks: 'async',
          priority: 0,
          reuseExistingChunk: true,
          enforce: true // create chunk regardless of the size of the chunk
        }
      }
    }
  },
  performance: { hints: false },
  devServer: {
    contentBase: outDir,
    // serve index.html for all 404 (required for push-state)
    historyApiFallback: true
  },
  devtool: production ? 'nosources-source-map' : 'cheap-module-eval-source-map',
  module: {
    rules: [
      // CSS required in JS/TS files should use the style-loader that auto-injects it into the website
      // only when the issuer is a .js/.ts file, so the loaders are not applied inside html templates
      {
        test: /\.css$/i,
        issuer: [{ not: [{ test: /\.html$/i }] }],
        use: extractCss
          ? [
            {
              loader: MiniCssExtractPlugin.loader
            },
            ...cssRules
          ]
          : ['style-loader', ...cssRules]
      },
      {
        test: /\.css$/i,
        issuer: [{ test: /\.html$/i }],
        // CSS required in templates cannot be extracted safely
        // because Aurelia would try to require it again in runtime
        use: cssRules
      },
      { test: /\.html$/i, loader: 'html-loader' },
      {
        test: /\.js$/i,
        loader: 'babel-loader',
        exclude: nodeModulesDir,
        options: coverage ? { sourceMap: 'inline', plugins: ['istanbul'] } : {}
      },
      // use Bluebird as the global Promise implementation:
      {
        test: /[\/\\]node_modules[\/\\]bluebird[\/\\].+\.js$/,
        loader: 'expose-loader?Promise'
      },
      // embed small images and fonts as Data Urls and larger ones as files:
      {
        test: /\.(png|gif|jpg|cur)$/i,
        loader: 'url-loader',
        options: { limit: 8192 }
      },
      {
        test: /\.(eot|otf|ttf|woff|woff2)$/,
        loader: 'file-loader?name=fonts/[name].[ext]'
      },
      {
        test: /\.(svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/i,
        loader: 'file-loader'
      }
    ]
  },
  plugins: [
    ...when(!karma, new DuplicatePackageCheckerPlugin()),
    new AureliaPlugin(),
    new ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery',
      'window.jQuery': 'jquery',
      Hammer: 'hammerjs'
    }),
    new ProvidePlugin({
      Promise: 'bluebird'
    }),
    new ModuleDependenciesPlugin({
      'aurelia-testing': ['./compile-spy', './view-spy']
    }),
    new HtmlWebpackPlugin({
      template: 'index.ejs',
      minify: production
        ? {
          removeComments: true,
          collapseWhitespace: true,
          collapseInlineTagWhitespace: true,
          collapseBooleanAttributes: true,
          removeAttributeQuotes: true,
          minifyCSS: true,
          minifyJS: true,
          removeScriptTypeAttributes: true,
          removeStyleLinkTypeAttributes: true,
          ignoreCustomFragments: [/\${.*?}/g]
        }
        : undefined,
      metadata: {
        // available in index.ejs //
        title,
        server,
        baseUrl
      }
    }),
    // ref: https://webpack.js.org/plugins/mini-css-extract-plugin/
    ...when(extractCss, [
      new MiniCssExtractPlugin({
        // updated to match the naming conventions for the js files
        filename: production
          ? 'css/[name].[contenthash].bundle.css'
          : 'css/[name].[hash].bundle.css',
        chunkFilename: production
          ? 'css/[name].[contenthash].chunk.css'
          : 'css/[name].[hash].chunk.css'
      })
    ]),

    ...when(
      production || server,
      new CopyWebpackPlugin([{ from: 'static', to: outDir, ignore: ['.*'] }])
    ), // ignore dot (hidden) files
    new MediaQueryPlugin({
      include: true,
      queries: {
        'only screen and (min-width: 1200px)': 'desktop',
        'only screen and (min-width: 992px)': 'desktop',
        'only screen and (max-width: 767px)': 'mobile'
      }
    }),
    //new Critters({
    //  preload: 'js'
    //}),
    ...when(analyze, new BundleAnalyzerPlugin())
  ]
});

So actually this plugin doesn't seem to do anything at all currently. Maybe it is incompatible?

Aditionally I have a doubt, that this plugin can actually work with a plugin I intend to use: https://github.com/GoogleChromeLabs/critters any thoughts to that?

wzrdtales commented 5 years ago

Ok I found out that, if there is no matches to the queries, extraced is empty. And it is empty since these are only direct matches and css nano alters the actual query.

SassNinja commented 5 years ago

Hi @wzrdtales

it's hard to say regarding your large webpack config with many plugins. My usual workflow is to comment out most parts and then bring it back one after the other to find the responsible one.

Ok I found out that, if there is no matches to the queries, extraced is empty.

Yup, only what matches the queries gets extracted from the original CSS. You think cssnano is responsible? If so a minimum config is appreciated.

wzrdtales commented 5 years ago

I have it running by now. cssnano is "responsible" or better said it is due to the fact, that the queries are checked as a hard string and not in the matter of equality of parsed result.

Using a string like this instead:

    'only screen and (max-width:767px)': 'mobile'

Where only the one space is the thing being minified is the solution to the trouble.

SassNinja commented 5 years ago

Makes sense, if cssnano removes that space the query doesn't match anymore of course.

Glad to hear you've been able to solve the problem :)