提交 8449ebc2 编写于 作者: A Alexander Dreith 提交者: Joe Haddad

[Experimental] Add built-in Sass support (#10133)

* 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 380319d9d0c4bfb19e28c210262ccd82d19f3556.

# 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: NTim Neutkens <tim@timneutkens.nl>
Co-authored-by: NJoe Haddad <timer150@gmail.com>
上级 7d419f88
......@@ -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)
......@@ -834,6 +834,7 @@ export default async function getBaseWebpackConfig(
isDevelopment: dev,
isServer,
hasSupportCss: !!config.experimental.css,
hasSupportScss: !!config.experimental.scss,
assetPrefix: config.assetPrefix || '',
})
......
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'
......@@ -13,13 +13,20 @@ 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
) {
......@@ -27,6 +34,32 @@ export const css = curry(async function css(
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: [
......@@ -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\./ },
......@@ -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: {
......@@ -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'),
},
],
})
)
......@@ -137,6 +204,24 @@ 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`
......@@ -144,7 +229,10 @@ export const css = curry(async function css(
loader({
oneOf: [
{
test: regexCssGlobal,
test: [
regexCssGlobal,
(scssEnabled && regexSassGlobal) as RegExp,
].filter(Boolean),
issuer: { include: [/node_modules/] },
use: {
loader: 'error-loader',
......@@ -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: {
......@@ -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: {
......
......@@ -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,
......@@ -14,9 +16,7 @@ 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'
const fileNameOrFolder = regexLikeIndexModule.test(relativePath)
? '[folder]'
: '[name]'
......
......@@ -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[] = []
......@@ -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
......@@ -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
}
......@@ -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[] = []
......@@ -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: {
......@@ -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
}
......@@ -11,6 +11,7 @@ export async function build(
isDevelopment,
isServer,
hasSupportCss,
hasSupportScss,
assetPrefix,
}: {
rootDirectory: string
......@@ -18,6 +19,7 @@ export async function build(
isDevelopment: boolean
isServer: boolean
hasSupportCss: boolean
hasSupportScss: boolean
assetPrefix: string
}
): Promise<webpack.Configuration> {
......@@ -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)
}
......@@ -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,
......
......@@ -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",
......
......@@ -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}"`
)
})
})
......
import { foo } from './index.module.scss'
export default function Home() {
return <div id="verify-div" className={foo} />
}
.foo {
position: relative;
}
.foo :global(.bar),
.foo :global(.baz) {
height: 100%;
overflow: hidden;
}
.foo :global(.lol) {
width: 80%;
}
.foo > :global(.lel) {
width: 80%;
}
import { redText } from './index.module.scss'
export default function Home() {
return (
<div id="verify-red" className={redText}>
This text should be red.
</div>
)
}
import React from 'react'
import App from 'next/app'
import '../styles/global.scss'
class MyApp extends App {
render() {
const { Component, pageProps } = this.props
return <Component {...pageProps} />
}
}
export default MyApp
export default function Home() {
return <div className="red-text">This text should be red.</div>
}
$var: red;
.redText {
::placeholder {
color: $var;
}
}
import { subClass } from './index.module.scss'
export default function Home() {
return (
<div id="verify-yellow" className={subClass}>
This text should be yellow on blue.
</div>
)
}
$var: red;
.className {
background: $var;
color: yellow;
}
.subClass {
composes: className;
background: blue;
}
import { subClass } from './index.module.scss'
export default function Home() {
return (
<div id="verify-yellow" className={subClass}>
This text should be yellow on blue.
</div>
)
}
$var: blue;
.subClass {
composes: className from './other.scss';
background: $var;
}
$var: red;
.className {
background: $var;
color: yellow;
}
import { redText } from './index.module.scss'
export default function Home() {
return (
<div id="verify-red" className={redText}>
This text should be red.
</div>
)
}
import { redText } from './index.module.scss'
function Home() {
return (
<>
<div id="verify-red" className={redText}>
This text should be red.
</div>
<br />
<input key={'' + Math.random()} id="text-input" type="text" />
</>
)
}
export default Home
import 'example'
function Home() {
return <div>This should fail at build time.</div>
}
export default Home
import React from 'react'
import App from 'next/app'
class MyApp extends App {
render() {
const { Component, pageProps } = this.props
return <Component {...pageProps} />
}
}
export default MyApp
import '../styles/global.scss'
export default function Home() {
return <div className="red-text">This text should be red.</div>
}
import '../styles/global.scss'
export default function Home() {
return <div className="red-text">This text should be red.</div>
}
import Document, { Head, Html, Main, NextScript } from 'next/document'
import styles from '../styles.module.scss'
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx)
return { ...initialProps }
}
render() {
return (
<Html>
<Head />
<body className={styles['red-text']}>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
export default function Home() {
return <div>Hello</div>
}
import * as classes from 'example'
function Home() {
return <div>This should fail at build time {JSON.stringify(classes)}.</div>
}
export default Home
import React from 'react'
import App from 'next/app'
import '../styles/global2.scss'
import '../styles/global1.scss'
class MyApp extends App {
render() {
const { Component, pageProps } = this.props
return <Component {...pageProps} />
}
}
export default MyApp
export default function Home() {
return <div className="red-text">This text should be red.</div>
}
import React from 'react'
import App from 'next/app'
import '../styles/global1.scss'
import '../styles/global2.scss'
class MyApp extends App {
render() {
const { Component, pageProps } = this.props
return <Component {...pageProps} />
}
}
export default MyApp
export default function Home() {
return <div className="red-text">This text should be red.</div>
}
import Link from 'next/link'
import { blueText } from './blue.module.scss'
export default function Blue() {
return (
<>
<div id="verify-blue" className={blueText}>
This text should be blue.
</div>
<br />
<Link href="/red" prefetch>
<a id="link-red">Red</a>
</Link>
<br />
<Link href="/none" prefetch={false}>
<a id="link-none">None</a>
</Link>
</>
)
}
import Link from 'next/link'
export default function None() {
return (
<>
<div id="verify-black" style={{ color: 'black' }}>
This text should be black.
</div>
<br />
<Link href="/red" prefetch={false}>
<a id="link-red">Red</a>
</Link>
<br />
<Link href="/blue" prefetch={false}>
<a id="link-blue">Blue</a>
</Link>
</>
)
}
import Link from 'next/link'
import { redText } from './red.module.scss'
export default function Red() {
return (
<>
<div id="verify-red" className={redText}>
This text should be red.
</div>
<br />
<Link href="/blue" prefetch={false}>
<a id="link-blue">Blue</a>
</Link>
<br />
<Link href="/none" prefetch={false}>
<a id="link-none">None</a>
</Link>
</>
)
}
import React from 'react'
import App from 'next/app'
import '../styles/global1.scss'
import '../styles/global2.scss'
class MyApp extends App {
render() {
const { Component, pageProps } = this.props
return <Component {...pageProps} />
}
}
export default MyApp
import Link from 'next/link'
export default function Page1() {
return (
<>
<div className="red-text">This text should be red.</div>
<br />
<input key={'' + Math.random()} id="text-input" type="text" />
<br />
<Link href="/page2">
<a>Switch page</a>
</Link>
</>
)
}
import Link from 'next/link'
export default function Page2() {
return (
<>
<div className="blue-text">This text should be blue.</div>
<br />
<input key={'' + Math.random()} id="text-input" type="text" />
<br />
<Link href="/page1">
<a>Switch page</a>
</Link>
</>
)
}
import React from 'react'
import App from 'next/app'
import '../styles/global1.scss'
import '../styles/global2.scss'
class MyApp extends App {
render() {
const { Component, pageProps } = this.props
return <Component {...pageProps} />
}
}
export default MyApp
export default function Home() {
return <div className="red-text">This text should be red.</div>
}
@import './global1b.scss';
$var: red;
.red-text {
color: $var;
}
$var: purple;
.red-text {
color: $var;
font-weight: bolder;
}
@import './global2b.scss';
$var: blue;
.blue-text {
color: $var;
}
$var: orange;
.blue-text {
color: $var;
font-weight: bolder;
}
module.exports = {
onDemandEntries: {
// Make sure entries are not getting disposed.
maxInactiveAge: 1000 * 60 * 60,
},
experimental: { scss: true },
webpack(cfg) {
cfg.devtool = 'source-map'
return cfg
},
}
const message = 'Why hello there'
module.exports = { message }
@import 'other2.scss';
$var: blue;
.subClass {
composes: className from './other.scss';
background: $var;
}
@import 'other3.scss';
$var: red;
.className {
background: $var;
color: yellow;
}
import * as data from 'example'
import * as classes from 'example/index.module.scss'
function Home() {
return (
<div id="nm-div">
{JSON.stringify(data)} {JSON.stringify(classes)}
</div>
)
}
export default Home
const message = 'Why hello there'
module.exports = { message }
import * as data from 'example'
import * as classes from 'example/index.module.scss'
function Home() {
return (
<div id="nm-div">
{JSON.stringify(data)} {JSON.stringify(classes)}
</div>
)
}
export default Home
import React from 'react'
import App from 'next/app'
import '../styles/global.scss'
class MyApp extends App {
render() {
const { Component, pageProps } = this.props
return <Component {...pageProps} />
}
}
export default MyApp
export default function Home() {
return <div className="red-text">This text should be red.</div>
}
const message = 'Why hello there'
module.exports = { message }
import React from 'react'
import App from 'next/app'
import '../styles/global.scss'
class MyApp extends App {
render() {
const { Component, pageProps } = this.props
return <Component {...pageProps} />
}
}
export default MyApp
export default function Home() {
return <div className="red-text">This text should be red.</div>
}
import React from 'react'
import App from 'next/app'
import '../styles/global.scss'
class MyApp extends App {
render() {
const { Component, pageProps } = this.props
return <Component {...pageProps} />
}
}
export default MyApp
export default function Home() {
return <div className="red-text">This text should be red.</div>
}
import { redText } from './index.module.scss'
export default function Home() {
return (
<div id="verify-red" className={redText}>
This text should be red.
</div>
)
}
import React from 'react'
import App from 'next/app'
import '../../styles/global.scss'
class MyApp extends App {
render() {
const { Component, pageProps } = this.props
return <Component {...pageProps} />
}
}
export default MyApp
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册