Skip to content

Commit

Permalink
Informative Error for Invalid Global CSS (#8958)
Browse files Browse the repository at this point in the history
* Informative Error for Invalid Global CSS
This adds a helpful error message with a (basic) err.sh link for invalid Global CSS usage.

We'll want to expand on this topic more and offer alternatives when CSS Modules support lands.

* Update expected error message
  • Loading branch information
Timer authored Oct 4, 2019
1 parent d299811 commit c9d9ff6
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 93 deletions.
11 changes: 11 additions & 0 deletions errors/global-css.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Global CSS Must Be in Your Custom <App>

#### Why This Error Occurred

An attempt to import Global CSS from a file other than `pages/_app.js` was made.

Global CSS cannot be used in files other than your Custom `<App>` due to its side-effects and ordering problems.

#### Possible Ways to Fix It

Relocate all Global CSS imports to your `pages/_app.js` file.
209 changes: 119 additions & 90 deletions packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import chalk from 'chalk'
import crypto from 'crypto'
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
Expand Down Expand Up @@ -497,6 +498,7 @@ export default async function getBaseWebpackConfig(
// The loaders Next.js provides
alias: [
'emit-file-loader',
'error-loader',
'next-babel-loader',
'next-client-pages-loader',
'next-data-loader',
Expand Down Expand Up @@ -553,100 +555,127 @@ export default async function getBaseWebpackConfig(
config.experimental.css &&
// Support CSS imports
({
test: /\.css$/,
issuer: { include: [customAppFile].filter(Boolean) },
use: isServer
? // Global CSS is ignored on the server because it's only needed
// on the client-side.
require.resolve('ignore-loader')
: [
// During development we load CSS via JavaScript so we can
// hot reload it without refreshing the page.
dev && {
loader: require.resolve('style-loader'),
options: {
// By default, style-loader injects CSS into the bottom
// of <head>. This causes ordering problems between dev
// and prod. To fix this, we render a <noscript> tag as
// an anchor for the styles to be placed before. These
// styles will be applied _before_ <style jsx global>.
insert: function(element: Node) {
// These elements should always exist. If they do not,
// this code should fail.
var anchorElement = document.querySelector(
'#__next_css__DO_NOT_USE__'
)!
var parentNode = anchorElement.parentNode! // Normally <head>

// Each style tag should be placed right before our
// anchor. By inserting before and not after, we do not
// need to track the last inserted element.
parentNode.insertBefore(element, anchorElement)

// Remember: this is development only code.
//
// After styles are injected, we need to remove the
// <style> tags that set `body { display: none; }`.
//
// We use `requestAnimationFrame` as a way to defer
// this operation since there may be multiple style
// tags.
;(self.requestAnimationFrame || setTimeout)(function() {
for (
var x = document.querySelectorAll(
'[data-next-hide-fouc]'
),
i = x.length;
i--;

) {
x[i].parentNode!.removeChild(x[i])
}
})
},
},
},
// When building for production we extract CSS into
// separate files.
!dev && {
loader: MiniCssExtractPlugin.loader,
options: {},
},
oneOf: [
{
test: /\.css$/,
issuer: { include: [customAppFile].filter(Boolean) },
use: isServer
? // Global CSS is ignored on the server because it's only needed
// on the client-side.
require.resolve('ignore-loader')
: [
// During development we load CSS via JavaScript so we can
// hot reload it without refreshing the page.
dev && {
loader: require.resolve('style-loader'),
options: {
// By default, style-loader injects CSS into the bottom
// of <head>. This causes ordering problems between dev
// and prod. To fix this, we render a <noscript> tag as
// an anchor for the styles to be placed before. These
// styles will be applied _before_ <style jsx global>.
insert: function(element: Node) {
// These elements should always exist. If they do not,
// this code should fail.
var anchorElement = document.querySelector(
'#__next_css__DO_NOT_USE__'
)!
var parentNode = anchorElement.parentNode! // Normally <head>

// Resolve CSS `@import`s and `url()`s
{
loader: require.resolve('css-loader'),
options: { importLoaders: 1, sourceMap: true },
},
// Each style tag should be placed right before our
// anchor. By inserting before and not after, we do not
// need to track the last inserted element.
parentNode.insertBefore(element, anchorElement)

// Remember: this is development only code.
//
// After styles are injected, we need to remove the
// <style> tags that set `body { display: none; }`.
//
// We use `requestAnimationFrame` as a way to defer
// this operation since there may be multiple style
// tags.
;(self.requestAnimationFrame || setTimeout)(
function() {
for (
var x = document.querySelectorAll(
'[data-next-hide-fouc]'
),
i = x.length;
i--;

// Compile CSS
{
loader: require.resolve('postcss-loader'),
options: {
ident: 'postcss',
plugins: () => [
// Make Flexbox behave like the spec cross-browser.
require('postcss-flexbugs-fixes'),
// Run Autoprefixer and compile new CSS features.
require('postcss-preset-env')({
autoprefixer: {
// Disable legacy flexbox support
flexbox: 'no-2009',
) {
x[i].parentNode!.removeChild(x[i])
}
}
)
},
// Enable CSS features that have shipped to the
// web platform, i.e. in 2+ browsers unflagged.
stage: 3,
}),
],
sourceMap: true,
},
},
// When building for production we extract CSS into
// separate files.
!dev && {
loader: MiniCssExtractPlugin.loader,
options: {},
},

// Resolve CSS `@import`s and `url()`s
{
loader: require.resolve('css-loader'),
options: { importLoaders: 1, sourceMap: true },
},

// Compile CSS
{
loader: require.resolve('postcss-loader'),
options: {
ident: 'postcss',
plugins: () => [
// Make Flexbox behave like the spec cross-browser.
require('postcss-flexbugs-fixes'),
// Run Autoprefixer and compile new CSS features.
require('postcss-preset-env')({
autoprefixer: {
// Disable legacy flexbox support
flexbox: 'no-2009',
},
// Enable CSS features that have shipped to the
// web platform, i.e. in 2+ browsers unflagged.
stage: 3,
}),
],
sourceMap: true,
},
},
].filter(Boolean),
// A global CSS import always has side effects. Webpack will tree
// shake the CSS without this option if the issuer claims to have
// no side-effects.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
{
test: /\.css$/,
use: isServer
? require.resolve('ignore-loader')
: {
loader: 'error-loader',
options: {
reason:
`Global CSS ${chalk.bold(
'cannot'
)} be imported from files other than your ${chalk.bold(
'Custom <App>'
)}. Please move all global CSS imports to ${chalk.cyan(
customAppFile
? path.relative(dir, customAppFile)
: 'pages/_app.js'
)}.\n` +
`Read more: https://err.sh/next.js/global-css`,
},
},
},
].filter(Boolean),
// A global CSS import always has side effects. Webpack will tree
// shake the CSS without this option if the issuer claims to have
// no side-effects.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
],
} as webpack.RuleSetRule),
config.experimental.css &&
({
Expand Down
13 changes: 13 additions & 0 deletions packages/next/build/webpack/loaders/error-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import loaderUtils from 'loader-utils'
import { loader } from 'webpack'

const ErrorLoader: loader.Loader = function() {
const options = loaderUtils.getOptions(this) || {}

const { reason = 'An unknown error has occurred' } = options

const err = new Error(reason)
this.emitError(err)
}

export default ErrorLoader
6 changes: 3 additions & 3 deletions test/integration/css/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ describe('CSS Support', () => {
})
expect(stderr).toContain('Failed to compile')
expect(stderr).toContain('styles/global.css')
expect(stderr).toContain('no loaders are configured to process this file')
expect(stderr).toContain('Please move all global CSS imports')
})
})

Expand All @@ -211,7 +211,7 @@ describe('CSS Support', () => {
})
expect(stderr).toContain('Failed to compile')
expect(stderr).toContain('styles/global.css')
expect(stderr).toContain('no loaders are configured to process this file')
expect(stderr).toContain('Please move all global CSS imports')
})
})

Expand All @@ -228,7 +228,7 @@ describe('CSS Support', () => {
})
expect(stderr).toContain('Failed to compile')
expect(stderr).toContain('styles/global.css')
expect(stderr).toContain('no loaders are configured to process this file')
expect(stderr).toContain('Please move all global CSS imports')
})
})

Expand Down

0 comments on commit c9d9ff6

Please sign in to comment.