@glitchr/media-query-plugin 1.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,200 @@
1
+ # Media Query Plugin
2
+
3
+ [![Npm Version](https://badge.fury.io/js/media-query-plugin.svg)](https://www.npmjs.com/package/media-query-plugin)
4
+ [![Build Status](https://travis-ci.com/SassNinja/media-query-plugin.svg?branch=master)](https://travis-ci.com/SassNinja/media-query-plugin)
5
+ [![Month Downloads](https://img.shields.io/npm/dm/media-query-plugin.svg)](http://npm-stat.com/charts.html?package=media-query-plugin)
6
+
7
+ Have you ever thought about extracting your media queries from your CSS so a mobile user doesn't have to load desktop specific CSS?
8
+ If so this plugin is what you need!
9
+
10
+ When writing CSS with the help of a framework (such as [Bootstrap](https://getbootstrap.com/) or [Foundation](https://get.foundation/sites.html)) and with a modular design pattern you'll mostly end up with CSS that contains all media queries. Using this plugin lets you easily extract the media queries from your CSS and load it async.
11
+
12
+ So instead of forcing the user to load this
13
+
14
+ ```css
15
+ .foo { color: red }
16
+ @media print, screen and (min-width: 75em) {
17
+ .foo { color: blue }
18
+ }
19
+ .bar { font-size: 1rem }
20
+ ```
21
+
22
+ he only has to load this always
23
+
24
+ ```css
25
+ .foo { color: red }
26
+ .bar { font-size: 1rem }
27
+ ```
28
+
29
+ and on desktop viewport size this in addition
30
+
31
+ ```css
32
+ @media print, screen and (min-width: 75em) {
33
+ .foo { color: blue }
34
+ }
35
+ ```
36
+
37
+
38
+ ## Prerequisites
39
+
40
+ You should already have a working webpack configuration before you try to use this plugin. If you haven't used webpack yet please go through the [webpack guide](https://webpack.js.org/guides/) first and start using this awesome tool for your assets mangement!
41
+
42
+ ## Installation
43
+
44
+ Simply install the package with your prefered package manager.
45
+
46
+ - npm
47
+ ```bash
48
+ npm install media-query-plugin --save-dev
49
+ ```
50
+
51
+ - yarn
52
+ ```bash
53
+ yarn add media-query-plugin --dev
54
+ ```
55
+
56
+ ## Let's get started
57
+
58
+ ### 1. Loader
59
+
60
+ The plugin comes together with a loader which takes care of the CSS extraction from the source and provides it for the injection afterwards.
61
+
62
+ **Important:** make sure the loader receives plain CSS so place it between the css-loader and the sass-loader/less-loader.
63
+
64
+ ```javascript
65
+ const MediaQueryPlugin = require('media-query-plugin');
66
+
67
+ module.exports = {
68
+ module: {
69
+ rules: [
70
+ {
71
+ test: /\.scss$/,
72
+ use: [
73
+ MiniCssExtractPlugin.loader,
74
+ 'css-loader',
75
+ MediaQueryPlugin.loader,
76
+ 'postcss-loader',
77
+ 'sass-loader'
78
+ ]
79
+ }
80
+ ]
81
+ }
82
+ };
83
+ ```
84
+
85
+ ### 2. Plugin
86
+
87
+ Add the plugin to your webpack config. It will inject the extracted CSS of the loader after the compilation. To identify the target file for the injection it'll look for `[name]-[query]`. So if CSS with the query `desktop` is extracted from `example.scss`, it'll look for `example-desktop` to do the injection. In case there's no match the extracted CSS gets simply emited as CSS file (it doesn't disappear in nirvana :wink:).
88
+
89
+ ```javascript
90
+ const MediaQueryPlugin = require('./plugins/media-query-plugin');
91
+
92
+ module.exports = {
93
+ plugins: [
94
+ new MediaQueryPlugin({
95
+ include: [
96
+ 'example'
97
+ ],
98
+ queries: {
99
+ 'print, screen and (min-width: 75em)': 'desktop'
100
+ }
101
+ })
102
+ ]
103
+ };
104
+ ```
105
+
106
+ ### 3. Use Extracted Files
107
+
108
+ If you import the extracted CSS (mostly as dynamic import with viewport condition), webpack will try to resolve that import and throw an error if the file does not exist. Thus you have to create those files manually before running webpack. Empty files as placeholder do the job (the get filled later by the plugin).
109
+
110
+ **Important:** as mentioned above the name of those files must follow the pattern `[name]-[query]` so an example file could be `example-desktop.scss`
111
+
112
+ ```javascript
113
+ import './example.scss';
114
+
115
+ if (window.innerWidth >= 960) {
116
+ import(/* webpackChunkName: 'example-desktop' */ './example-desktop.scss');
117
+ }
118
+ ```
119
+
120
+ ## Options
121
+
122
+ The following options are available.
123
+
124
+ | name | mandatory |
125
+ | ----------- | --------- |
126
+ | include | yes |
127
+ | queries | yes |
128
+ | groups | no |
129
+
130
+ ### include
131
+
132
+ Each chunk (which uses the loader) gets checked if its name matches this option. In case of a match each query specified in the `queries` options gets extracted from the chunk.
133
+
134
+ Possible types
135
+ - array (e.g. `['example']`)
136
+ - regex (e.g. `/example/`)
137
+ - boolean (e.g. `true`)
138
+
139
+ ### queries
140
+
141
+ This option tells the plugin which media queries are supposed to get extracted. If a media query doesn't match it'll stay untouched. Otherwise it gets extracted and afterwards injected.
142
+
143
+ **Important:** make sure the queries match 100% the source CSS rule excl the `@media`.
144
+
145
+ **Tip:** you can use the same name for different media queries to concatenate them (e.g. desktop portrait and desktop landscape)
146
+
147
+ ```javascript
148
+ queries: {
149
+ 'print, screen and (max-width: 60em) and (orientation: portrait)': 'desktop',
150
+ 'print, screen and (max-width: 60em) and (orientation: landscape)': 'desktop'
151
+ }
152
+ ```
153
+
154
+ ### groups
155
+
156
+ By default the name of the extracted CSS file(s) is `[chunk]-[query]`. This option lets you map chunk names to a specific group name what results in `[group]-[query]`.
157
+ So the following code would generate a `app-desktop.css` instead of `exampleA-desktop.css` and `exampleB-desktop.css`. This can be useful when working with [splitChunks](https://webpack.js.org/plugins/split-chunks-plugin/).
158
+
159
+ ```javascript
160
+ groups: {
161
+ app: ['exampleA', 'exampleB']
162
+ }
163
+ ```
164
+
165
+ **Tip:** you can also use regex to target chunks
166
+ ```javascript
167
+ groups: {
168
+ app: /^example/
169
+ }
170
+ ```
171
+
172
+ ## Other Webpack Plugins
173
+
174
+ This plugin plays together well with the following other webpack plugins.
175
+
176
+ ### mini-css-extract-plugin
177
+
178
+ If you don't want the CSS included in your JS but emit it as external files, you can use the [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin). The media query plugin automatically recognizes the additional CSS chunks and even takes over the plugins filename option!
179
+
180
+ ### html-webpack-plugin
181
+
182
+ If you're using the hash feature of webpack (e.g. `[name].[hash].js`) you might also be using the [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) to inject the hashed files into your templates. Good news – the media query plugin supports it! It hooks into the plugin and makes extracted files available in your HTML template via `htmlWebpackPlugin.files.extracted.js` or `htmlWebpackPlugin.files.extracted.css`.
183
+
184
+ This let you inject something as `<link rel="stylesheet" href="..." media="...">` so that the extracted files get downloaded but not applied if not necessary (reduces render blocking time). However most of the time it's better to use dynamic imports for the extracted CSS to achieve best performance.
185
+
186
+ Compared to the regular files (`htmlWebpackPlugin.files.js` or `htmlWebpackPlugin.files.css`) the extracted files object does not have the structure `[file, file]` but `[{file:file,query:query}, {file:file,query:query}]`. Keep this in mind when using it (or check out the example [template](examples/webpack/src/index.hbs)).
187
+
188
+ ## Not using webpack?
189
+
190
+ This plugin is built for webpack only and can't be used with other module bundlers (such as [FuseBox](https://fuse-box.org/)) or task runners (such as [Gulp](https://gulpjs.com/)). However if you can't or don't want to use webpack but nevertheless want to extract media queries you should check out my [PostCSS plugin](https://github.com/SassNinja/postcss-extract-media-query) which supports much more tools.
191
+
192
+ However it also breaks out of the bundler/runner and emits files within the PostCSS plugin which will ignore all other pipes in your task.
193
+ So it's highly recommended to use this webpack plugin instead of the PostCSS alternative!
194
+
195
+ ## Contribution
196
+
197
+ This plugin has been built because I wasn't able to find a webpack solution for such a trivial task of splitting files by media query and loading them async. It works for my use cases by I'm pretty sure it can get more improved. So if you miss any feature don't hesitate to create an issue as feature request or to create a PR to do the job.
198
+
199
+ **And last but not least, if you like this plugin please give it a star on github and share it!**
200
+
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@glitchr/media-query-plugin",
3
+ "version": "1.5.3",
4
+ "description": "Webpack plugin for media query extraction.",
5
+ "license": "MIT",
6
+ "access": "public",
7
+ "main": "src/index.js",
8
+ "scripts": {
9
+ "test": "mocha --timeout 10000"
10
+ },
11
+ "keywords": [
12
+ "webpack",
13
+ "plugin",
14
+ "loader",
15
+ "mediaquery",
16
+ "extract",
17
+ "split",
18
+ "css"
19
+ ],
20
+ "dependencies": {
21
+ "loader-utils": "^2.0.0",
22
+ "postcss": "^7.0.32",
23
+ "webpack": "^5.21.2",
24
+ "webpack-sources": "^2.2.0"
25
+ },
26
+ "devDependencies": {
27
+ "chai": "^4.2.0",
28
+ "css-loader": "^4.3.0",
29
+ "mini-css-extract-plugin": "^0.11.1",
30
+ "mocha": "^8.1.3",
31
+ "rimraf": "^3.0.2",
32
+ "sass": "^1.26.10",
33
+ "sass-loader": "^10.0.2",
34
+ "style-loader": "^1.2.1",
35
+ "webpack-merge": "^5.1.4"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "ssh://git@gitlab.glitchr.dev:public-repository/javascript/media-query-plugin.git"
40
+ }
41
+ }
package/src/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Bootstraper that exports the plugin and provides easy access to the loader.
3
+ */
4
+
5
+ const loader = require('./loader');
6
+ const plugin = require('./plugin');
7
+
8
+ // provide easy access to the loader
9
+ plugin.loader = require.resolve('./loader');
10
+
11
+ // export default webpack plugin
12
+ module.exports = plugin;
package/src/loader.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The loader component is supposed to extract the media CSS from the source chunks.
3
+ * To do this it uses a custom PostCSS plugin.
4
+ * In the course of this the original media CSS gets removed.
5
+ */
6
+
7
+ const { getOptions, interpolateName } = require('loader-utils');
8
+ const postcss = require('postcss');
9
+ const store = require('./store');
10
+ const plugin = require('./postcss');
11
+
12
+ module.exports = function(source) {
13
+
14
+ // make loader async
15
+ const cb = this.async();
16
+
17
+ // merge loader's options with plugin's options from store
18
+ const options = Object.assign(store.options, getOptions(this));
19
+
20
+ // basename gets used later to build the key for media query store
21
+ options.basename = interpolateName(this, '[name]', {});
22
+
23
+ // path gets used later to invalidate store (watch mode)
24
+ // (don't use options.filename to avoid name conflicts)
25
+ options.path = interpolateName(this, '[path][name].[ext]', {});
26
+
27
+ let isIncluded = false;
28
+
29
+ // check if current file should be affected
30
+ if (options.include instanceof Array && options.include.indexOf(options.basename) !== -1) {
31
+ isIncluded = true;
32
+ } else if (options.include instanceof RegExp && options.basename.match(options.include)) {
33
+ isIncluded = true;
34
+ } else if (options.include === true) {
35
+ isIncluded = true;
36
+ }
37
+
38
+ // return (either modified or not) source
39
+ if (isIncluded === true) {
40
+ postcss([ plugin(options) ])
41
+ .process(source, { from: options.basename })
42
+ .then(result => {
43
+ cb(null, result.toString())
44
+ });
45
+ } else {
46
+ cb(null, source);
47
+ }
48
+ };
package/src/plugin.js ADDED
@@ -0,0 +1,286 @@
1
+ /**
2
+ * The plugin component is supposed to inject the extracted media CSS (from store) into the file(s).
3
+ */
4
+
5
+ const pluginName = 'MediaQueryPlugin';
6
+
7
+ const { OriginalSource, ConcatSource } = require('webpack-sources');
8
+ const { interpolateName } = require('loader-utils');
9
+ const Chunk = require('webpack/lib/Chunk');
10
+ const Compilation = require('webpack/lib/Compilation');
11
+
12
+ const store = require('./store');
13
+ const escapeUtil = require('./utils/escape');
14
+
15
+ module.exports = class MediaQueryPlugin {
16
+
17
+ constructor(options) {
18
+ this.options = Object.assign({
19
+ include: [],
20
+ queries: {},
21
+ groups: {}
22
+ }, options);
23
+ }
24
+
25
+ getFilenameOption(compiler) {
26
+ const plugins = compiler.options.plugins;
27
+ let MiniCssExtractPluginOptions = {};
28
+
29
+ for (const plugin of plugins) {
30
+ if (plugin.constructor.name === 'MiniCssExtractPlugin') {
31
+ MiniCssExtractPluginOptions = plugin.options || {};
32
+ }
33
+ }
34
+
35
+ return MiniCssExtractPluginOptions.filename || compiler.options.output.filename;
36
+ }
37
+
38
+ apply(compiler) {
39
+
40
+ // if no filename option set, use default
41
+ this.options.filename = this.options.filename || this.getFilenameOption(compiler);
42
+
43
+ // save options in store to provide to loader
44
+ store.options = this.options;
45
+
46
+ // reset store for every webpack instance
47
+ // required for unit testing because the store is shared
48
+ compiler.hooks.entryOption.tap(pluginName, () => {
49
+ store.resetMedia();
50
+ });
51
+
52
+ // if a filename has become invalid (watch mode)
53
+ // remove all related data from store
54
+ compiler.hooks.invalid.tap(pluginName, (fileName, changeTime) => {
55
+ store.removeMediaByFilename(fileName);
56
+ });
57
+
58
+ compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
59
+
60
+ const hasDeprecatedChunks = (compilation.chunks instanceof Set) === false; // webpack < 5
61
+
62
+ if (hasDeprecatedChunks) {
63
+ console.warn('\n\n[WARNING] media-query-plugin is going to drop webpack 4 support with the next major version so you should consider upgrading asap!\n\n');
64
+ }
65
+
66
+ const processAssets = (compilationAssets, cb) => {
67
+
68
+ const chunks = compilation.chunks;
69
+ const chunkIds = [...chunks].map(chunk => chunk.id);
70
+ const assets = hasDeprecatedChunks ? compilation.assets : compilationAssets;
71
+
72
+ store.getMediaKeys().forEach(mediaKey => {
73
+
74
+ const css = store.getMedia(mediaKey);
75
+ const queries = store.getQueries(mediaKey);
76
+
77
+ // generate hash and use for [hash] within basename
78
+ const hash = interpolateName({}, `[hash:${compiler.options.output.hashDigestLength}]`, { content: css });
79
+
80
+ // compute basename according to filename option
81
+ // while considering hash
82
+ const basename = this.options.filename
83
+ .replace('[name]', mediaKey)
84
+ .replace(/\[(content|chunk)?hash\]/, hash)
85
+ .replace(/\.[^.]+$/, '');
86
+
87
+ // if there's no chunk for the extracted media, create one
88
+ if (chunkIds.indexOf(mediaKey) === -1) {
89
+ const mediaChunk = new Chunk(mediaKey);
90
+ mediaChunk.id = mediaKey;
91
+ mediaChunk.ids = [mediaKey];
92
+
93
+ if (hasDeprecatedChunks) {
94
+ chunks.push(mediaChunk);
95
+ } else {
96
+ chunks.add(mediaChunk);
97
+ }
98
+ }
99
+
100
+ const chunk = [...chunks].filter(chunk => chunk.id === mediaKey)[0];
101
+
102
+ // add query to chunk data if available
103
+ // can be used to determine query of a chunk (html-webpack-plugin)
104
+ if (queries) {
105
+ chunk.query = queries[0];
106
+ }
107
+
108
+ // find existing js & css files of this chunk
109
+ let existingFiles = { js: [], css: [] };
110
+ [...chunk.files].forEach(file => {
111
+ if (file.match(/\.js$/)) {
112
+ existingFiles.js.push(file);
113
+ } else if (file.match(/\.css$/)) {
114
+ existingFiles.css.push(file);
115
+ }
116
+ });
117
+
118
+ // if css included in js (style-loader), inject into js
119
+ if (existingFiles.js.length > 0 && existingFiles.css.length === 0) {
120
+ let content;
121
+ const extractedContent = new OriginalSource(`\nexports.push([module.i, ${escapeUtil(css)}, ""]);\n\n\n`, `${basename}.css`);
122
+
123
+ for (let i = 0; i < existingFiles.js.length; i++) {
124
+ const file = existingFiles.js[i];
125
+
126
+ if (assets[file]) {
127
+ if (i === 0) {
128
+ // since I've to inject the extracted content somewhere into the existing JS code (after `// module`)
129
+ // I can't simply use ConcatSource here but need to split the raw source code first
130
+ // This is only necessary for the first file/iteration
131
+ const originalSource = assets[file].source();
132
+ const aboveContent = new OriginalSource(originalSource.match(/[\s\S]*\/\/ module/gim)[0], `${basename}.js`);
133
+ const belowContent = new OriginalSource(originalSource.match(/(\/\/\s)?exports[\s\S]*/gm)[0], `${basename}.js`);
134
+
135
+ content = new ConcatSource(aboveContent, extractedContent, belowContent);
136
+ } else {
137
+ content = new ConcatSource(assets[file], content);
138
+ }
139
+ if (hasDeprecatedChunks) {
140
+ chunk.files.splice(chunk.files.indexOf(file), 1);
141
+ } else {
142
+ chunk.files.delete(file);
143
+ }
144
+ delete assets[file];
145
+ }
146
+ }
147
+
148
+ if (hasDeprecatedChunks) {
149
+ chunk.files.push(`${basename}.js`);
150
+ } else {
151
+ chunk.files.add(`${basename}.js`);
152
+ }
153
+ assets[`${basename}.js`] = content;
154
+ }
155
+
156
+ // else create additional css asset (new chunk)
157
+ // or replace existing css assert (mini-css-extract-plugin)
158
+ else {
159
+
160
+ let content = new OriginalSource(css, `${basename}.css`);
161
+ existingFiles.css.forEach(file => {
162
+ if (assets[file]) {
163
+ content = new ConcatSource(assets[file], content);
164
+
165
+ if (hasDeprecatedChunks) {
166
+ chunk.files.splice(chunk.files.indexOf(file), 1);
167
+ } else {
168
+ chunk.files.delete(file);
169
+ }
170
+ delete assets[file];
171
+ }
172
+ });
173
+
174
+ if (hasDeprecatedChunks) {
175
+ chunk.files.push(`${basename}.css`);
176
+ } else {
177
+ chunk.files.add(`${basename}.css`);
178
+ }
179
+ assets[`${basename}.css`] = content;
180
+ }
181
+
182
+ });
183
+
184
+ // sort assets object for nicer stats and correct order
185
+ // bcz due to our injection the order got changed
186
+ const chunksCompareFn = (a, b) => {
187
+ if (a.id > b.id)
188
+ return 1;
189
+ else if (a.id < b.id)
190
+ return -1;
191
+ else
192
+ return 0;
193
+ };
194
+ const sortedChunks = [...chunks].sort(chunksCompareFn);
195
+
196
+ compilation.chunks = hasDeprecatedChunks ? sortedChunks : new Set(sortedChunks);
197
+
198
+ const assetsCompareFn = (a, b) => {
199
+ // take file extension out of sort
200
+ a = a.replace(/\.[^.]+$/, '');
201
+ b = b.replace(/\.[^.]+$/, '');
202
+
203
+ if (a > b)
204
+ return 1;
205
+ else if (a < b)
206
+ return -1;
207
+ else
208
+ return 0;
209
+ };
210
+ const sortedAssets = Object.keys(assets).sort(assetsCompareFn).reduce((res, key) => (res[key] = assets[key], res), {});
211
+
212
+ if (hasDeprecatedChunks) {
213
+ compilation.assets = sortedAssets;
214
+ } else {
215
+ compilationAssets = sortedAssets;
216
+ }
217
+
218
+ cb();
219
+ };
220
+
221
+ // Since webpack 4 doesn't have the processAssets hook, we need the following condition.
222
+ // In future (once webpack 4 support has been dropped) this can be simplified again.
223
+ if (hasDeprecatedChunks) {
224
+ compilation.hooks.additionalAssets.tapAsync(pluginName, (cb) => {
225
+ processAssets(compilation.assets, cb);
226
+ });
227
+ } else {
228
+ compilation.hooks.processAssets.tapAsync({
229
+ name: pluginName,
230
+ stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL
231
+ }, (assets, cb) => {
232
+ processAssets(assets, cb);
233
+ });
234
+ }
235
+
236
+ // consider html-webpack-plugin and provide extracted files
237
+ // which can be accessed in templates via htmlWebpackPlugin.files.extracted
238
+ // { css: [{file:'',query:''},{file:'',query:''}] }
239
+
240
+ try {
241
+ const htmlWebpackPlugin = require('html-webpack-plugin');
242
+
243
+ compilation.hooks.afterOptimizeChunkAssets.tap(pluginName, (chunks) => {
244
+
245
+ const hookFn = (pluginArgs, cb) => {
246
+ const assetJson = [];
247
+ const extracted = {};
248
+
249
+ chunks.forEach(chunk => {
250
+ const query = chunk.query;
251
+
252
+ chunk.files.forEach(file => {
253
+ const ext = file.match(/\w+$/)[0];
254
+
255
+ if (query) {
256
+ extracted[ext] = extracted[ext] || [];
257
+ extracted[ext].push({
258
+ file: file,
259
+ query: query
260
+ });
261
+ }
262
+ assetJson.push(file);
263
+ });
264
+ });
265
+
266
+ pluginArgs.assets.extracted = extracted;
267
+ pluginArgs.plugin.assetJson = JSON.stringify(assetJson);
268
+ cb();
269
+ };
270
+
271
+ if (htmlWebpackPlugin.getHooks) {
272
+ htmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(pluginName, hookFn);
273
+ } else if (compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration) {
274
+ compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration.tapAsync(pluginName, hookFn);
275
+ }
276
+ });
277
+ } catch (err) {
278
+ if (err.code !== 'MODULE_NOT_FOUND') {
279
+ throw err;
280
+ }
281
+ }
282
+
283
+ });
284
+
285
+ }
286
+ };
package/src/postcss.js ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The PostCSS plugin component is supposed to extract the media CSS from the source chunks.
3
+ * The CSS get saved in the store.
4
+ */
5
+
6
+ const postcss = require('postcss');
7
+ const store = require('./store');
8
+ const normalize = require('./utils/normalize');
9
+
10
+ module.exports = postcss.plugin('MediaQueryPostCSS', options => {
11
+
12
+ function addToStore(name, atRule) {
13
+
14
+ const css = postcss.root().append(atRule).toString();
15
+ const query = atRule.params;
16
+
17
+ store.addMedia(name, css, options.path, query);
18
+ }
19
+
20
+ function getGroupName(name) {
21
+ const groupNames = Object.keys(options.groups);
22
+
23
+ for (let i = 0; i < groupNames.length; i++) {
24
+ const groupName = groupNames[i];
25
+ const group = options.groups[groupName];
26
+
27
+ if (group instanceof RegExp) {
28
+ if (name.match(group)) {
29
+ return groupName;
30
+ }
31
+ } else if (Array.isArray(group)) {
32
+ if (group.includes(name)) {
33
+ return groupName;
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ function getQueryName(query) {
40
+ const queries = Object.keys(options.queries);
41
+
42
+ for (let i = 0; i < queries.length; i++) {
43
+ if (normalize(query) === normalize(queries[i])) {
44
+ return options.queries[queries[i]];
45
+ }
46
+ }
47
+ }
48
+
49
+ return (css, result) => {
50
+
51
+ css.walkAtRules('media', atRule => {
52
+
53
+ const queryname = getQueryName(atRule.params);
54
+
55
+ if (queryname) {
56
+ const groupName = getGroupName(options.basename);
57
+ const name = groupName ? `${groupName}-${queryname}` : `${options.basename}-${queryname}`;
58
+
59
+ addToStore(name, atRule);
60
+ atRule.remove();
61
+ }
62
+ });
63
+ };
64
+ });
package/src/store.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The store component is supposed to transfer the extracted CSS from the loader to the plugin.
3
+ * Besides it also provides the plugin's options to the loader (as default options).
4
+ */
5
+
6
+ class MediaQueryStore {
7
+
8
+ constructor() {
9
+ this.media = {};
10
+ this.options = {};
11
+ }
12
+
13
+ addMedia(key, css, filename, query) {
14
+ const data = {
15
+ css: css,
16
+ filename: filename,
17
+ query: query
18
+ };
19
+
20
+ if (typeof this.media[key] !== 'object') {
21
+ this.media[key] = [];
22
+ }
23
+ this.media[key].push(data);
24
+ }
25
+
26
+ getMedia(key) {
27
+ // create css array from media[key] data
28
+ // which has the structure [{css:'',filename:'',query:''},{css:'',filename:'',query:''}]
29
+ const css = this.media[key].map(data => data.css);
30
+
31
+ return css.join('\n');
32
+ }
33
+
34
+ getQueries(key) {
35
+ // create queries array from media[key] data
36
+ // which can be used to determine the used query for a key
37
+ const queries = this.media[key].map(data => data.query);
38
+
39
+ return queries;
40
+ }
41
+
42
+ removeMediaByFilename(filename) {
43
+ this.getMediaKeys().forEach(key => {
44
+ this.media[key] = this.media[key].filter(media => media.filename !== filename);
45
+ if (this.media[key].length === 0) {
46
+ delete this.media[key];
47
+ }
48
+ });
49
+ }
50
+
51
+ resetMedia() {
52
+ this.media = {};
53
+ }
54
+
55
+ getMediaKeys() {
56
+ return Object.keys(this.media);
57
+ }
58
+ };
59
+
60
+ module.exports = new MediaQueryStore();
@@ -0,0 +1,20 @@
1
+ // Utility to escape a string.
2
+ // Taken over from css-loader
3
+ // https://github.com/webpack-contrib/css-loader/blob/master/lib/url/escape.js
4
+
5
+ module.exports = function escape(url) {
6
+ if (typeof url !== 'string') {
7
+ return url
8
+ }
9
+ // If url is already wrapped in quotes, remove them
10
+ if (/^['"].*['"]$/.test(url)) {
11
+ url = url.slice(1, -1);
12
+ }
13
+ // Should url be wrapped?
14
+ // See https://drafts.csswg.org/css-values-3/#urls
15
+ if (/["'() \t\n]/.test(url)) {
16
+ return '"' + url.replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"'
17
+ }
18
+
19
+ return url
20
+ }
@@ -0,0 +1,6 @@
1
+ // Utility to normalize a query for more tolerant comparison.
2
+ // normalize('@media (min-width: 1000px)') === normalize('@media(min-width:1000px)')
3
+
4
+ module.exports = function normalize(query) {
5
+ return query.toLowerCase().replace(/\s/g, '');
6
+ }
@@ -0,0 +1,6 @@
1
+
2
+ module.exports = {
3
+ 'only-javascript-output': require('./webpack/only-javascript-output'),
4
+ 'external-css-output': require('./webpack/external-css-output'),
5
+ 'groups-option-output': require('./webpack/groups-option-output')
6
+ };
@@ -0,0 +1,11 @@
1
+
2
+ module.exports = {
3
+ mode: 'development',
4
+ devtool: false,
5
+ entry: {
6
+ example: './test/data/example.js'
7
+ },
8
+ stats: {
9
+ children: false
10
+ }
11
+ };
@@ -0,0 +1,39 @@
1
+
2
+ const path = require('path');
3
+ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
4
+ const MediaQueryPlugin = require('../../../src');
5
+ const { merge } = require('webpack-merge');
6
+ const baseConfig = require('./base');
7
+
8
+ module.exports = merge(baseConfig, {
9
+ output: {
10
+ filename: '[name].js',
11
+ path: path.resolve(__dirname, `../../output/external-css-output`)
12
+ },
13
+ module: {
14
+ rules: [
15
+ {
16
+ test: /\.scss$/,
17
+ use: [
18
+ MiniCssExtractPlugin.loader,
19
+ 'css-loader',
20
+ MediaQueryPlugin.loader,
21
+ 'sass-loader'
22
+ ]
23
+ }
24
+ ]
25
+ },
26
+ plugins: [
27
+ new MiniCssExtractPlugin({
28
+ filename: '[name].css'
29
+ }),
30
+ new MediaQueryPlugin({
31
+ include: [
32
+ 'example'
33
+ ],
34
+ queries: {
35
+ 'print, screen and (max-width: 60em)': 'desktop'
36
+ }
37
+ })
38
+ ]
39
+ });
@@ -0,0 +1,45 @@
1
+
2
+ const path = require('path');
3
+ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
4
+ const MediaQueryPlugin = require('../../../src');
5
+
6
+ module.exports = {
7
+ mode: 'development',
8
+ entry: {
9
+ app: './test/data/app.js'
10
+ },
11
+ output: {
12
+ filename: '[name].js',
13
+ path: path.resolve(__dirname, `../../output/groups-option-output`)
14
+ },
15
+ module: {
16
+ rules: [
17
+ {
18
+ test: /\.scss$/,
19
+ use: [
20
+ MiniCssExtractPlugin.loader,
21
+ 'css-loader',
22
+ MediaQueryPlugin.loader,
23
+ 'sass-loader'
24
+ ]
25
+ }
26
+ ]
27
+ },
28
+ plugins: [
29
+ new MiniCssExtractPlugin({
30
+ filename: '[name].css'
31
+ }),
32
+ new MediaQueryPlugin({
33
+ include: true,
34
+ queries: {
35
+ 'print, screen and (max-width: 60em)': 'desktop'
36
+ },
37
+ groups: {
38
+ app: /^example/
39
+ }
40
+ })
41
+ ],
42
+ stats: {
43
+ children: false
44
+ }
45
+ };
@@ -0,0 +1,35 @@
1
+
2
+ const path = require('path');
3
+ const MediaQueryPlugin = require('../../../src');
4
+ const { merge } = require('webpack-merge');
5
+ const baseConfig = require('./base');
6
+
7
+ module.exports = merge(baseConfig, {
8
+ output: {
9
+ filename: '[name].js',
10
+ path: path.resolve(__dirname, `../../output/only-javascript-output`)
11
+ },
12
+ module: {
13
+ rules: [
14
+ {
15
+ test: /\.scss$/,
16
+ use: [
17
+ 'style-loader',
18
+ 'css-loader',
19
+ MediaQueryPlugin.loader,
20
+ 'sass-loader'
21
+ ]
22
+ }
23
+ ]
24
+ },
25
+ plugins: [
26
+ new MediaQueryPlugin({
27
+ include: [
28
+ 'example'
29
+ ],
30
+ queries: {
31
+ 'print, screen and (max-width: 60em)': 'desktop'
32
+ }
33
+ })
34
+ ]
35
+ });
@@ -0,0 +1,4 @@
1
+
2
+ // testing groups option
3
+ import './exampleA.scss';
4
+ import './exampleB.scss';
@@ -0,0 +1 @@
1
+ // wrapper
@@ -0,0 +1,6 @@
1
+
2
+ import './example.scss';
3
+
4
+ if (window.innerWidth >= 960) {
5
+ import(/* webpackChunkName: 'example-desktop' */ './example-desktop.scss');
6
+ }
@@ -0,0 +1,11 @@
1
+ .foo {
2
+ color: red;
3
+ }
4
+ @media print, screen and (max-width: 60em) {
5
+ .foo {
6
+ color: green;
7
+ }
8
+ }
9
+ .bar {
10
+ font-size: 1rem;
11
+ }
@@ -0,0 +1,8 @@
1
+ .foo {
2
+ color: red;
3
+ }
4
+ @media print, screen and (max-width: 60em) {
5
+ .foo {
6
+ color: green;
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ .bar {
2
+ font-size: 1rem;
3
+ }
4
+ @media print, screen and (max-width: 60em) {
5
+ .bar {
6
+ font-size: 2rem;
7
+ }
8
+ }
@@ -0,0 +1,88 @@
1
+
2
+ const assert = require('chai').assert;
3
+ const rimraf = require('rimraf');
4
+ const webpack = require('webpack');
5
+ const configs = require('./configs');
6
+
7
+ describe('Webpack Integration', function() {
8
+
9
+ // clear output before starting any test
10
+ afterEach(function clearOutput(done) {
11
+ rimraf('./test/output', done);
12
+ });
13
+
14
+ // test style-loader
15
+ it('should only emit js files when using style-loader', function(done) {
16
+
17
+ const expected = {
18
+ assets: ['example.js', 'example-desktop.js'],
19
+ chunks: ['example', 'example-desktop']
20
+ };
21
+
22
+ webpack(configs['only-javascript-output'], (err, stats) => {
23
+
24
+ if (err)
25
+ done(err);
26
+ else if (stats.hasErrors())
27
+ done(stats.toString());
28
+
29
+ const assets = Object.keys(stats.compilation.assets);
30
+ const chunks = [...stats.compilation.chunks].map(chunk => chunk.id);
31
+
32
+ assert.deepEqual(assets, expected.assets);
33
+ assert.deepEqual(chunks, expected.chunks);
34
+ done();
35
+ });
36
+
37
+ });
38
+
39
+ // test mini-css-extract-plugin
40
+ it('should emit css files when using mini-css-extract-plugin', function(done) {
41
+
42
+ const expected = {
43
+ assets: ['example.css', 'example.js', 'example-desktop.js', 'example-desktop.css'],
44
+ chunks: ['example', 'example-desktop']
45
+ };
46
+
47
+ webpack(configs['external-css-output'], (err, stats) => {
48
+
49
+ if (err)
50
+ done(err);
51
+ else if (stats.hasErrors())
52
+ done(stats.toString());
53
+
54
+ const assets = Object.keys(stats.compilation.assets);
55
+ const chunks = [...stats.compilation.chunks].map(chunk => chunk.id);
56
+
57
+ assert.deepEqual(assets, expected.assets);
58
+ assert.deepEqual(chunks, expected.chunks);
59
+ done();
60
+ });
61
+
62
+ });
63
+
64
+ it('should use groups option for extraxted file name', function(done) {
65
+
66
+ const expected = {
67
+ assets: ['app.css', 'app.js', 'app-desktop.css'],
68
+ chunks: ['app', 'app-desktop']
69
+ };
70
+
71
+ webpack(configs['groups-option-output'], (err, stats) => {
72
+
73
+ if (err)
74
+ done(err);
75
+ else if (stats.hasErrors())
76
+ done(stats.toString());
77
+
78
+ const assets = Object.keys(stats.compilation.assets);
79
+ const chunks = [...stats.compilation.chunks].map(chunk => chunk.id);
80
+
81
+ assert.deepEqual(assets, expected.assets);
82
+ assert.deepEqual(chunks, expected.chunks);
83
+ done();
84
+ });
85
+
86
+ });
87
+
88
+ });