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:
parent
7282f43f7b
commit
183866a96d
|
@ -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 = {|
|
||||
|
|
|
@ -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']
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
.helloWorld {
|
||||
color: red;
|
||||
font-size: 100px;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
$color: yellow;
|
||||
|
||||
.helloWorldSass {
|
||||
color: $color;
|
||||
}
|
|
@ -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
|
||||
}
|
||||
// })
|
||||
}
|
||||
}))
|
||||
|
|
2
test/integration/config/node_modules/css-framework/framework.css
generated
vendored
2
test/integration/config/node_modules/css-framework/framework.css
generated
vendored
|
@ -1,3 +1,3 @@
|
|||
.frameworkClass {
|
||||
font-size: 100px;
|
||||
background: blue
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)))
|
||||
|
||||
|
|
Loading…
Reference in a new issue