Skip to content

Commit

Permalink
[Experimental] Add built-in Sass support (#10133)
Browse files Browse the repository at this point in the history
* Add built-in Sass support

* Add copy of CSS tests for SCSS

* Fix failing tests

* Fix url-loader tests

* Remove css file generated by tests

* Fix nprogress import for css file

* Fix SCSS modules (still 2 tests that need investigating)

* Update documentation for Sass support

* Fix plain CSS import test

* Fix formatting with prettier fix

* Update test output to reflect scss usage

* Revert "Fix plain CSS import test"

This reverts commit 380319d.

# Conflicts:
#	test/integration/scss-modules/test/index.test.js

* Update loader structure

* Resolve loaders before passing to compile function

* Remove dead filter  code

* Arrange loaders in order and push to array

* Fix loader order bug

* Fix global Sass loader and make module prepocessor optional

* Adjust Sass Modules Implementation

* Fix typo

* Adjust regexps

* Use regexes

* Simplify global setup

* Adjust comments

* fix regex

* Simplify identifier file

* Update Sass Instructions

* Remove unneeded fixtures

* Adjust global tests

* Remove wrapper

* Update source maps

* Flag scss behavior

* Fix css property value

* Update fixtures with Sass vars

* Turn on Scss support

* fix HMR test

* Fix snapshots

Co-authored-by: Tim Neutkens <tim@timneutkens.nl>
Co-authored-by: Joe Haddad <timer150@gmail.com>
  • Loading branch information
3 people committed Jan 22, 2020
1 parent 7d419f8 commit 8449ebc
Show file tree
Hide file tree
Showing 149 changed files with 2,524 additions and 30 deletions.
18 changes: 15 additions & 3 deletions docs/basic-features/built-in-css-support.md
Expand Up @@ -157,10 +157,22 @@ export default HelloWorld

Please see the [styled-jsx documentation](https://github.com/zeit/styled-jsx) for more examples.

## Sass, Less, and Stylus Support
## Sass Support

To support importing `.scss``.less` or `.styl` files you can use the following plugins:
Next.js allows you to import Sass using both the `.scss` and `.sass` extensions.
You can use component-level Sass via CSS Modules and the `.module.scss` or `.module.sass` extension.

Before you can use Next.js' built-in Sass support, be sure to install [`sass`](https://github.com/sass/sass):

```bash
npm install sass
```

Sass support has the same benefits and restrictions as the built-in CSS support detailed above.

## Less and Stylus Support

To support importing `.less` or `.styl` files you can use the following plugins:

- [@zeit/next-sass](https://github.com/zeit/next-plugins/tree/master/packages/next-sass)
- [@zeit/next-less](https://github.com/zeit/next-plugins/tree/master/packages/next-less)
- [@zeit/next-stylus](https://github.com/zeit/next-plugins/tree/master/packages/next-stylus)
1 change: 1 addition & 0 deletions packages/next/build/webpack-config.ts
Expand Up @@ -834,6 +834,7 @@ export default async function getBaseWebpackConfig(
isDevelopment: dev,
isServer,
hasSupportCss: !!config.experimental.css,
hasSupportScss: !!config.experimental.scss,
assetPrefix: config.assetPrefix || '',
})

Expand Down
109 changes: 100 additions & 9 deletions packages/next/build/webpack/config/blocks/css/index.ts
@@ -1,6 +1,6 @@
import curry from 'lodash.curry'
import path from 'path'
import { Configuration } from 'webpack'
import webpack, { Configuration } from 'webpack'
import MiniCssExtractPlugin from '../../../plugins/mini-css-extract-plugin'
import { loader, plugin } from '../../helpers'
import { ConfigurationContext, ConfigurationFn, pipe } from '../../utils'
Expand All @@ -13,20 +13,53 @@ import {
} from './messages'
import { getPostCssPlugins } from './plugins'

// RegExps for Stylesheets
const regexCssAll = /\.css$/
// RegExps for all Style Sheet variants
const regexLikeCss = /\.(css|scss|sass)$/

// RegExps for Style Sheets
const regexCssGlobal = /(?<!\.module)\.css$/
const regexCssModules = /\.module\.css$/

// RegExps for Syntactically Awesome Style Sheets
const regexSassGlobal = /(?<!\.module)\.(scss|sass)$/
const regexSassModules = /\.module\.(scss|sass)$/

export const css = curry(async function css(
enabled: boolean,
scssEnabled: boolean,
ctx: ConfigurationContext,
config: Configuration
) {
if (!enabled) {
return config
}

const sassPreprocessors: webpack.RuleSetUseItem[] = [
// First, process files with `sass-loader`: this inlines content, and
// compiles away the proprietary syntax.
{
loader: require.resolve('sass-loader'),
options: {
// Source maps are required so that `resolve-url-loader` can locate
// files original to their source directory.
sourceMap: true,
},
},
// Then, `sass-loader` will have passed-through CSS imports as-is instead
// of inlining them. Because they were inlined, the paths are no longer
// correct.
// To fix this, we use `resolve-url-loader` to rewrite the CSS
// imports to real file paths.
{
loader: require.resolve('resolve-url-loader'),
options: {
// Source maps are not required here, but we may as well emit
// them.
sourceMap: true,
},
},
]

const fns: ConfigurationFn[] = [
loader({
oneOf: [
Expand Down Expand Up @@ -55,7 +88,7 @@ export const css = curry(async function css(
loader({
oneOf: [
{
test: regexCssAll,
test: regexLikeCss,
// Use a loose regex so we don't have to crawl the file system to
// find the real file name (if present).
issuer: { test: /pages[\\/]_document\./ },
Expand Down Expand Up @@ -94,13 +127,41 @@ export const css = curry(async function css(
],
})
)
if (scssEnabled) {
fns.push(
loader({
oneOf: [
// Opt-in support for Sass (using .scss or .sass extensions).
{
// Sass Modules should never have side effects. This setting will
// allow unused Sass to be removed from the production build.
// We ensure this by disallowing `:global()` Sass at the top-level
// via the `pure` mode in `css-loader`.
sideEffects: false,
// Sass Modules are activated via this specific extension.
test: regexSassModules,
// Sass Modules are only supported in the user's application. We're
// not yet allowing Sass imports _within_ `node_modules`.
issuer: {
include: [ctx.rootDirectory],
exclude: /node_modules/,
},
use: getCssModuleLoader(ctx, postCssPlugins, sassPreprocessors),
},
],
})
)
}

// Throw an error for CSS Modules used outside their supported scope
fns.push(
loader({
oneOf: [
{
test: regexCssModules,
test: [
regexCssModules,
(scssEnabled && regexSassModules) as RegExp,
].filter(Boolean),
use: {
loader: 'error-loader',
options: {
Expand All @@ -116,7 +177,13 @@ export const css = curry(async function css(
fns.push(
loader({
oneOf: [
{ test: regexCssGlobal, use: require.resolve('ignore-loader') },
{
test: [
regexCssGlobal,
(scssEnabled && regexSassGlobal) as RegExp,
].filter(Boolean),
use: require.resolve('ignore-loader'),
},
],
})
)
Expand All @@ -137,14 +204,35 @@ export const css = curry(async function css(
],
})
)
if (scssEnabled) {
fns.push(
loader({
oneOf: [
{
// A global Sass import always has side effects. Webpack will tree
// shake the Sass without this option if the issuer claims to have
// no side-effects.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
test: regexSassGlobal,
issuer: { include: ctx.customAppFile },
use: getGlobalCssLoader(ctx, postCssPlugins, sassPreprocessors),
},
],
})
)
}
}

// Throw an error for Global CSS used inside of `node_modules`
fns.push(
loader({
oneOf: [
{
test: regexCssGlobal,
test: [
regexCssGlobal,
(scssEnabled && regexSassGlobal) as RegExp,
].filter(Boolean),
issuer: { include: [/node_modules/] },
use: {
loader: 'error-loader',
Expand All @@ -162,7 +250,10 @@ export const css = curry(async function css(
loader({
oneOf: [
{
test: regexCssGlobal,
test: [
regexCssGlobal,
(scssEnabled && regexSassGlobal) as RegExp,
].filter(Boolean),
use: {
loader: 'error-loader',
options: {
Expand All @@ -185,7 +276,7 @@ export const css = curry(async function css(
oneOf: [
{
// This should only be applied to CSS files
issuer: { test: regexCssAll },
issuer: { test: regexLikeCss },
// Exclude extensions that webpack handles by default
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
use: {
Expand Down
Expand Up @@ -2,6 +2,8 @@ import loaderUtils from 'loader-utils'
import path from 'path'
import webpack from 'webpack'

const regexLikeIndexModule = /(?<!pages[\\/])index\.module\.(scss|sass|css)$/

export function getCssModuleLocalIdent(
context: webpack.loader.LoaderContext,
_: any,
Expand All @@ -14,11 +16,9 @@ export function getCssModuleLocalIdent(

// Generate a more meaningful name (parent folder) when the user names the
// file `index.module.css`.
const fileNameOrFolder =
relativePath.endsWith('index.module.css') &&
relativePath !== 'pages/index.module.css'
? '[folder]'
: '[name]'
const fileNameOrFolder = regexLikeIndexModule.test(relativePath)
? '[folder]'
: '[name]'

// Generate a hash to make the class name unique.
const hash = loaderUtils.getHashDigest(
Expand Down
11 changes: 9 additions & 2 deletions packages/next/build/webpack/config/blocks/css/loaders/global.ts
Expand Up @@ -5,7 +5,8 @@ import { getClientStyleLoader } from './client'

export function getGlobalCssLoader(
ctx: ConfigurationContext,
postCssPlugins: postcss.AcceptedPlugin[]
postCssPlugins: postcss.AcceptedPlugin[],
preProcessors: webpack.RuleSetUseItem[] = []
): webpack.RuleSetUseItem[] {
const loaders: webpack.RuleSetUseItem[] = []

Expand All @@ -23,7 +24,7 @@ export function getGlobalCssLoader(
// Resolve CSS `@import`s and `url()`s
loaders.push({
loader: require.resolve('css-loader'),
options: { importLoaders: 1, sourceMap: true },
options: { importLoaders: 1 + preProcessors.length, sourceMap: true },
})

// Compile CSS
Expand All @@ -36,5 +37,11 @@ export function getGlobalCssLoader(
},
})

loaders.push(
// Webpack loaders run like a stack, so we need to reverse the natural
// order of preprocessors.
...preProcessors.reverse()
)

return loaders
}
11 changes: 9 additions & 2 deletions packages/next/build/webpack/config/blocks/css/loaders/modules.ts
Expand Up @@ -6,7 +6,8 @@ import { getCssModuleLocalIdent } from './getCssModuleLocalIdent'

export function getCssModuleLoader(
ctx: ConfigurationContext,
postCssPlugins: postcss.AcceptedPlugin[]
postCssPlugins: postcss.AcceptedPlugin[],
preProcessors: webpack.RuleSetUseItem[] = []
): webpack.RuleSetUseItem[] {
const loaders: webpack.RuleSetUseItem[] = []

Expand All @@ -25,7 +26,7 @@ export function getCssModuleLoader(
loaders.push({
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
importLoaders: 1 + preProcessors.length,
sourceMap: true,
onlyLocals: ctx.isServer,
modules: {
Expand All @@ -52,5 +53,11 @@ export function getCssModuleLoader(
},
})

loaders.push(
// Webpack loaders run like a stack, so we need to reverse the natural
// order of preprocessors.
...preProcessors.reverse()
)

return loaders
}
4 changes: 3 additions & 1 deletion packages/next/build/webpack/config/index.ts
Expand Up @@ -11,13 +11,15 @@ export async function build(
isDevelopment,
isServer,
hasSupportCss,
hasSupportScss,
assetPrefix,
}: {
rootDirectory: string
customAppFile: string | null
isDevelopment: boolean
isServer: boolean
hasSupportCss: boolean
hasSupportScss: boolean
assetPrefix: string
}
): Promise<webpack.Configuration> {
Expand All @@ -35,6 +37,6 @@ export async function build(
: '',
}

const fn = pipe(base(ctx), css(hasSupportCss, ctx))
const fn = pipe(base(ctx), css(hasSupportCss, hasSupportScss, ctx))
return fn(config)
}
1 change: 1 addition & 0 deletions packages/next/next-server/server/config.ts
Expand Up @@ -42,6 +42,7 @@ const defaultConfig: { [key: string]: any } = {
(os.cpus() || { length: 1 }).length) - 1
),
css: true,
scss: false,
documentMiddleware: false,
granularChunks: true,
modern: false,
Expand Down
2 changes: 2 additions & 0 deletions packages/next/package.json
Expand Up @@ -122,6 +122,8 @@
"raw-body": "2.4.0",
"react-error-overlay": "5.1.6",
"react-is": "16.8.6",
"resolve-url-loader": "3.1.1",
"sass-loader": "8.0.2",
"send": "0.17.1",
"source-map": "0.6.1",
"string-hash": "1.1.3",
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion test/integration/css-modules/test/index.test.js
Expand Up @@ -286,7 +286,7 @@ describe('Valid CSS Module Usage from within node_modules', () => {
const cssContent = await readFile(join(cssFolder, cssFiles[0]), 'utf8')

expect(cssContent.replace(/\/\*.*?\*\//g, '').trim()).toMatchInlineSnapshot(
`".example_redText__1rb5g{color:\\"red\\"}"`
`".example_redText__1rb5g{color:red}"`
)
})
})
Expand Down
@@ -0,0 +1,5 @@
import { foo } from './index.module.scss'

export default function Home() {
return <div id="verify-div" className={foo} />
}
@@ -0,0 +1,17 @@
.foo {
position: relative;
}

.foo :global(.bar),
.foo :global(.baz) {
height: 100%;
overflow: hidden;
}

.foo :global(.lol) {
width: 80%;
}

.foo > :global(.lel) {
width: 80%;
}
9 changes: 9 additions & 0 deletions test/integration/scss-fixtures/basic-module/pages/index.js
@@ -0,0 +1,9 @@
import { redText } from './index.module.scss'

export default function Home() {
return (
<div id="verify-red" className={redText}>
This text should be red.
</div>
)
}

0 comments on commit 8449ebc

Please sign in to comment.