From 708b135269eb5db791e7e859988d189e23999106 Mon Sep 17 00:00:00 2001 From: Eli Perelman Date: Fri, 1 Dec 2017 10:20:06 -0600 Subject: [PATCH] Enable CSS Modules by default in *.module.css files (#495) * Enable CSS Modules by default in *.module.css files * Enable CSS modules when options.modules = true in style-loader * Fix double-compile bug when matching both .css and .module.css files * Use extension API for css modules * Revert bad rebase of web READMEs * Use a function to test css modules input * Use ignoreOrder with CSS modules --- docs/packages/preact/README.md | 19 ++-- docs/packages/react/README.md | 7 +- docs/packages/style-loader/README.md | 70 ++++++++++++++- docs/packages/web/README.md | 2 + packages/preact/README.md | 5 +- packages/react/README.md | 5 +- packages/style-loader/README.md | 68 +++++++++++++- packages/style-loader/index.js | 130 +++++++++++++++++---------- packages/web/README.md | 2 + 9 files changed, 244 insertions(+), 64 deletions(-) diff --git a/docs/packages/preact/README.md b/docs/packages/preact/README.md index 3f14c80db..e538099ca 100644 --- a/docs/packages/preact/README.md +++ b/docs/packages/preact/README.md @@ -14,14 +14,17 @@ - Automatic import of `Preact.h`, no need to import `h` or `createElement` yourself - Compatibility and pre-configured aliasing for React-based modules and packages - Extends from [@neutrinojs/web](../web) - - Modern Babel compilation supporting ES modules, last 2 major browser versions, async functions, and dynamic imports - - webpack loaders for importing HTML, CSS, images, icons, fonts, and web workers - - webpack Dev Server during development - - Automatic creation of HTML pages, no templating necessary - - Hot Module Replacement support - - Tree-shaking to create smaller bundles - - Production-optimized bundles with Babel minification, easy chunking, and scope-hoisted modules for faster execution - - Easily extensible to customize your project as needed + - Zero upfront configuration necessary to start developing and building a web app + - Modern Babel compilation supporting ES modules, last 2 major browser versions, async functions, and dynamic imports + - webpack loaders for importing HTML, CSS, images, icons, fonts, and web workers + - webpack Dev Server during development + - Automatic creation of HTML pages, no templating necessary + - Automatic stylesheet extraction; importing stylesheets into modules creates bundled external stylesheets + - Pre-configured to support CSS Modules via `*.module.css` file extensions + - Hot Module Replacement support including CSS + - Tree-shaking to create smaller bundles + - Production-optimized bundles with Babel minification, easy chunking, and scope-hoisted modules for faster execution + - Easily extensible to customize your project as needed ## Requirements diff --git a/docs/packages/react/README.md b/docs/packages/react/README.md index 93e7de480..bbbe15f4b 100644 --- a/docs/packages/react/README.md +++ b/docs/packages/react/README.md @@ -14,13 +14,16 @@ - Write JSX in .js or .jsx files - Automatic import of `React.createElement`, no need to import `react` or `React.createElement` yourself - Extends from [@neutrinojs/web](../@neutrinojs/web/README.md) + - Zero upfront configuration necessary to start developing and building a web app - Modern Babel compilation supporting ES modules, last 2 major browser versions, async functions, and dynamic imports - webpack loaders for importing HTML, CSS, images, icons, fonts, and web workers - webpack Dev Server during development - Automatic creation of HTML pages, no templating necessary - - Hot Module Replacement support + - Automatic stylesheet extraction; importing stylesheets into modules creates bundled external stylesheets + - Pre-configured to support CSS Modules via `*.module.css` file extensions + - Hot Module Replacement support including CSS - Tree-shaking to create smaller bundles - - Production-optimized bundles with Babili minification, easy chunking, and scope-hoisted modules for faster execution + - Production-optimized bundles with Babel minification, easy chunking, and scope-hoisted modules for faster execution - Easily extensible to customize your project as needed ## Requirements diff --git a/docs/packages/style-loader/README.md b/docs/packages/style-loader/README.md index 8884d6286..7eb9d85b8 100644 --- a/docs/packages/style-loader/README.md +++ b/docs/packages/style-loader/README.md @@ -6,6 +6,14 @@ [![NPM downloads][npm-downloads]][npm-url] [![Join the Neutrino community on Spectrum][spectrum-image]][spectrum-url] +## Features + +- Zero upfront configuration necessary to import stylesheets into modules +- Automatic stylesheet extraction; importing stylesheets into modules creates bundled external stylesheets +- Pre-configured to support CSS Modules via `*.module.css` file extensions +- CSS Hot Module Replacement support +- Very extensible to customize as needed + ## Requirements - Node.js v6.10+ @@ -43,7 +51,21 @@ neutrino.use(styles); // Usage showing default options neutrino.use(styles, { style: {}, - css: {} + css: {}, + test: /\.css$/, + ruleId: 'style', + styleUseId: 'style', + cssUseId: 'css', + hotUseId: 'hot', + hot: true, + modules: true, + modulesSuffix: '-modules', + modulesTest: /\.module.css$/, + extractId: 'extract', + extract: { + plugin: {}, + loader: {} + } }); ``` @@ -60,7 +82,21 @@ module.exports = { use: [ ['@neutrinojs/style-loader', { style: {}, - css: {} + css: {}, + test: /\.css$/, + ruleId: 'style', + styleUseId: 'style', + cssUseId: 'css', + hotUseId: 'hot', + hot: true, + modules: true, + modulesSuffix: '-modules', + modulesTest: /\.module.css$/, + extractId: 'extract', + extract: { + plugin: {}, + loader: {} + } }] ] }; @@ -68,6 +104,20 @@ module.exports = { - `style`: Set options for the style-loader used when loading CSS files. - `css`: Set options for the css-loader used when loading CSS files. +- `test`: File extensions which support stylesheets +- `ruleId`: The ID of the webpack-chain rule used to identify the stylesheet loaders +- `styleUseId`: The ID of the webpack-chain `use` used to identify the style-loader +- `cssUseId`: The ID of the webpack-chain `use` used to identify the css-loader +- `hotUseId`: The ID of the webpack-chain `use` used to identify the css-hot-loader +- `hot`: Enable usage of CSS Hot Module Replacement. Set to `false` to disable. +- `modules`: Enable usage of CSS modules via `*.module.css` files. Set to `false` to disable and skip defining these rules. +- `modulesSuffix`: A suffix added to `ruleId`, `styleUseId`, `cssUseId`, `hotUseId`, and `extractId` to derive names for +modules-related rules. For example, the default `-modules` suffix will generate a rule ID for the CSS modules rules of +`style-modules`, while the normal rule remains as `style`. +- `modulesTest`: File extensions which support CSS Modules stylesheets +- `extractId`: The ID of the webpack-chain plugin used to identify the `ExtractTextPlugin` +- `extract`: Options relating to the `ExtractTextPlugin` instance. Override `extract.plugin` to override plugin options. +Override `extract.loader` to override the loader options. Set to `false` to disable stylesheet extraction. ## Customization @@ -76,17 +126,29 @@ ready to make changes. ### Rules -The following is a list of rules and their identifiers which can be overridden: +The following is a list of default rules and their identifiers which can be overridden: | Name | Description | Environments and Commands | | --- | --- | --- | | `style` | Allows importing CSS stylesheets from modules. Contains two loaders named `style` and `css` which use `style-loader` and `css-loader`, respectively. | all | +| `style-modules` | Allows importing CSS Modules styles from modules. Contains two loaders named `style-modules` and `css-modules` which use `style-loader` and `css-loader`, respectively. | all | + +### Plugins + +The following is a default list of plugins and their identifiers which can be overridden: + +_Note: Some plugins may be only available in certain environments. To override them, they should be modified conditionally._ + +| Name | Description | Environments and Commands | +| --- | --- | --- | +| `extract` | Extracts CSS from JS bundle into a separate stylesheet file. | all | +| `extract-modules` | Extracts CSS from JS bundle into a separate stylesheet file. | all | ## Contributing This middleware is part of the [neutrino-dev](https://github.com/mozilla-neutrino/neutrino-dev) repository, a monorepo containing all resources for developing Neutrino and its core presets and middleware. Follow the -[contributing guide](https://neutrino.js.org/contributing) for details. +[contributing guide](../../contributing) for details. [npm-image]: https://img.shields.io/npm/v/@neutrinojs/style-loader.svg [npm-downloads]: https://img.shields.io/npm/dt/@neutrinojs/style-loader.svg diff --git a/docs/packages/web/README.md b/docs/packages/web/README.md index 279221775..4f3bc5a0a 100644 --- a/docs/packages/web/README.md +++ b/docs/packages/web/README.md @@ -339,6 +339,8 @@ _Note: Some plugins are only available in certain environments. To override them | Name | Description | Environments and Commands | | --- | --- | --- | | `env` | Inject environment variables into source code at `process.env`, defaults to only inject `NODE_ENV`. From `@neutrinojs/env`. | all | +| `extract` | Extracts CSS from JS bundle into a separate stylesheet file. From `@neutrinojs/style-loader`. | all | +| `extract-modules` | Extracts CSS from JS bundle into a separate stylesheet file. From `@neutrinojs/style-loader`. | all | | `html` | Automatically generates HTML files for configured main entry points. From `@neutrinojs/html-template` | all | | `named-modules` | Enables named modules for improved debugging and console output. From `@neutrinojs/chunk` and `@neutrinojs/hot`. | `NODE_ENV production`, `start` command | | `named-chunks` | Enables named chunks for improved debugging and console output. From `@neutrinojs/chunk`. | `NODE_ENV production` | diff --git a/packages/preact/README.md b/packages/preact/README.md index edffb6c3a..cd4476572 100644 --- a/packages/preact/README.md +++ b/packages/preact/README.md @@ -14,11 +14,14 @@ - Automatic import of `Preact.h`, no need to import `h` or `createElement` yourself - Compatibility and pre-configured aliasing for React-based modules and packages - Extends from [@neutrinojs/web](https://neutrino.js.org/packages/web) + - Zero upfront configuration necessary to start developing and building a web app - Modern Babel compilation supporting ES modules, last 2 major browser versions, async functions, and dynamic imports - webpack loaders for importing HTML, CSS, images, icons, fonts, and web workers - webpack Dev Server during development - Automatic creation of HTML pages, no templating necessary - - Hot Module Replacement support + - Automatic stylesheet extraction; importing stylesheets into modules creates bundled external stylesheets + - Pre-configured to support CSS Modules via `*.module.css` file extensions + - Hot Module Replacement support including CSS - Tree-shaking to create smaller bundles - Production-optimized bundles with Babel minification, easy chunking, and scope-hoisted modules for faster execution - Easily extensible to customize your project as needed diff --git a/packages/react/README.md b/packages/react/README.md index 43a474568..0ede15ea3 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -14,11 +14,14 @@ - Write JSX in .js or .jsx files - Automatic import of `React.createElement`, no need to import `react` or `React.createElement` yourself - Extends from [@neutrinojs/web](https://neutrino.js.org/packages/web) + - Zero upfront configuration necessary to start developing and building a web app - Modern Babel compilation supporting ES modules, last 2 major browser versions, async functions, and dynamic imports - webpack loaders for importing HTML, CSS, images, icons, fonts, and web workers - webpack Dev Server during development - Automatic creation of HTML pages, no templating necessary - - Hot Module Replacement support + - Automatic stylesheet extraction; importing stylesheets into modules creates bundled external stylesheets + - Pre-configured to support CSS Modules via `*.module.css` file extensions + - Hot Module Replacement support including CSS - Tree-shaking to create smaller bundles - Production-optimized bundles with Babel minification, easy chunking, and scope-hoisted modules for faster execution - Easily extensible to customize your project as needed diff --git a/packages/style-loader/README.md b/packages/style-loader/README.md index 8884d6286..6128641b3 100644 --- a/packages/style-loader/README.md +++ b/packages/style-loader/README.md @@ -6,6 +6,14 @@ [![NPM downloads][npm-downloads]][npm-url] [![Join the Neutrino community on Spectrum][spectrum-image]][spectrum-url] +## Features + +- Zero upfront configuration necessary to import stylesheets into modules +- Automatic stylesheet extraction; importing stylesheets into modules creates bundled external stylesheets +- Pre-configured to support CSS Modules via `*.module.css` file extensions +- CSS Hot Module Replacement support +- Very extensible to customize as needed + ## Requirements - Node.js v6.10+ @@ -43,7 +51,21 @@ neutrino.use(styles); // Usage showing default options neutrino.use(styles, { style: {}, - css: {} + css: {}, + test: /\.css$/, + ruleId: 'style', + styleUseId: 'style', + cssUseId: 'css', + hotUseId: 'hot', + hot: true, + modules: true, + modulesSuffix: '-modules', + modulesTest: /\.module.css$/, + extractId: 'extract', + extract: { + plugin: {}, + loader: {} + } }); ``` @@ -60,7 +82,21 @@ module.exports = { use: [ ['@neutrinojs/style-loader', { style: {}, - css: {} + css: {}, + test: /\.css$/, + ruleId: 'style', + styleUseId: 'style', + cssUseId: 'css', + hotUseId: 'hot', + hot: true, + modules: true, + modulesSuffix: '-modules', + modulesTest: /\.module.css$/, + extractId: 'extract', + extract: { + plugin: {}, + loader: {} + } }] ] }; @@ -68,6 +104,20 @@ module.exports = { - `style`: Set options for the style-loader used when loading CSS files. - `css`: Set options for the css-loader used when loading CSS files. +- `test`: File extensions which support stylesheets +- `ruleId`: The ID of the webpack-chain rule used to identify the stylesheet loaders +- `styleUseId`: The ID of the webpack-chain `use` used to identify the style-loader +- `cssUseId`: The ID of the webpack-chain `use` used to identify the css-loader +- `hotUseId`: The ID of the webpack-chain `use` used to identify the css-hot-loader +- `hot`: Enable usage of CSS Hot Module Replacement. Set to `false` to disable. +- `modules`: Enable usage of CSS modules via `*.module.css` files. Set to `false` to disable and skip defining these rules. +- `modulesSuffix`: A suffix added to `ruleId`, `styleUseId`, `cssUseId`, `hotUseId`, and `extractId` to derive names for +modules-related rules. For example, the default `-modules` suffix will generate a rule ID for the CSS modules rules of +`style-modules`, while the normal rule remains as `style`. +- `modulesTest`: File extensions which support CSS Modules stylesheets +- `extractId`: The ID of the webpack-chain plugin used to identify the `ExtractTextPlugin` +- `extract`: Options relating to the `ExtractTextPlugin` instance. Override `extract.plugin` to override plugin options. +Override `extract.loader` to override the loader options. Set to `false` to disable stylesheet extraction. ## Customization @@ -76,11 +126,23 @@ ready to make changes. ### Rules -The following is a list of rules and their identifiers which can be overridden: +The following is a list of default rules and their identifiers which can be overridden: | Name | Description | Environments and Commands | | --- | --- | --- | | `style` | Allows importing CSS stylesheets from modules. Contains two loaders named `style` and `css` which use `style-loader` and `css-loader`, respectively. | all | +| `style-modules` | Allows importing CSS Modules styles from modules. Contains two loaders named `style-modules` and `css-modules` which use `style-loader` and `css-loader`, respectively. | all | + +### Plugins + +The following is a default list of plugins and their identifiers which can be overridden: + +_Note: Some plugins may be only available in certain environments. To override them, they should be modified conditionally._ + +| Name | Description | Environments and Commands | +| --- | --- | --- | +| `extract` | Extracts CSS from JS bundle into a separate stylesheet file. | all | +| `extract-modules` | Extracts CSS from JS bundle into a separate stylesheet file. | all | ## Contributing diff --git a/packages/style-loader/index.js b/packages/style-loader/index.js index 52ea463b1..307af7f73 100644 --- a/packages/style-loader/index.js +++ b/packages/style-loader/index.js @@ -2,9 +2,29 @@ const ExtractTextPlugin = require('extract-text-webpack-plugin'); const merge = require('deepmerge'); module.exports = (neutrino, opts = {}) => { + const cssTest = neutrino.regexFromExtensions(['css']); + const cssModulesTest = neutrino.regexFromExtensions(['module.css']); + const options = merge({ + test: (input) => { + const isCssModule = cssModulesTest.test(input); + const isRegularCss = cssTest.test(input); + + if (opts.modules !== false && isCssModule) { + return false; + } + + return isRegularCss; + }, + ruleId: 'style', styleUseId: 'style', + cssUseId: 'css', hot: true, + hotUseId: 'hot', + modules: true, + modulesSuffix: '-modules', + modulesTest: cssModulesTest, + extractId: 'extract', extract: { plugin: { filename: neutrino.options.command === 'build' ? '[name].[contenthash].css' : '[name].css' @@ -12,54 +32,74 @@ module.exports = (neutrino, opts = {}) => { } }, opts); - neutrino.config.module - .rule(options.ruleId || 'style') - .test(neutrino.regexFromExtensions(['css'])) - .use(options.styleUseId) - .loader(require.resolve('style-loader')) - .when(options.style, use => use.options(options.style)) - .end() - .use(options.cssUseId || 'css') - .loader(require.resolve('css-loader')) - .when(options.css, use => use.options(options.css)); - - if (options.extract) { - const styleRule = neutrino.config.module.rule('style'); - const styleEntries = styleRule.uses.entries(); - const useKeys = Object.keys(styleEntries).filter(key => key !== options.styleUseId); + const rules = [options]; - options.extract.loader = Object.assign({ - use: useKeys.map(key => ({ - loader: styleEntries[key].get('loader'), - options: styleEntries[key].get('options') - })), - fallback: { - loader: styleEntries[options.styleUseId].get('loader'), - options: styleEntries[options.styleUseId].get('options') + if (options.modules) { + rules.push(merge(options, { + test: options.modulesTest, + ruleId: `${options.ruleId}${options.modulesSuffix}`, + styleUseId: `${options.styleUseId}${options.modulesSuffix}`, + cssUseId: `${options.cssUseId}${options.modulesSuffix}`, + hotUseId: `${options.hotUseId}${options.modulesSuffix}`, + extractId: `${options.extractId}${options.modulesSuffix}`, + css: { + modules: options.modules, + importLoaders: 1, + ignoreOrder: true } - }, options.extract.loader || {}); + })); + } - styleRule - .uses - .clear() - .end() - .when(options.hot, (rule) => { - rule.use(options.hotUseId || 'hot') - .loader(require.resolve('css-hot-loader')) - .when(options.hot !== true, use => use.options(options.hot)); - }); + rules.forEach(options => { + neutrino.config.module + .rule(options.ruleId) + .test(options.test) + .use(options.styleUseId) + .loader(require.resolve('style-loader')) + .when(options.style, use => use.options(options.style)) + .end() + .use(options.cssUseId) + .loader(require.resolve('css-loader')) + .when(options.css, use => use.options(options.css)); - ExtractTextPlugin - .extract(options.extract.loader) - .forEach(({ loader, options }) => { - styleRule - .use(loader) - .loader(loader) - .options(options); - }); + if (options.extract) { + const styleRule = neutrino.config.module.rule(options.ruleId); + const styleEntries = styleRule.uses.entries(); + const useKeys = Object.keys(styleEntries).filter(key => key !== options.styleUseId); + const extractLoader = Object.assign({ + use: useKeys.map(key => ({ + loader: styleEntries[key].get('loader'), + options: styleEntries[key].get('options') + })), + fallback: { + loader: styleEntries[options.styleUseId].get('loader'), + options: styleEntries[options.styleUseId].get('options') + } + }, options.extract.loader || {}); + + styleRule + .uses + .clear() + .end() + .when(options.hot, (rule) => { + rule.use(options.hotUseId) + .loader(require.resolve('css-hot-loader')) + .when(options.hot !== true, use => use.options(options.hot)); + }); + + ExtractTextPlugin + .extract(extractLoader) + .forEach(({ loader, options }) => { + styleRule + .use(loader) + .loader(loader) + .options(options); + }); + + neutrino.config + .plugin(options.extractId) + .use(ExtractTextPlugin, [options.extract.plugin]); + } + }); - neutrino.config - .plugin('extract') - .use(ExtractTextPlugin, [options.extract.plugin]); - } }; diff --git a/packages/web/README.md b/packages/web/README.md index c1320c235..119d3d1ed 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -338,6 +338,8 @@ _Note: Some plugins are only available in certain environments. To override them | Name | Description | Environments and Commands | | --- | --- | --- | | `env` | Inject environment variables into source code at `process.env`, defaults to only inject `NODE_ENV`. From `@neutrinojs/env`. | all | +| `extract` | Extracts CSS from JS bundle into a separate stylesheet file. From `@neutrinojs/style-loader`. | all | +| `extract-modules` | Extracts CSS from JS bundle into a separate stylesheet file. From `@neutrinojs/style-loader`. | all | | `html` | Automatically generates HTML files for configured entry points. From `@neutrinojs/html-template` | all | | `named-modules` | Enables named modules for improved debugging and console output. From `@neutrinojs/chunk` and `@neutrinojs/hot`. | `NODE_ENV production`, `start` command | | `named-chunks` | Enables named chunks for improved debugging and console output. From `@neutrinojs/chunk`. | `NODE_ENV production` |