Skip to content

Commit

Permalink
Global CSS Support (#8710)
Browse files Browse the repository at this point in the history
* Global CSS Support

* Fix webpack configuration

* oneOf rule isn't necessary yet

* Adjust CSS chunk naming

* Begin testing CSS behavior

* Add another test TODO

* Replace null-loader with ignore-loader

* Turn on chunks for new CSS feature

* Fix multi test suite

* Test CSS import order

* Test style HMR

* Test CSS compilation

* Test compilation and prefixing together

* Verify CSS styling works for Development and Production

* Add missing TODO

* Remove unnecessary test

* Adjust TODO message

* Hide page until React hydrates

* Revert "Hide page until React hydrates"

This reverts commit 898d4e0.

* Hide FOUC during development

* Test CSS imports

* Update tests TODO

* Add fixture for url() test

* Test `file-loader` support in CSS files

* Use a simple variant of cssnano

* Self-import

* Undo bundling

* Implement suggestion
  • Loading branch information
Timer committed Sep 17, 2019
1 parent 20aa1e4 commit 65358b7
Show file tree
Hide file tree
Showing 51 changed files with 1,383 additions and 8 deletions.
108 changes: 107 additions & 1 deletion packages/next/build/webpack-config.ts
@@ -1,5 +1,7 @@
import crypto from 'crypto'
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
import OptimizeCssAssetsPlugin from 'optimize-css-assets-webpack-plugin'
import path from 'path'
// @ts-ignore: Currently missing types
import PnpWebpackPlugin from 'pnp-webpack-plugin'
Expand All @@ -20,6 +22,7 @@ import {
SERVER_DIRECTORY,
SERVERLESS_DIRECTORY,
} from '../next-server/lib/constants'
import { findPageFile } from '../server/lib/find-page-file'
import { WebpackEntrypoints } from './entries'
import BuildManifestPlugin from './webpack/plugins/build-manifest-plugin'
import { ChunkGraphPlugin } from './webpack/plugins/chunk-graph-plugin'
Expand Down Expand Up @@ -291,6 +294,17 @@ export default async function getBaseWebpackConfig(
? 'anonymous'
: config.crossOrigin

let customAppFile: string | null = config.experimental.css
? await findPageFile(
path.join(dir, 'pages'),
'/_app',
config.pageExtensions
)
: null
if (customAppFile) {
customAppFile = path.resolve(path.join(dir, 'pages', customAppFile))
}

let webpackConfig: webpack.Configuration = {
devtool,
mode: webpackMode,
Expand Down Expand Up @@ -419,11 +433,26 @@ export default async function getBaseWebpackConfig(
: { name: CLIENT_STATIC_FILES_RUNTIME_WEBPACK },
minimize: !(dev || isServer),
minimizer: [
// Minify JavaScript
new TerserPlugin({
...terserPluginConfig,
terserOptions,
}),
],
// Minify CSS
config.experimental.css &&
new OptimizeCssAssetsPlugin({
cssProcessorOptions: {
map: {
// `inline: false` generates the source map in a separate file.
// Otherwise, the CSS file is needlessly large.
inline: false,
// `annotation: true` appends the `sourceMappingURL` to the end
// of the CSS file for DevTools to find them.
annotation: true,
},
},
}),
].filter(Boolean),
},
recordsPath: path.join(outputPath, 'records.json'),
context: dir,
Expand Down Expand Up @@ -518,6 +547,75 @@ export default async function getBaseWebpackConfig(
},
use: defaultLoaders.babel,
},
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 && require.resolve('style-loader'),
// 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: !dev },
},

// 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: !dev,
},
},
].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 &&
({
loader: require.resolve('file-loader'),
issuer: {
// file-loader is only used for CSS files, e.g. url() for a SVG
// or font files
test: /\.css$/,
},
// Exclude extensions that webpack handles by default
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
options: {
name: 'static/media/[name].[hash].[ext]',
},
} as webpack.RuleSetRule),
].filter(Boolean),
},
plugins: [
Expand Down Expand Up @@ -652,6 +750,14 @@ export default async function getBaseWebpackConfig(
clientManifest: config.experimental.granularChunks,
modern: config.experimental.modern,
}),
// Extract CSS as CSS file(s) in the client-side production bundle.
config.experimental.css &&
!isServer &&
!dev &&
new MiniCssExtractPlugin({
filename: 'static/css/[contenthash].css',
chunkFilename: 'static/css/[contenthash].chunk.css',
}),
tracer &&
new ProfilingPlugin({
tracer,
Expand Down
22 changes: 20 additions & 2 deletions packages/next/client/index.js
Expand Up @@ -238,10 +238,28 @@ function renderReactElement (reactEl, domEl) {

// The check for `.hydrate` is there to support React alternatives like preact
if (isInitialRender) {
ReactDOM.hydrate(reactEl, domEl, markHydrateComplete)
ReactDOM.hydrate(reactEl, domEl, function () {
if (process.env.NODE_ENV !== 'production') {
document
.querySelectorAll('[data-next-hydrating]')
.forEach(function (el) {
el.remove()
})
}
markHydrateComplete()
})
isInitialRender = false
} else {
ReactDOM.render(reactEl, domEl, markRenderComplete)
ReactDOM.render(reactEl, domEl, function () {
if (process.env.NODE_ENV !== 'production') {
document
.querySelectorAll('[data-next-hydrating]')
.forEach(function (el) {
el.remove()
})
}
markRenderComplete()
})
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/next/next-server/lib/utils.ts
Expand Up @@ -140,6 +140,8 @@ export type DocumentProps = DocumentInitialProps & {
inAmpMode: boolean
hybridAmp: boolean
staticMarkup: boolean
isDevelopment: boolean
hasCssMode: boolean
devFiles: string[]
files: string[]
dynamicImports: ManifestItem[]
Expand Down
1 change: 1 addition & 0 deletions packages/next/next-server/server/config.ts
Expand Up @@ -40,6 +40,7 @@ const defaultConfig: { [key: string]: any } = {
(Number(process.env.CIRCLE_NODE_TOTAL) ||
(os.cpus() || { length: 1 }).length) - 1
),
css: false,
documentMiddleware: false,
granularChunks: false,
modern: false,
Expand Down
2 changes: 2 additions & 0 deletions packages/next/next-server/server/next-server.ts
Expand Up @@ -76,6 +76,7 @@ export default class Server {
assetPrefix?: string
canonicalBase: string
documentMiddlewareEnabled: boolean
hasCssMode: boolean
dev?: boolean
}
private compression?: Middleware
Expand Down Expand Up @@ -121,6 +122,7 @@ export default class Server {
canonicalBase: this.nextConfig.amp.canonicalBase,
documentMiddlewareEnabled: this.nextConfig.experimental
.documentMiddleware,
hasCssMode: this.nextConfig.experimental.css,
staticMarkup,
buildId: this.buildId,
generateEtags,
Expand Down
4 changes: 4 additions & 0 deletions packages/next/next-server/server/render.tsx
Expand Up @@ -130,6 +130,7 @@ type RenderOpts = {
runtimeConfig?: { [key: string]: any }
dangerousAsPath: string
assetPrefix?: string
hasCssMode: boolean
err?: Error | null
autoExport?: boolean
nextExport?: boolean
Expand Down Expand Up @@ -168,6 +169,7 @@ function renderDocument(
skeleton,
dynamicImportsIds,
dangerousAsPath,
hasCssMode,
err,
dev,
ampPath,
Expand Down Expand Up @@ -219,6 +221,8 @@ function renderDocument(
canonicalBase={canonicalBase}
ampPath={ampPath}
inAmpMode={inAmpMode}
isDevelopment={!!dev}
hasCssMode={hasCssMode}
hybridAmp={hybridAmp}
staticMarkup={staticMarkup}
devFiles={devFiles}
Expand Down
11 changes: 11 additions & 0 deletions packages/next/package.json
Expand Up @@ -84,20 +84,28 @@
"conf": "5.0.0",
"content-type": "1.0.4",
"cookie": "0.4.0",
"css-loader": "3.2.0",
"devalue": "2.0.0",
"etag": "1.8.1",
"file-loader": "4.2.0",
"find-up": "4.0.0",
"fork-ts-checker-webpack-plugin": "1.3.4",
"fresh": "0.5.2",
"ignore-loader": "0.1.2",
"is-docker": "2.0.0",
"jest-worker": "24.9.0",
"launch-editor": "2.2.1",
"loader-utils": "1.2.3",
"mini-css-extract-plugin": "0.8.0",
"mkdirp": "0.5.1",
"node-fetch": "2.6.0",
"optimize-css-assets-webpack-plugin": "5.0.3",
"ora": "3.4.0",
"path-to-regexp": "2.1.0",
"pnp-webpack-plugin": "1.5.0",
"postcss-flexbugs-fixes": "4.1.0",
"postcss-loader": "3.0.0",
"postcss-preset-env": "6.7.0",
"prop-types": "15.7.2",
"prop-types-exact": "1.2.0",
"raw-body": "2.4.0",
Expand All @@ -107,6 +115,7 @@
"source-map": "0.6.1",
"string-hash": "1.1.3",
"strip-ansi": "5.2.0",
"style-loader": "1.0.0",
"styled-jsx": "3.2.2",
"terser": "4.0.0",
"unfetch": "4.1.0",
Expand Down Expand Up @@ -141,9 +150,11 @@
"@types/find-up": "2.1.1",
"@types/fresh": "0.5.0",
"@types/loader-utils": "1.1.3",
"@types/mini-css-extract-plugin": "0.8.0",
"@types/mkdirp": "0.5.2",
"@types/nanoid": "2.0.0",
"@types/node-fetch": "2.3.4",
"@types/optimize-css-assets-webpack-plugin": "5.0.0",
"@types/react": "16.8.18",
"@types/react-dom": "16.8.4",
"@types/react-is": "16.7.1",
Expand Down
19 changes: 19 additions & 0 deletions packages/next/pages/_document.tsx
Expand Up @@ -344,6 +344,25 @@ export class Head extends Component<

return (
<head {...this.props}>
{this.context._documentProps.isDevelopment &&
this.context._documentProps.hasCssMode && (
<>
<style
data-next-hydrating
dangerouslySetInnerHTML={{
__html: `body{-webkit-animation:-next-hydrating 3s steps(1,end) 0s 1 normal both;-moz-animation:-next-hydrating 3s steps(1,end) 0s 1 normal both;-ms-animation:-next-hydrating 3s steps(1,end) 0s 1 normal both;animation:-next-hydrating 3s steps(1,end) 0s 1 normal both}@-webkit-keyframes -next-hydrating{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -next-hydrating{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -next-hydrating{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -next-hydrating{from{visibility:hidden}to{visibility:visible}}@keyframes -next-hydrating{from{visibility:hidden}to{visibility:visible}}`,
}}
/>
<noscript>
<style
data-next-hydrating
dangerouslySetInnerHTML={{
__html: `body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}`,
}}
/>
</noscript>
</>
)}
{children}
{head}
<meta
Expand Down
@@ -0,0 +1,12 @@
import React from 'react'
import App from 'next/app'
import '../styles/global.css'

class MyApp extends App {
render () {
const { Component, pageProps } = this.props
return <Component {...pageProps} />
}
}

export default MyApp
@@ -0,0 +1,3 @@
export default function Home () {
return <div className='red-text'>This text should be red.</div>
}
@@ -0,0 +1,5 @@
@media (480px <= width < 768px) {
::placeholder {
color: green;
}
}
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,5 @@
import '../styles/global.css'

export default function Home () {
return <div className='red-text'>This text should be red.</div>
}
@@ -0,0 +1,3 @@
.red-text {
color: red;
}
5 changes: 5 additions & 0 deletions test/integration/css/fixtures/invalid-global/pages/index.js
@@ -0,0 +1,5 @@
import '../styles/global.css'

export default function Home () {
return <div className='red-text'>This text should be red.</div>
}
@@ -0,0 +1,3 @@
.red-text {
color: red;
}
13 changes: 13 additions & 0 deletions test/integration/css/fixtures/multi-global-reversed/pages/_app.js
@@ -0,0 +1,13 @@
import React from 'react'
import App from 'next/app'
import '../styles/global2.css'
import '../styles/global1.css'

class MyApp extends App {
render () {
const { Component, pageProps } = this.props
return <Component {...pageProps} />
}
}

export default MyApp
@@ -0,0 +1,3 @@
export default function Home () {
return <div className='red-text'>This text should be red.</div>
}
@@ -0,0 +1,3 @@
.red-text {
color: red;
}
@@ -0,0 +1,3 @@
.blue-text {
color: blue;
}

0 comments on commit 65358b7

Please sign in to comment.