Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Experimental] Add built-in Sass support #10133

Merged
merged 38 commits into from Jan 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
b3cab28
Add built-in Sass support
alejalapeno Jan 17, 2020
49f47af
Add copy of CSS tests for SCSS
timneutkens Jan 19, 2020
6877b99
Fix failing tests
timneutkens Jan 19, 2020
7302fed
Fix url-loader tests
timneutkens Jan 19, 2020
11ce903
Remove css file generated by tests
timneutkens Jan 19, 2020
952aba6
Fix nprogress import for css file
timneutkens Jan 19, 2020
cd23277
Fix SCSS modules (still 2 tests that need investigating)
timneutkens Jan 19, 2020
81a6a80
Update documentation for Sass support
alejalapeno Jan 19, 2020
380319d
Fix plain CSS import test
alejalapeno Jan 19, 2020
ac9288d
Fix formatting with prettier fix
alejalapeno Jan 19, 2020
7095ed5
Update test output to reflect scss usage
timneutkens Jan 20, 2020
d45e799
Merge branch 'sass-support' of github.com:alejalapeno/next.js into al…
timneutkens Jan 20, 2020
bd25e8e
Revert "Fix plain CSS import test"
timneutkens Jan 20, 2020
c271571
Update loader structure
alejalapeno Jan 20, 2020
5bc4e72
Fix loader order bug
alejalapeno Jan 20, 2020
0fa30ac
Fix global Sass loader and make module prepocessor optional
alejalapeno Jan 21, 2020
a69c9b9
Merge remote-tracking branch 'upstream/canary' into sass-support
Timer Jan 22, 2020
c0fb936
Adjust Sass Modules Implementation
Timer Jan 22, 2020
d54ba9e
Fix typo
Timer Jan 22, 2020
4cb6e9b
Adjust regexps
Timer Jan 22, 2020
20ce297
Use regexes
Timer Jan 22, 2020
1039a23
Simplify global setup
Timer Jan 22, 2020
ba5e73f
Adjust comments
Timer Jan 22, 2020
711837b
fix regex
Timer Jan 22, 2020
dca6b50
Simplify identifier file
Timer Jan 22, 2020
feab923
Update Sass Instructions
Timer Jan 22, 2020
251d5f8
Remove unneeded fixtures
Timer Jan 22, 2020
c14c7f0
Adjust global tests
Timer Jan 22, 2020
e5f3931
Remove wrapper
Timer Jan 22, 2020
6196d9a
Update source maps
Timer Jan 22, 2020
34500ac
Flag scss behavior
Timer Jan 22, 2020
703b7ad
Fix css property value
alejalapeno Jan 22, 2020
cf6834f
Update fixtures with Sass vars
alejalapeno Jan 22, 2020
fa1dd81
Merge branch 'canary' into sass-support
Timer Jan 22, 2020
3000643
Turn on Scss support
Timer Jan 22, 2020
825d790
fix HMR test
Timer Jan 22, 2020
7572109
Fix snapshots
Timer Jan 22, 2020
fb04a14
Merge branch 'canary' into sass-support
Timer Jan 22, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
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
}
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>
)
}