1
0
Fork 0
mirror of https://github.com/terribleplan/next.js.git synced 2024-01-19 02:48:18 +00:00

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 // 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() return callback()
} }
@ -59,7 +59,6 @@ function externalsConfig (dir, isServer) {
function optimizationConfig ({dir, dev, isServer, totalPages}) { function optimizationConfig ({dir, dev, isServer, totalPages}) {
if (isServer) { if (isServer) {
return { return {
// runtimeChunk: 'single',
splitChunks: false, splitChunks: false,
minimize: false minimize: false
} }
@ -69,7 +68,12 @@ function optimizationConfig ({dir, dev, isServer, totalPages}) {
runtimeChunk: { runtimeChunk: {
name: CLIENT_STATIC_FILES_RUNTIME_WEBPACK name: CLIENT_STATIC_FILES_RUNTIME_WEBPACK
}, },
splitChunks: false splitChunks: {
cacheGroups: {
default: false,
vendors: false
}
}
} }
if (dev) { if (dev) {
@ -79,21 +83,14 @@ function optimizationConfig ({dir, dev, isServer, totalPages}) {
// Only enabled in production // Only enabled in production
// This logic will create a commons bundle // This logic will create a commons bundle
// with modules that are used in 50% of all pages // with modules that are used in 50% of all pages
return { config.splitChunks.chunks = 'all'
...config, config.splitChunks.cacheGroups.commons = {
splitChunks: { name: 'commons',
chunks: 'all', chunks: 'all',
cacheGroups: { minChunks: totalPages > 2 ? totalPages * 0.5 : 2
default: false,
vendors: false,
commons: {
name: 'commons',
chunks: 'all',
minChunks: totalPages > 2 ? totalPages * 0.5 : 2
}
}
}
} }
return config
} }
type BaseConfigContext = {| type BaseConfigContext = {|

View file

@ -8,7 +8,7 @@ export default class BuildManifestPlugin {
apply (compiler: any) { apply (compiler: any) {
compiler.hooks.emit.tapAsync('NextJsBuildManifest', (compilation, callback) => { compiler.hooks.emit.tapAsync('NextJsBuildManifest', (compilation, callback) => {
const {chunks} = compilation const {chunks} = compilation
const assetMap = {pages: {}, css: []} const assetMap = {pages: {}}
const mainJsChunk = chunks.find((c) => c.name === CLIENT_STATIC_FILES_RUNTIME_MAIN) 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)) : [] 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) { 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)) {
if (/\.map$/.test(file) || /\.hot-update\.js$/.test(file) || !/\.js$/.test(file)) {
continue 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)) { if (IS_BUNDLED_PAGE_REGEX.exec(file)) {
continue continue
} }
@ -52,35 +56,6 @@ export default class BuildManifestPlugin {
assetMap.pages[`/${pagePath.replace(/\\/g, '/')}`] = [...filesForEntry, ...mainJsFiles] 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') { if (typeof assetMap.pages['/index'] !== 'undefined') {
assetMap.pages['/'] = assetMap.pages['/index'] assetMap.pages['/'] = assetMap.pages['/index']
} }

View file

@ -1,6 +1,7 @@
import webpack from 'webpack' import webpack from 'webpack'
import { RawSource } from 'webpack-sources' import { RawSource } from 'webpack-sources'
import { join, relative, dirname } from 'path' import { join, relative, dirname } from 'path'
import {IS_BUNDLED_PAGE_REGEX} from '../../../lib/constants'
const SSR_MODULE_CACHE_FILENAME = 'ssr-module-cache.js' const SSR_MODULE_CACHE_FILENAME = 'ssr-module-cache.js'
@ -31,7 +32,14 @@ export default class NextJsSsrImportPlugin {
compilation.mainTemplate.hooks.localVars.intercept({ compilation.mainTemplate.hooks.localVars.intercept({
register (tapInfo) { register (tapInfo) {
if (tapInfo.name === 'MainTemplate') { if (tapInfo.name === 'MainTemplate') {
const originalFn = tapInfo.fn
tapInfo.fn = (source, chunk) => { 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 pagePath = join(outputPath, dirname(chunk.name))
const relativePathToBaseDir = relative(pagePath, join(outputPath, SSR_MODULE_CACHE_FILENAME)) const relativePathToBaseDir = relative(pagePath, join(outputPath, SSR_MODULE_CACHE_FILENAME))
// Make sure even in windows, the path looks like in unix // Make sure even in windows, the path looks like in unix

View file

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

View file

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

View file

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

View file

@ -1,3 +1,4 @@
import css from './hello-webpack-css.css' import css from './hello-webpack-css.css'
import sass from './hello-webpack-sass.scss'
import framework from 'css-framework/framework.css' 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') const webpack = require('webpack')
// module.exports = withCSS({ const path = require('path')
module.exports = { module.exports = withCSS(withSass({
onDemandEntries: { onDemandEntries: {
// Make sure entries are not getting disposed. // Make sure entries are not getting disposed.
maxInactiveAge: 1000 * 60 * 60 maxInactiveAge: 1000 * 60 * 60
@ -14,6 +15,16 @@ module.exports = {
staticFolder: '/static' staticFolder: '/static'
}, },
webpack (config, {buildId}) { 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( config.plugins.push(
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env.CONFIG_BUILD_ID': JSON.stringify(buildId) 'process.env.CONFIG_BUILD_ID': JSON.stringify(buildId)
@ -22,5 +33,4 @@ module.exports = {
return config return config
} }
// }) }))
}

View file

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

View file

@ -2,6 +2,8 @@
import webdriver from 'next-webdriver' import webdriver from 'next-webdriver'
import { waitFor } from 'next-test-utils' import { waitFor } from 'next-test-utils'
import { readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
export default (context, render) => { export default (context, render) => {
describe('Configuration', () => { describe('Configuration', () => {
@ -17,5 +19,81 @@ export default (context, render) => {
expect(serverClientText).toBe('/static') expect(serverClientText).toBe('/static')
browser.close() 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 // pre-build all pages at the start
await Promise.all([ await Promise.all([
renderViaHTTP(context.appPort, '/next-config'), renderViaHTTP(context.appPort, '/next-config'),
renderViaHTTP(context.appPort, '/build-id') renderViaHTTP(context.appPort, '/build-id'),
// renderViaHTTP(context.appPort, '/webpack-css') renderViaHTTP(context.appPort, '/webpack-css')
]) ])
}) })
afterAll(() => killApp(context.server)) afterAll(() => killApp(context.server))

View file

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

View file

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

545
yarn.lock

File diff suppressed because it is too large Load diff