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] CSS Module Support #9686

Merged
merged 21 commits into from
Dec 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import loaderUtils from 'loader-utils'
import path from 'path'
import webpack from 'webpack'

export function getCssModuleLocalIdent(
context: webpack.loader.LoaderContext,
_: any,
exportName: string,
options: object
) {
const relativePath = path.posix.relative(
context.rootContext,
context.resourcePath
)

// 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]'

// Generate a hash to make the class name unique.
const hash = loaderUtils.getHashDigest(
Buffer.from(`filePath:${relativePath}#className:${exportName}`),
'md5',
'base64',
5
)

// Have webpack interpolate the `[folder]` or `[name]` to its real value.
return loaderUtils
.interpolateName(
context,
fileNameOrFolder + '_' + exportName + '__' + hash,
options
)
.replace(
// Webpack name interpolation returns `about.module_root__2oFM9` for
// `.root {}` inside a file named `about.module.css`. Let's simplify
// this.
/\.module_/,
'_'
)
}
91 changes: 85 additions & 6 deletions packages/next/build/webpack/config/blocks/css/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import curry from 'lodash.curry'
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
import path from 'path'
import { Configuration } from 'webpack'
import webpack, { Configuration } from 'webpack'
import { loader } from '../../helpers'
import { ConfigurationContext, ConfigurationFn, pipe } from '../../utils'
import { getGlobalImportError } from './messages'
import { getCssModuleLocalIdent } from './getCssModuleLocalIdent'
import { getGlobalImportError, getModuleImportError } from './messages'
import { getPostCssPlugins } from './plugins'
import webpack from 'webpack'

function getStyleLoader({
function getClientStyleLoader({
isDevelopment,
}: {
isDevelopment: boolean
Expand Down Expand Up @@ -73,14 +73,93 @@ export const css = curry(async function css(

const fns: ConfigurationFn[] = []

const postCssPlugins = await getPostCssPlugins(ctx.rootDirectory)
// CSS Modules support must be enabled on the server and client so the class
// names are availble for SSR or Prerendering.
fns.push(
loader({
oneOf: [
{
// CSS Modules should never have side effects. This setting will
// allow unused CSS to be removed from the production build.
// We ensure this by disallowing `:global()` CSS at the top-level
// via the `pure` mode in `css-loader`.
sideEffects: false,
// CSS Modules are activated via this specific extension.
test: /\.module\.css$/,
// CSS Modules are only supported in the user's application. We're
// not yet allowing CSS imports _within_ `node_modules`.
issuer: {
include: [ctx.rootDirectory],
exclude: /node_modules/,
},

use: ([
// Add appropriate development more or production mode style
// loader
ctx.isClient &&
getClientStyleLoader({ isDevelopment: ctx.isDevelopment }),

// Resolve CSS `@import`s and `url()`s
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
sourceMap: true,
onlyLocals: ctx.isServer,
modules: {
// Disallow global style exports so we can code-split CSS and
// not worry about loading order.
mode: 'pure',
// Generate a friendly production-ready name so it's
// reasonably understandable. The same name is used for
// development.
// TODO: Consider making production reduce this to a single
// character?
getLocalIdent: getCssModuleLocalIdent,
},
},
},

// Compile CSS
{
loader: require.resolve('postcss-loader'),
options: {
ident: 'postcss',
plugins: postCssPlugins,
sourceMap: true,
},
},
] as webpack.RuleSetUseItem[]).filter(Boolean),
},
],
})
)

// Throw an error for CSS Modules used outside their supported scope
fns.push(
loader({
oneOf: [
{
test: /\.module\.css$/,
use: {
loader: 'error-loader',
options: {
reason: getModuleImportError(),
},
},
},
],
})
)

if (ctx.isServer) {
fns.push(
loader({
oneOf: [{ test: /\.css$/, use: require.resolve('ignore-loader') }],
})
)
} else if (ctx.customAppFile) {
const postCssPlugins = await getPostCssPlugins(ctx.rootDirectory)
fns.push(
loader({
oneOf: [
Expand All @@ -96,7 +175,7 @@ export const css = curry(async function css(
use: [
// Add appropriate development more or production mode style
// loader
getStyleLoader({ isDevelopment: ctx.isDevelopment }),
getClientStyleLoader({ isDevelopment: ctx.isDevelopment }),

// Resolve CSS `@import`s and `url()`s
{
Expand Down
7 changes: 7 additions & 0 deletions packages/next/build/webpack/config/blocks/css/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,10 @@ export function getGlobalImportError(file: string | null) {
file ? file : 'pages/_app.js'
)}.\nRead more: https://err.sh/next.js/global-css`
}

export function getModuleImportError() {
// TODO: Read more link
return `CSS Modules ${chalk.bold(
'cannot'
)} be imported from within ${chalk.bold('node_modules')}.`
}
20 changes: 17 additions & 3 deletions packages/next/client/page-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,20 @@ function supportsPreload(el) {

const hasPreload = supportsPreload(document.createElement('link'))

function preloadScript(url) {
function preloadLink(url, resourceType) {
const link = document.createElement('link')
link.rel = 'preload'
link.crossOrigin = process.crossOrigin
link.href = url
link.as = 'script'
link.as = resourceType
document.head.appendChild(link)
}

function loadStyle(url) {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.crossOrigin = process.crossOrigin
link.href = url
document.head.appendChild(link)
}

Expand Down Expand Up @@ -105,6 +113,12 @@ export default class PageLoader {
) {
this.loadScript(d, route, false)
}
if (
/\.css$/.test(d) &&
!document.querySelector(`link[rel=stylesheet][href^="${d}"]`)
) {
loadStyle(d) // FIXME: handle failure
}
})
this.loadRoute(route)
this.loadingRoutes[route] = true
Expand Down Expand Up @@ -228,7 +242,7 @@ export default class PageLoader {
// If not fall back to loading script tags before the page is loaded
// https://caniuse.com/#feat=link-rel-preload
if (hasPreload) {
preloadScript(url)
preloadLink(url, url.match(/\.css$/) ? 'style' : 'script')
return
}

Expand Down
2 changes: 1 addition & 1 deletion packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
"conf": "5.0.0",
"content-type": "1.0.4",
"cookie": "0.4.0",
"css-loader": "3.2.0",
"css-loader": "3.3.0",
"cssnano-simple": "1.0.0",
"devalue": "2.0.1",
"etag": "1.8.1",
Expand Down
9 changes: 9 additions & 0 deletions test/integration/css/fixtures/basic-module/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { redText } from './index.module.css'

export default function Home() {
return (
<div id="verify-red" className={redText}>
This text should be red.
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.redText {
color: red;
}
9 changes: 9 additions & 0 deletions test/integration/css/fixtures/dev-module/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { redText } from './index.module.css'

export default function Home() {
return (
<div id="verify-red" className={redText}>
This text should be red.
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.redText {
color: red;
}
15 changes: 15 additions & 0 deletions test/integration/css/fixtures/hmr-module/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { redText } from './index.module.css'

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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.redText {
color: red;
}

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

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

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

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

7 changes: 7 additions & 0 deletions test/integration/css/fixtures/invalid-module/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as classes from 'example'

function Home() {
return <div>This should fail at build time {JSON.stringify(classes)}.</div>
}

export default Home
20 changes: 20 additions & 0 deletions test/integration/css/fixtures/multi-module/pages/blue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Link from 'next/link'
import { blueText } from './blue.module.css'

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>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.blueText {
color: blue;
}
19 changes: 19 additions & 0 deletions test/integration/css/fixtures/multi-module/pages/none.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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>
</>
)
}
20 changes: 20 additions & 0 deletions test/integration/css/fixtures/multi-module/pages/red.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Link from 'next/link'
import { redText } from './red.module.css'

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>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.redText {
color: red;
}

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

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

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

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

Loading