Add support for rendering .css chunks (#4861)

Depends on https://github.com/zeit/next-plugins/pull/228

Failing tests are expected as `@zeit/next-css` has to be updated/released first.

This implements rendering of `.css` chunks. Effectively removing the custom document requirement when adding next-css/sass/less/stylus.
This commit is contained in:
Tim Neutkens 2018-07-30 15:48:02 +02:00 committed by GitHub
parent 7282f43f7b
commit 183866a96d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 642 additions and 171 deletions

View file

@ -41,7 +41,7 @@ function externalsConfig (dir, isServer) {
}
// Webpack itself has to be compiled because it doesn't always use module relative paths
if (res.match(/node_modules[/\\]webpack/)) {
if (res.match(/node_modules[/\\]webpack/) || res.match(/node_modules[/\\]css-loader/)) {
return callback()
}
@ -59,7 +59,6 @@ function externalsConfig (dir, isServer) {
function optimizationConfig ({dir, dev, isServer, totalPages}) {
if (isServer) {
return {
// runtimeChunk: 'single',
splitChunks: false,
minimize: false
}
@ -69,7 +68,12 @@ function optimizationConfig ({dir, dev, isServer, totalPages}) {
runtimeChunk: {
name: CLIENT_STATIC_FILES_RUNTIME_WEBPACK
},
splitChunks: false
splitChunks: {
cacheGroups: {
default: false,
vendors: false
}
}
}
if (dev) {
@ -79,21 +83,14 @@ function optimizationConfig ({dir, dev, isServer, totalPages}) {
// Only enabled in production
// This logic will create a commons bundle
// with modules that are used in 50% of all pages
return {
...config,
splitChunks: {
chunks: 'all',
cacheGroups: {
default: false,
vendors: false,
commons: {
name: 'commons',
chunks: 'all',
minChunks: totalPages > 2 ? totalPages * 0.5 : 2
}
}
}
config.splitChunks.chunks = 'all'
config.splitChunks.cacheGroups.commons = {
name: 'commons',
chunks: 'all',
minChunks: totalPages > 2 ? totalPages * 0.5 : 2
}
return config
}
type BaseConfigContext = {|

View file

@ -8,7 +8,7 @@ export default class BuildManifestPlugin {
apply (compiler: any) {
compiler.hooks.emit.tapAsync('NextJsBuildManifest', (compilation, callback) => {
const {chunks} = compilation
const assetMap = {pages: {}, css: []}
const assetMap = {pages: {}}
const mainJsChunk = chunks.find((c) => c.name === CLIENT_STATIC_FILES_RUNTIME_MAIN)
const mainJsFiles = mainJsChunk && mainJsChunk.files.length > 0 ? mainJsChunk.files.filter((file) => /\.js$/.test(file)) : []
@ -35,12 +35,16 @@ export default class BuildManifestPlugin {
}
for (const file of chunk.files) {
// Only `.js` files are added for now. In the future we can also handle other file types.
if (/\.map$/.test(file) || /\.hot-update\.js$/.test(file) || !/\.js$/.test(file)) {
if (/\.map$/.test(file) || /\.hot-update\.js$/.test(file)) {
continue
}
// These are manually added to _document.js
// Only `.js` and `.css` files are added for now. In the future we can also handle other file types.
if (!/\.js$/.test(file) && !/\.css$/.test(file)) {
continue
}
// The page bundles are manually added to _document.js as they need extra properties
if (IS_BUNDLED_PAGE_REGEX.exec(file)) {
continue
}
@ -52,35 +56,6 @@ export default class BuildManifestPlugin {
assetMap.pages[`/${pagePath.replace(/\\/g, '/')}`] = [...filesForEntry, ...mainJsFiles]
}
for (const chunk of chunks) {
if (!chunk.name || !chunk.files) {
continue
}
const files = []
for (const file of chunk.files) {
if (/\.map$/.test(file)) {
continue
}
if (/\.hot-update\.js$/.test(file)) {
continue
}
if (/\.css$/.exec(file)) {
assetMap.css.push(file)
continue
}
files.push(file)
}
if (files.length > 0) {
assetMap[chunk.name] = files
}
}
if (typeof assetMap.pages['/index'] !== 'undefined') {
assetMap.pages['/'] = assetMap.pages['/index']
}

View file

@ -1,6 +1,7 @@
import webpack from 'webpack'
import { RawSource } from 'webpack-sources'
import { join, relative, dirname } from 'path'
import {IS_BUNDLED_PAGE_REGEX} from '../../../lib/constants'
const SSR_MODULE_CACHE_FILENAME = 'ssr-module-cache.js'
@ -31,7 +32,14 @@ export default class NextJsSsrImportPlugin {
compilation.mainTemplate.hooks.localVars.intercept({
register (tapInfo) {
if (tapInfo.name === 'MainTemplate') {
const originalFn = tapInfo.fn
tapInfo.fn = (source, chunk) => {
// If the chunk is not part of the pages directory we have to keep the original behavior,
// otherwise webpack will error out when the file is used before the compilation finishes
// this is the case with mini-css-extract-plugin
if (!IS_BUNDLED_PAGE_REGEX.exec(chunk.name)) {
return originalFn(source, chunk)
}
const pagePath = join(outputPath, dirname(chunk.name))
const relativePathToBaseDir = relative(pagePath, join(outputPath, SSR_MODULE_CACHE_FILENAME))
// Make sure even in windows, the path looks like in unix

View file

@ -99,7 +99,7 @@
"unfetch": "3.0.0",
"url": "0.11.0",
"uuid": "3.1.0",
"webpack": "4.16.1",
"webpack": "4.16.3",
"webpack-dev-middleware": "3.1.3",
"webpack-hot-middleware": "2.22.2",
"webpack-sources": "1.1.0",
@ -112,7 +112,8 @@
"@taskr/clear": "1.1.0",
"@taskr/esnext": "1.1.0",
"@taskr/watch": "1.1.0",
"@zeit/next-css": "0.0.7",
"@zeit/next-css": "0.2.1-canary.1",
"@zeit/next-sass": "0.2.1-canary.1",
"@zeit/next-typescript": "1.1.0",
"babel-eslint": "8.2.2",
"babel-jest": "21.2.0",
@ -135,6 +136,7 @@
"mkdirp": "0.5.1",
"node-fetch": "1.7.3",
"node-notifier": "5.1.2",
"node-sass": "4.9.2",
"nyc": "11.2.1",
"react": "16.4.0",
"react-dom": "16.4.0",

View file

@ -49,15 +49,41 @@ export class Head extends Component {
return null
}
return files.map((file) => (
<link
return files.map((file) => {
// Only render .js files here
if(!/\.js$/.exec(file)) {
return null
}
return <link
key={file}
nonce={this.props.nonce}
rel='preload'
href={`${assetPrefix}/_next/${file}`}
as='script'
/>
))
})
}
getCssLinks () {
const { assetPrefix, files } = this.context._documentProps
if(!files || files.length === 0) {
return null
}
return files.map((file) => {
// Only render .css files here
if(!/\.css$/.exec(file)) {
return null
}
return <link
key={file}
nonce={this.props.nonce}
rel='stylesheet'
href={`${assetPrefix}/_next/${file}`}
/>
})
}
getPreloadDynamicChunks () {
@ -85,6 +111,7 @@ export class Head extends Component {
<link rel='preload' href={`${assetPrefix}/_next/static/${buildId}/pages/_error.js`} as='script' nonce={this.props.nonce} />
{this.getPreloadDynamicChunks()}
{this.getPreloadMainLinks()}
{this.getCssLinks()}
{styles || null}
{this.props.children}
</head>
@ -122,14 +149,19 @@ export class NextScript extends Component {
return null
}
return files.map((file) => (
<script
return files.map((file) => {
// Only render .js files here
if(!/\.js$/.exec(file)) {
return null
}
return <script
key={file}
src={`${assetPrefix}/_next/${file}`}
nonce={this.props.nonce}
async
/>
))
})
}
getDynamicChunks () {

View file

@ -1,3 +1,3 @@
.helloWorld {
color: red;
font-size: 100px;
}

View file

@ -1,3 +1,4 @@
import css from './hello-webpack-css.css'
import sass from './hello-webpack-sass.scss'
import framework from 'css-framework/framework.css'
export default () => <div className={`${css.helloWorld} ${framework.frameworkClass}`}>Hello World</div>
export default () => <div className={`hello-world ${css.helloWorld} ${sass.helloWorldSass} ${framework.frameworkClass}`}>Hello World</div>

View file

@ -0,0 +1,5 @@
$color: yellow;
.helloWorldSass {
color: $color;
}

View file

@ -1,7 +1,8 @@
// const withCSS = require('@zeit/next-css')
const withCSS = require('@zeit/next-css')
const withSass = require('@zeit/next-sass')
const webpack = require('webpack')
// module.exports = withCSS({
module.exports = {
const path = require('path')
module.exports = withCSS(withSass({
onDemandEntries: {
// Make sure entries are not getting disposed.
maxInactiveAge: 1000 * 60 * 60
@ -14,6 +15,16 @@ module.exports = {
staticFolder: '/static'
},
webpack (config, {buildId}) {
// When next-css is `npm link`ed we have to solve loaders from the project root
const nextLocation = path.join(require.resolve('next/package.json'), '../')
const nextCssNodeModulesLocation = path.join(
require.resolve('@zeit/next-css'),
'../../../node_modules'
)
if (nextCssNodeModulesLocation.indexOf(nextLocation) === -1) {
config.resolveLoader.modules.push(nextCssNodeModulesLocation)
}
config.plugins.push(
new webpack.DefinePlugin({
'process.env.CONFIG_BUILD_ID': JSON.stringify(buildId)
@ -22,5 +33,4 @@ module.exports = {
return config
}
// })
}
}))

View file

@ -1,3 +1,3 @@
.frameworkClass {
font-size: 100px;
background: blue
}

View file

@ -2,6 +2,8 @@
import webdriver from 'next-webdriver'
import { waitFor } from 'next-test-utils'
import { readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
export default (context, render) => {
describe('Configuration', () => {
@ -17,5 +19,81 @@ export default (context, render) => {
expect(serverClientText).toBe('/static')
browser.close()
})
it('should update css styles using hmr', async () => {
let browser
try {
browser = await webdriver(context.appPort, '/webpack-css')
const pTag = await browser.elementByCss('.hello-world')
const initialFontSize = await pTag.getComputedCss('font-size')
expect(initialFontSize).toBe('100px')
const pagePath = join(__dirname, '../', 'components', 'hello-webpack-css.css')
const originalContent = readFileSync(pagePath, 'utf8')
const editedContent = originalContent.replace('100px', '200px')
// Change the page
writeFileSync(pagePath, editedContent, 'utf8')
// wait for 5 seconds
await waitFor(5000)
try {
// Check whether the this page has reloaded or not.
const editedPTag = await browser.elementByCss('.hello-world')
const editedFontSize = await editedPTag.getComputedCss('font-size')
expect(editedFontSize).toBe('200px')
} finally {
// Finally is used so that we revert the content back to the original regardless of the test outcome
// restore the about page content.
writeFileSync(pagePath, originalContent, 'utf8')
}
} finally {
if (browser) {
browser.close()
}
}
})
it('should update sass styles using hmr', async () => {
let browser
try {
browser = await webdriver(context.appPort, '/webpack-css')
const pTag = await browser.elementByCss('.hello-world')
const initialFontSize = await pTag.getComputedCss('color')
expect(initialFontSize).toBe('rgba(255, 255, 0, 1)')
const pagePath = join(__dirname, '../', 'components', 'hello-webpack-sass.scss')
const originalContent = readFileSync(pagePath, 'utf8')
const editedContent = originalContent.replace('yellow', 'red')
// Change the page
writeFileSync(pagePath, editedContent, 'utf8')
// wait for 5 seconds
await waitFor(5000)
try {
// Check whether the this page has reloaded or not.
const editedPTag = await browser.elementByCss('.hello-world')
const editedFontSize = await editedPTag.getComputedCss('color')
expect(editedFontSize).toBe('rgba(255, 0, 0, 1)')
} finally {
// Finally is used so that we revert the content back to the original regardless of the test outcome
// restore the about page content.
writeFileSync(pagePath, originalContent, 'utf8')
}
} finally {
if (browser) {
browser.close()
}
}
})
})
}

View file

@ -24,8 +24,8 @@ describe('Configuration', () => {
// pre-build all pages at the start
await Promise.all([
renderViaHTTP(context.appPort, '/next-config'),
renderViaHTTP(context.appPort, '/build-id')
// renderViaHTTP(context.appPort, '/webpack-css')
renderViaHTTP(context.appPort, '/build-id'),
renderViaHTTP(context.appPort, '/webpack-css')
])
})
afterAll(() => killApp(context.server))

View file

@ -9,15 +9,15 @@ export default function ({ app }, suiteName, render, fetch) {
}
describe(suiteName, () => {
// test('renders css imports', async () => {
// const $ = await get$('/webpack-css')
// expect($('._46QtCORzC4BWRnIseSbG-').text() === 'Hello World')
// })
test('renders css imports', async () => {
const $ = await get$('/webpack-css')
expect($('._46QtCORzC4BWRnIseSbG-').text() === 'Hello World')
})
// test('renders non-js imports from node_modules', async () => {
// const $ = await get$('/webpack-css')
// expect($('._2pRSkKTPDMGLMnmsEkP__J').text() === 'Hello World')
// })
test('renders non-js imports from node_modules', async () => {
const $ = await get$('/webpack-css')
expect($('._2pRSkKTPDMGLMnmsEkP__J').text() === 'Hello World')
})
test('renders server config on the server only', async () => {
const $ = await get$('/next-config')

View file

@ -15,7 +15,7 @@ import webdriver from 'next-webdriver'
import fetch from 'node-fetch'
import dynamicImportTests from '../../basic/test/dynamic'
import security from './security'
import {BUILD_MANIFEST, REACT_LOADABLE_MANIFEST, CLIENT_STATIC_FILES_RUNTIME_MAIN} from 'next/constants'
import {BUILD_MANIFEST, REACT_LOADABLE_MANIFEST} from 'next/constants'
const appDir = join(__dirname, '../')
let appPort
@ -68,8 +68,10 @@ describe('Production Usage', () => {
// test dynamic chunk
resources.push(url + reactLoadableManifest['../../components/hello1'][0].publicPath)
// test main.js
resources.push(url + buildManifest[CLIENT_STATIC_FILES_RUNTIME_MAIN][0])
// test main.js runtime etc
for (const item of buildManifest.pages['/']) {
resources.push(url + item)
}
const responses = await Promise.all(resources.map((resource) => fetch(resource)))

545
yarn.lock

File diff suppressed because it is too large Load diff