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

Universal Webpack (#3578)

* Speed up next build

* Document webpack config

* Speed up next build

* Remove comment

* Add comment

* Clean up rules

* Add comments

* Run in parallel

* Push plugins seperately

* Create a new chunk for react

* Don’t uglify react since it’s already uglified. Move react to commons in development

* Use the minified version directly

* Re-add globpattern

* Move loaders into a separate variable

* Add comment linking to Dan’s explanation

* Remove dot

* Add universal webpack

* Initial dev support

* Fix linting

* Add changes from Arunoda's work

* Made next dev works.
But super slow and no HMR support.

* Fix client side hot reload

* Server side hmr

* Only in dev

* Add on-demand-entries client + hot-middleware

* Add .babelrc support

* Speed up on demand entries by running in parallel

* Serve static generated files

* Add missing config in dev

* Add sass support

* Add support for .map

* Add cssloader config and fix .jsx support

* Rename

* use same defaults as css-loader. Fix linting

* Add NoEmitErrorsPlugin

* Add clientBootstrap

* Use webpackhotmiddleware on the multi compiler

* alpha.3

* Use babel 16.2.x

* Fix reloading after error

* Remove comment

* Release 5.0.0-univeral-alpha.1

* Remove check for React 16

* Release 5.0.0-universal-alpha.2

* React hot loader v4

* Use our static file rendering machanism to serve pages.
This should work well since the file path for a page is predictable.

* Release 5.0.0-universal-alpha.3

* Remove optional loaders

* Release 5.0.0-universal-alpha.4

* Remove clientBootstrap

* Remove renderScript

* Make sure pages bundles are served correctly

* Remove unused import

* Revert to using the same code as canary

* Fix hot loader

* Release 5.0.0-universal-alpha.5

* Check if externals dir exist before applying config

* Add typescript support

* Add support for transpiling certain packages in node_modules

Thanks to @giuseppeg’s work in https://github.com/zeit/next.js/pull/3319

* Add BABEL_DISABLE_CACHE support

* Make sourcemaps in production opt-in

* Revert "Add support for transpiling certain packages in node_modules"

This reverts commit d4b1d9babfb4b9ed4f4b12d56d52dee233e862da.

In favor of a better api around this.

* Support typescript through next.config.js

* Remove comments

* Bring back commons.js calculation

* Remove unused dependencies

* Move base.config.js to webpack.js

* Make sure to only invalidate webpackDevMiddleware one after other.

* Allow babel-loder caching by default.

* Add comment about preact support

* Bring back buildir replace

* Remove obsolete plugin

* Remove build replace, speed up build

* Resolve page entries like pages/day/index.js to pages/day.js

* Add componentDidCatch back

* Compile to bundles

* Use config.distDir everywhere

* Make sure the file is an array

* Remove console.log

* Apply optimization to uglifyjs

* Add comment pointing to source

* Create entries the same way in dev and production

* Remove unused and broken pagesGlobPattern

* day/index.js is automatically turned into day.js at build time

* Remove poweredByHeader option

* Load pages with the correct path.

* Release 5.0.0-universal-alpha.6

* Make sure react-dom/server can be overwritten by module-alias

* Only add react-hot-loader babel plugin in dev

* Release 5.0.0-universal-alpha.7

* Revert tests

* Release 5.0.0-universal-alpha.10

* Make sure next/head is working properly.

* Add wepack alias for 'next' back.

* Make sure overriding className in next/head works

* Alias react too

* Add missing r

* Fragment fallback has to wrap the children

* Use min.js

* Remove css.js

* Remove wallaby.js

* Release 5.0.0-universal-alpha.11

* Resolve relative to workdir instead of next

* Make sure we touch the right file

* Resolve next modules

* Remove dotjsx removal plugins since we use webpack on the server

* Revert "Resolve relative to workdir instead of next"

This reverts commit a13f3e4ab565df9e2c9a3dfc8eb4009c0c2e02ed.

* Externalize any locally loaded module lives outside of app dir.

* Remove server aliases

* Check node_modules reliably

* Add symlink to next for tests

* Make sure dynamic imports work locally.
This is why we need it: b545b519b2/lib/MainTemplate.js (L68)
We need to have the finally clause in the above in __webpack_require__.
webpack output option strictModuleExceptionHandling does that.

* dynmaic -> dynamic

* Remove webpack-node-externals

* Make sure dynamic imports support SSR.

* Remove css support in favor of next-css

* Make sure we load path from `/` since it’s included in the path matching

* Catch when ensurepage couldn’t be fulfilled for `.js.map`

* Register require cache flusher for both client and server

* Add comment explaining this is to facilitate hot reloading

* Only load module when needed

* Remove unused modules

* Release 5.0.0-universal-alpha.12

* Only log the `found babel` message once

* Make sure ondemand entries working correctly.
Now we are just using a single instance of OnDemandEntryHandler.

* Better sourcemaps

* Release 5.0.0-universal-alpha.13

* Lock uglify version to 1.1.6

* Release 5.0.0-universal-alpha.14

* Fix a typo.

* Introduce multi-zones support for mircofrontends

* Add section on css
This commit is contained in:
Tim Neutkens 2018-01-30 16:40:52 +01:00 committed by Tim Neutkens
parent 202ceca1af
commit e093441bad
60 changed files with 1222 additions and 1609 deletions

1
asset.js Normal file
View file

@ -0,0 +1 @@
module.exports = require('./dist/lib/asset')

View file

@ -1,11 +1,9 @@
#!/usr/bin/env node #!/usr/bin/env node
import 'source-map-support/register'
import { resolve, join } from 'path' import { resolve, join } from 'path'
import parseArgs from 'minimist' import parseArgs from 'minimist'
import { existsSync, readFileSync } from 'fs' import { existsSync, readFileSync } from 'fs'
import Server from '../server' import Server from '../server'
import { printAndExit } from '../lib/utils' import { printAndExit } from '../lib/utils'
import pkgUp from 'pkg-up'
const argv = parseArgs(process.argv.slice(2), { const argv = parseArgs(process.argv.slice(2), {
alias: { alias: {
@ -64,7 +62,7 @@ srv.start(argv.port, argv.hostname)
.catch((err) => { .catch((err) => {
if (err.code === 'EADDRINUSE') { if (err.code === 'EADDRINUSE') {
let errorMessage = `Port ${argv.port} is already in use.` let errorMessage = `Port ${argv.port} is already in use.`
const pkgAppPath = pkgUp.sync('.') const pkgAppPath = require('pkg-up').sync('.')
const appPackage = JSON.parse(readFileSync(pkgAppPath, 'utf8')) const appPackage = JSON.parse(readFileSync(pkgAppPath, 'utf8'))
const nextScript = Object.entries(appPackage.scripts).find(scriptLine => scriptLine[1] === 'next') const nextScript = Object.entries(appPackage.scripts).find(scriptLine => scriptLine[1] === 'next')
if (nextScript) errorMessage += `\nUse \`npm run ${nextScript[0]} -- -p <some other port>\`.` if (nextScript) errorMessage += `\nUse \`npm run ${nextScript[0]} -- -p <some other port>\`.`

View file

@ -6,6 +6,7 @@ import EventEmitter from '../lib/EventEmitter'
import App from '../lib/app' import App from '../lib/app'
import { loadGetInitialProps, getURL } from '../lib/utils' import { loadGetInitialProps, getURL } from '../lib/utils'
import PageLoader from '../lib/page-loader' import PageLoader from '../lib/page-loader'
import * as asset from '../lib/asset'
// Polyfill Promise globally // Polyfill Promise globally
// This is needed because Webpack2's dynamic loading(common chunks) code // This is needed because Webpack2's dynamic loading(common chunks) code
@ -29,6 +30,9 @@ const {
location location
} = window } = window
// With this, static assets will work across zones
asset.setAssetPrefix(assetPrefix)
const asPath = getURL() const asPath = getURL()
const pageLoader = new PageLoader(buildId, assetPrefix) const pageLoader = new PageLoader(buildId, assetPrefix)
@ -93,10 +97,7 @@ export default async ({ ErrorDebugComponent: passedDebugComponent, stripAnsi: pa
} }
export async function render (props) { export async function render (props) {
// There are some errors we should ignore. if (props.err) {
// Next.js rendering logic knows how to handle them.
// These are specially 404 errors
if (props.err && !props.err.ignore) {
await renderError(props.err) await renderError(props.err)
return return
} }
@ -159,7 +160,8 @@ async function doRender ({ Component, props, hash, err, emitter: emitterProp = e
let isInitialRender = true let isInitialRender = true
function renderReactElement (reactEl, domEl) { function renderReactElement (reactEl, domEl) {
if (isInitialRender) { // The check for `.hydrate` is there to support React alternatives like preact
if (isInitialRender && typeof ReactDOM.hydrate === 'function') {
ReactDOM.hydrate(reactEl, domEl) ReactDOM.hydrate(reactEl, domEl)
isInitialRender = false isInitialRender = false
} else { } else {

View file

@ -1,10 +1,11 @@
import 'react-hot-loader/patch'
import stripAnsi from 'strip-ansi' import stripAnsi from 'strip-ansi'
import initNext, * as next from './' import initNext, * as next from './'
import ErrorDebugComponent from '../lib/error-debug' import ErrorDebugComponent from '../lib/error-debug'
import initOnDemandEntries from './on-demand-entries-client' import initOnDemandEntries from './on-demand-entries-client'
import initWebpackHMR from './webpack-hot-middleware-client' import initWebpackHMR from './webpack-hot-middleware-client'
require('@zeit/source-map-support/browser-source-map-support')
window.next = next window.next = next
initNext({ ErrorDebugComponent, stripAnsi }) initNext({ ErrorDebugComponent, stripAnsi })

View file

@ -3,6 +3,12 @@
import Router from '../lib/router' import Router from '../lib/router'
import fetch from 'unfetch' import fetch from 'unfetch'
const {
__NEXT_DATA__: {
assetPrefix
}
} = window
export default () => { export default () => {
Router.ready(() => { Router.ready(() => {
Router.router.events.on('routeChangeComplete', ping) Router.router.events.on('routeChangeComplete', ping)
@ -10,16 +16,16 @@ export default () => {
async function ping () { async function ping () {
try { try {
const url = `/_next/on-demand-entries-ping?page=${Router.pathname}` const url = `${assetPrefix}/_next/on-demand-entries-ping?page=${Router.pathname}`
const res = await fetch(url, { const res = await fetch(url, {
credentials: 'same-origin' credentials: 'omit'
}) })
const payload = await res.json() const payload = await res.json()
if (payload.invalid) { if (payload.invalid) {
// Payload can be invalid even if the page is not exists. // Payload can be invalid even if the page is not exists.
// So, we need to make sure it's exists before reloading. // So, we need to make sure it's exists before reloading.
const pageRes = await fetch(location.href, { const pageRes = await fetch(location.href, {
credentials: 'same-origin' credentials: 'omit'
}) })
if (pageRes.status === 200) { if (pageRes.status === 200) {
location.reload() location.reload()

View file

@ -1,7 +1,19 @@
import webpackHotMiddlewareClient from 'webpack-hot-middleware/client?overlay=false&reload=true&path=/_next/webpack-hmr' import webpackHotMiddlewareClient from 'webpack-hot-middleware/client?autoConnect=false'
import Router from '../lib/router' import Router from '../lib/router'
const {
__NEXT_DATA__: {
assetPrefix
}
} = window
export default () => { export default () => {
webpackHotMiddlewareClient.setOptionsAndConnect({
overlay: false,
reload: true,
path: `${assetPrefix}/_next/webpack-hmr`
})
const handlers = { const handlers = {
reload (route) { reload (route) {
if (route === '/_error') { if (route === '/_error') {

1
css.js
View file

@ -1 +0,0 @@
module.exports = require('./dist/lib/css')

View file

@ -0,0 +1,15 @@
# The poweredByHeader has been removed
#### Why This Error Occurred
Starting at Next.js version 5.0.0 the `poweredByHeader` option has been removed.
#### Possible Ways to Fix It
If you still want to remove `x-powered-by` you can use one of the custom-server examples.
And then manually remove the header using `res.removeHeader('x-powered-by')`
### Useful Links
- [Custom Server documentation + examples](https://github.com/zeit/next.js#custom-server-and-routing)

View file

@ -0,0 +1,3 @@
export default () => (
<div>About 2</div>
)

View file

@ -0,0 +1,3 @@
export default () => (
<div>Hello Day</div>
)

View file

@ -0,0 +1,71 @@
[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-zones)
# Using multiple zones
With Next.js you can use multiple apps as a single app using it's multi-zones feature.
This is an example showing how to use it.
In this example, we've two apps: 'home' and 'blog'.
We also have a set of rules defined in `rules.json` for the proxy.
Now let's start two of our app using:
```
npm run home
npm run blog
```
Then start the proxy:
```
npm run proxy
```
Now you can visit http://localhost:9000 and access and develop both apps a single app.
### Proxy Rules
This is the place we define rules for our proxy. Here are the rules(in `rules.json`) available for this app:
```json
{
"rules": [
{"pathname": "/blog", "method":["GET", "POST", "OPTIONS"], "dest": "http://localhost:5000"},
{"pathname": "/**", "dest": "http://localhost:4000"}
]
}
```
These rules are based on ZEIT now [path alias](https://zeit.co/docs/features/path-aliases) rules and use [`micro-proxy`](https://github.com/zeit/micro-proxy) as the proxy.
## Special Notes
* All pages should be unique across zones. A page with the same name should not exist in multiple zones. Otherwise, there'll be unexpected behaviour in client side navigation.
* According to the above example, a page named `blog` should not be exist in the `home` zone.
## Production Deployment
Here's how are going to deploy this application into production.
* Open the `now.json` file in both `blog` and `home` directories and change the aliases as you wish.
* Then update `rules-prod.json` accordingly.
* Now deploy both apps:
~~~sh
cd home
now && now alias
cd ../blog
now && now alias
cd ..
~~~
* Finally, set the path alias rules with
~~~sh
now alias with-zones.now.sh -r rules-prod.json
~~~
> You can use a domain name of your choice in the above command instead of `with-zones.now.sh`.
That's it.
Now you can access the final app via: <https://with-zones.now.sh>

2
examples/with-zones/blog/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.next
node_modules

View file

@ -0,0 +1,6 @@
const { NOW_URL } = process.env
const { alias } = require('./now.json')
module.exports = {
assetPrefix: NOW_URL ? `https://${alias}` : 'http://localhost:5000'
}

View file

@ -0,0 +1,3 @@
{
"alias": "with-zones-blog.now.sh"
}

View file

@ -0,0 +1,14 @@
{
"name": "with-zones-blog",
"version": "1.0.0",
"scripts": {
"build": "next build",
"start": "next start -p 4000"
},
"dependencies": {
"next": "zones",
"react": "^16.0.0",
"react-dom": "^16.0.0"
},
"license": "ISC"
}

View file

@ -0,0 +1,5 @@
export default () => (
<div>
This is our blog
</div>
)

2
examples/with-zones/home/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.next
node_modules

View file

@ -0,0 +1,5 @@
export default () => (
<div>
<h2>The Company</h2>
</div>
)

View file

@ -0,0 +1,6 @@
const { NOW_URL } = process.env
const { alias } = require('./now.json')
module.exports = {
assetPrefix: NOW_URL ? `https://${alias}` : 'http://localhost:4000'
}

View file

@ -0,0 +1,3 @@
{
"alias": "with-zones-home.now.sh"
}

View file

@ -0,0 +1,14 @@
{
"name": "with-zones-home",
"version": "1.0.0",
"scripts": {
"build": "next build",
"start": "next start -p 4000"
},
"dependencies": {
"next": "zones",
"react": "^16.0.0",
"react-dom": "^16.0.0"
},
"license": "ISC"
}

View file

@ -0,0 +1,10 @@
import asset from 'next/asset'
import Link from 'next/link'
export default () => (
<div>
<p>This is the about page.</p>
<div><Link href='/'><a>Go Back</a></Link></div>
<img width={200} src={asset('/zeit.png')} />
</div>
)

View file

@ -0,0 +1,15 @@
import Link from 'next/link'
import asset from 'next/asset'
import dynamic from 'next/dynamic'
const Header = dynamic(import('../components/Header'))
export default () => (
<div>
<Header />
<p>This is our homepage</p>
<div><Link href='/blog'><a>Blog</a></Link></div>
<div><Link href='/about'><a>About us</a></Link></div>
<img width={200} src={asset('/nextjs.png')} />
</div>
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -0,0 +1,20 @@
{
"name": "with-zones",
"version": "1.0.0",
"scripts": {
"home": "next home -p 4000",
"home-build": "next build home",
"home-start": "next start home -p 4000",
"blog": "next blog -p 5000",
"blog-build": "next build blog",
"blog-start": "next start blog -p 5000",
"proxy": "micro-proxy -r rules-dev.json"
},
"dependencies": {
"micro-proxy": "^1.0.0",
"next": "latest",
"react": "^16.0.0",
"react-dom": "^16.0.0"
},
"license": "ISC"
}

View file

@ -0,0 +1,6 @@
{
"rules": [
{"pathname": "/blog", "method":["GET", "POST", "OPTIONS"], "dest": "http://localhost:5000"},
{"pathname": "/**", "dest": "http://localhost:4000"}
]
}

View file

@ -0,0 +1,6 @@
{
"rules": [
{"pathname": "/blog", "method":["GET", "POST", "OPTIONS"], "dest": "https://with-zones-blog.now.sh"},
{"pathname": "/**", "dest": "https://with-zones-home.now.sh"}
]
}

10
lib/asset.js Normal file
View file

@ -0,0 +1,10 @@
let assetPrefix
export default function asset (path) {
const pathWithoutSlash = path.replace(/^\//, '')
return `${assetPrefix}/static/${pathWithoutSlash}`
}
export function setAssetPrefix (url) {
assetPrefix = url
}

View file

@ -1 +0,0 @@
throw new Error(`'next/css' has been removed in Next.js 2.0. Please refer to the migration guide: https://github.com/zeit/next.js/wiki/Migrating-from-next-css`)

View file

@ -160,6 +160,11 @@ export function flushChunks () {
} }
export class SameLoopPromise { export class SameLoopPromise {
static resolve (value) {
const promise = new SameLoopPromise((done) => done(value))
return promise
}
constructor (cb) { constructor (cb) {
this.onResultCallbacks = [] this.onResultCallbacks = []
this.onErrorCallbacks = [] this.onErrorCallbacks = []

View file

@ -33,7 +33,7 @@ function reduceComponents (components) {
.filter(unique()) .filter(unique())
.reverse() .reverse()
.map((c) => { .map((c) => {
const className = (c.className ? c.className + ' ' : '') + 'next-head' const className = (c.props && c.props.className ? c.props.className + ' ' : '') + 'next-head'
return React.cloneElement(c, { className }) return React.cloneElement(c, { className })
}) })
} }

View file

@ -153,6 +153,7 @@ export default class Link extends Component {
function isLocal (href) { function isLocal (href) {
const url = parse(href, false, true) const url = parse(href, false, true)
const origin = parse(getLocationOrigin(), false, true) const origin = parse(getLocationOrigin(), false, true)
return !url.host || return !url.host ||
(url.protocol === origin.protocol && url.host === origin.host) (url.protocol === origin.protocol && url.host === origin.host)
} }

View file

@ -129,7 +129,6 @@ export default class Router {
} }
this.abortComponentLoad(as) this.abortComponentLoad(as)
const { pathname, query } = parse(url, true)
// If the url change is only related to a hash change // If the url change is only related to a hash change
// We should not proceed. We should only change the state. // We should not proceed. We should only change the state.
@ -139,6 +138,8 @@ export default class Router {
return return
} }
const { pathname, query } = parse(url, true)
// If asked to change the current URL we should reload the current page // If asked to change the current URL we should reload the current page
// (not location.reload() but reload getInitalProps and other Next.js stuffs) // (not location.reload() but reload getInitalProps and other Next.js stuffs)
// We also need to set the method = replaceState always // We also need to set the method = replaceState always
@ -209,10 +210,6 @@ export default class Router {
this.components[route] = routeInfo this.components[route] = routeInfo
} catch (err) { } catch (err) {
if (err.cancelled) {
return { error: err }
}
if (err.buildIdMismatched) { if (err.buildIdMismatched) {
// Now we need to reload the page or do the action asked by the user // Now we need to reload the page or do the action asked by the user
_notifyBuildIdMismatch(as) _notifyBuildIdMismatch(as)
@ -223,9 +220,21 @@ export default class Router {
} }
if (err.statusCode === 404) { if (err.statusCode === 404) {
// Indicate main error display logic to // If there's 404 error for the page, it could be due to two reasons.
// ignore rendering this error as a runtime error. // 1. Page is not exists
err.ignore = true // 2. Page is exists in a different zone
// We are not sure whether this is actual 404 or exists in a different zone.
// So, doing a hard reload is the proper way to deal with this.
window.location.href = as
// Changing the URL doesn't block executing the current code path.
// So, we need to mark it as a cancelled error and stop the routing logic.
err.cancelled = true
return { error: err }
}
if (err.cancelled) {
return { error: err }
} }
const Component = this.ErrorComponent const Component = this.ErrorComponent
@ -301,29 +310,19 @@ export default class Router {
cancelled = true cancelled = true
} }
try { const Component = await this.fetchRoute(route)
const Component = await this.fetchRoute(route)
if (cancelled) { if (cancelled) {
const error = new Error(`Abort fetching component for route: "${route}"`) const error = new Error(`Abort fetching component for route: "${route}"`)
error.cancelled = true error.cancelled = true
throw error throw error
}
if (cancel === this.componentLoadCancel) {
this.componentLoadCancel = null
}
return Component
} catch (err) {
// There's an error in loading the route.
// Usually this happens when there's a failure in the webpack build
// So in that case, we need to load the page with full SSR
// That'll clean the invalid exising client side information.
// (Like cached routes)
window.location.href = as
throw err
} }
if (cancel === this.componentLoadCancel) {
this.componentLoadCancel = null
}
return Component
} }
async getInitialProps (Component, ctx) { async getInitialProps (Component, ctx) {

View file

@ -1,6 +1,6 @@
{ {
"name": "next", "name": "next",
"version": "4.4.0-canary.3", "version": "5.0.0-universal-alpha.14",
"description": "Minimalistic framework for server-rendered React applications", "description": "Minimalistic framework for server-rendered React applications",
"main": "./dist/server/next.js", "main": "./dist/server/next.js",
"license": "MIT", "license": "MIT",
@ -18,6 +18,7 @@
"dynamic.js", "dynamic.js",
"prefetch.js", "prefetch.js",
"router.js", "router.js",
"asset.js",
"error.js" "error.js"
], ],
"bin": { "bin": {
@ -29,8 +30,9 @@
"pretestonly": "taskr pretest", "pretestonly": "taskr pretest",
"testonly": "cross-env NODE_PATH=test/lib jest \\.test.js", "testonly": "cross-env NODE_PATH=test/lib jest \\.test.js",
"posttestonly": "taskr posttest", "posttestonly": "taskr posttest",
"testall": "npm run testonly -- --coverage --forceExit --runInBand --verbose --bail",
"pretest": "npm run lint", "pretest": "npm run lint",
"test": "npm run testonly -- --coverage --forceExit --runInBand --verbose --bail", "test": "cross-env npm run testall || npm run testall",
"coveralls": "nyc --instrument=false --source-map=false report --temp-directory=./coverage --reporter=text-lcov | coveralls", "coveralls": "nyc --instrument=false --source-map=false report --temp-directory=./coverage --reporter=text-lcov | coveralls",
"lint": "standard 'bin/*' 'client/**/*.js' 'examples/**/*.js' 'lib/**/*.js' 'pages/**/*.js' 'server/**/*.js' 'test/**/*.js'", "lint": "standard 'bin/*' 'client/**/*.js' 'examples/**/*.js' 'lib/**/*.js' 'pages/**/*.js' 'server/**/*.js' 'test/**/*.js'",
"prepublish": "npm run release", "prepublish": "npm run release",
@ -48,15 +50,13 @@
"bin/*": "standard" "bin/*": "standard"
}, },
"dependencies": { "dependencies": {
"@zeit/source-map-support": "0.6.0",
"ansi-html": "0.0.7", "ansi-html": "0.0.7",
"babel-core": "6.26.0", "babel-core": "6.26.0",
"babel-generator": "6.26.0",
"babel-loader": "7.1.2", "babel-loader": "7.1.2",
"babel-plugin-module-resolver": "2.7.1",
"babel-plugin-react-require": "3.0.0", "babel-plugin-react-require": "3.0.0",
"babel-plugin-syntax-dynamic-import": "6.18.0", "babel-plugin-syntax-dynamic-import": "6.18.0",
"babel-plugin-transform-class-properties": "6.24.1", "babel-plugin-transform-class-properties": "6.24.1",
"babel-plugin-transform-es2015-modules-commonjs": "6.26.0",
"babel-plugin-transform-object-rest-spread": "6.26.0", "babel-plugin-transform-object-rest-spread": "6.26.0",
"babel-plugin-transform-react-jsx-source": "6.22.0", "babel-plugin-transform-react-jsx-source": "6.22.0",
"babel-plugin-transform-react-remove-prop-types": "0.4.8", "babel-plugin-transform-react-remove-prop-types": "0.4.8",
@ -69,10 +69,11 @@
"cross-spawn": "5.1.0", "cross-spawn": "5.1.0",
"del": "3.0.0", "del": "3.0.0",
"etag": "1.8.1", "etag": "1.8.1",
"find-up": "2.1.0",
"fresh": "0.5.2", "fresh": "0.5.2",
"friendly-errors-webpack-plugin": "1.6.1", "friendly-errors-webpack-plugin": "1.6.1",
"glob": "7.1.2", "glob": "7.1.2",
"glob-promise": "3.2.0", "glob-promise": "3.3.0",
"hoist-non-react-statics": "2.3.1", "hoist-non-react-statics": "2.3.1",
"htmlescape": "1.1.1", "htmlescape": "1.1.1",
"http-status": "1.0.1", "http-status": "1.0.1",
@ -87,25 +88,25 @@
"pkg-up": "2.0.0", "pkg-up": "2.0.0",
"prop-types": "15.6.0", "prop-types": "15.6.0",
"prop-types-exact": "1.1.1", "prop-types-exact": "1.1.1",
"react-hot-loader": "3.1.1", "react-hot-loader": "4.0.0-beta.14",
"recursive-copy": "2.0.6", "recursive-copy": "2.0.6",
"resolve": "1.5.0",
"send": "0.16.1", "send": "0.16.1",
"source-map-support": "0.4.18",
"strip-ansi": "3.0.1", "strip-ansi": "3.0.1",
"styled-jsx": "2.2.1", "styled-jsx": "2.2.1",
"touch": "3.1.0", "touch": "3.1.0",
"uglifyjs-webpack-plugin": "1.1.6",
"unfetch": "3.0.0", "unfetch": "3.0.0",
"url": "0.11.0", "url": "0.11.0",
"uuid": "3.1.0", "uuid": "3.1.0",
"walk": "2.3.9", "walk": "2.3.9",
"webpack": "3.6.0", "webpack": "3.10.0",
"webpack-dev-middleware": "1.12.0", "webpack-dev-middleware": "1.12.0",
"webpack-hot-middleware": "2.19.1", "webpack-hot-middleware": "2.21.0",
"write-file-webpack-plugin": "4.2.0", "write-file-webpack-plugin": "4.2.0",
"xss-filters": "1.2.7" "xss-filters": "1.2.7"
}, },
"devDependencies": { "devDependencies": {
"uglifyjs-webpack-plugin": "^1.1.1",
"@taskr/babel": "1.1.0", "@taskr/babel": "1.1.0",
"@taskr/clear": "1.1.0", "@taskr/clear": "1.1.0",
"@taskr/esnext": "1.1.0", "@taskr/esnext": "1.1.0",
@ -129,8 +130,8 @@
"node-notifier": "5.1.2", "node-notifier": "5.1.2",
"nyc": "11.2.1", "nyc": "11.2.1",
"portfinder": "1.0.13", "portfinder": "1.0.13",
"react": "16.0.0", "react": "16.2.0",
"react-dom": "16.0.0", "react-dom": "16.2.0",
"standard": "9.0.2", "standard": "9.0.2",
"taskr": "1.1.0", "taskr": "1.1.0",
"wd": "1.4.1" "wd": "1.4.1"

View file

@ -164,6 +164,14 @@ export default () => <p style={{ color: 'red' }}>hi there</p>
To use more sophisticated CSS-in-JS solutions, you typically have to implement style flushing for server-side rendering. We enable this by allowing you to define your own [custom `<Document>`](#user-content-custom-document) component that wraps each page. To use more sophisticated CSS-in-JS solutions, you typically have to implement style flushing for server-side rendering. We enable this by allowing you to define your own [custom `<Document>`](#user-content-custom-document) component that wraps each page.
#### Importing CSS / Sass / Less files
To support importing `.css` `.scss` or `.less` files you can use these modules, which configure sensible defaults for server rendered applications.
- ![@zeit/next-css](https://github.com/zeit/next-plugins/tree/master/packages/next-css)
- ![@zeit/next-sass](https://github.com/zeit/next-plugins/tree/master/packages/next-sass)
- ![@zeit/next-less](https://github.com/zeit/next-plugins/tree/master/packages/next-less)
### Static file serving (e.g.: images) ### Static file serving (e.g.: images)
Create a folder called `static` in your project root directory. From your code you can then reference those files with `/static/` URLs: Create a folder called `static` in your project root directory. From your code you can then reference those files with `/static/` URLs:
@ -1006,7 +1014,7 @@ In order to extend our usage of `webpack`, you can define a function that extend
// (But you could use ES2015 features supported by your Node.js version) // (But you could use ES2015 features supported by your Node.js version)
module.exports = { module.exports = {
webpack: (config, { buildId, dev }) => { webpack: (config, { buildId, dev, isServer, defaultLoaders }) => {
// Perform customizations to webpack config // Perform customizations to webpack config
// Important: return the modified config // Important: return the modified config
@ -1021,7 +1029,15 @@ module.exports = {
} }
``` ```
*Warning: Adding loaders to support new file types (css, less, svg, etc.) is __not__ recommended because only the client code gets bundled via webpack and thus it won't work on the initial server rendering. Babel plugins are a good alternative because they're applied consistently between server/client rendering (e.g. [babel-plugin-inline-react-svg](https://github.com/kesne/babel-plugin-inline-react-svg)).* Some commonly asked for features are available as modules:
- ![@zeit/next-css](https://github.com/zeit/next-plugins/tree/master/packages/next-css)
- ![@zeit/next-sass](https://github.com/zeit/next-plugins/tree/master/packages/next-sass)
- ![@zeit/next-less](https://github.com/zeit/next-plugins/tree/master/packages/next-less)
- ![@zeit/next-preact](https://github.com/zeit/next-plugins/tree/master/packages/next-preact)
- ![@zeit/next-typescript](https://github.com/zeit/next-plugins/tree/master/packages/next-typescript)
*Warning: The `webpack` function is executed twice, once for the server and once for the client. This allows you to distinguish between client and server configuration using the `isServer` property*
### Customizing babel config ### Customizing babel config
@ -1214,20 +1230,6 @@ If you want to create re-usable React components that you can embed in your Next
Next.js bundles [styled-jsx](https://github.com/zeit/styled-jsx) supporting scoped css. However you can use any CSS-in-JS solution in your Next app by just including your favorite library [as mentioned before](#css-in-js) in the document. Next.js bundles [styled-jsx](https://github.com/zeit/styled-jsx) supporting scoped css. However you can use any CSS-in-JS solution in your Next app by just including your favorite library [as mentioned before](#css-in-js) in the document.
</details> </details>
<details>
<summary>How do I use CSS preprocessors like SASS / SCSS / LESS?</summary>
Next.js bundles [styled-jsx](https://github.com/zeit/styled-jsx) supporting scoped css. However you can use any CSS preprocessor solution in your Next app by following one of these examples:
- [with-external-scoped-css](./examples/with-external-scoped-css)
- [with-scoped-stylesheets-and-postcss](./examples/with-scoped-stylesheets-and-postcss)
- [with-global-stylesheet](./examples/with-global-stylesheet)
- [with-styled-jsx-scss](./examples/with-styled-jsx-scss)
- [with-styled-jsx-plugins](./examples/with-styled-jsx-plugins)
</details>
<details> <details>
<summary>What syntactic features are transpiled? How do I change them?</summary> <summary>What syntactic features are transpiled? How do I change them?</summary>

View file

@ -16,32 +16,23 @@ const TYPE_IMPORT = 'Import'
const buildImport = (args) => (template(` const buildImport = (args) => (template(`
( (
typeof require.resolveWeak !== 'function' ? new (require('next/dynamic').SameLoopPromise)((resolve, reject) => {
new (require('next/dynamic').SameLoopPromise)((resolve, reject) => { const weakId = require.resolveWeak(SOURCE)
eval('require.ensure = function (deps, callback) { callback(require) }') try {
require.ensure([], (require) => { const weakModule = __webpack_require__(weakId)
return resolve(weakModule)
} catch (err) {}
require.ensure([], (require) => {
try {
let m = require(SOURCE) let m = require(SOURCE)
m.__webpackChunkName = '${args.name}' m.__webpackChunkName = '${args.name}'
resolve(m); resolve(m)
}, 'chunks/${args.name}'); } catch(error) {
}) reject(error)
: }
new (require('next/dynamic').SameLoopPromise)((resolve, reject) => { }, 'chunks/${args.name}');
const weakId = require.resolveWeak(SOURCE) })
try {
const weakModule = __webpack_require__(weakId)
return resolve(weakModule)
} catch (err) {}
require.ensure([], (require) => {
try {
let m = require(SOURCE)
resolve(m)
} catch(error) {
reject(error)
}
}, 'chunks/${args.name}');
})
) )
`)) `))

View file

@ -1,15 +0,0 @@
// This plugins removes the `.jsx` extension from import statements. Because we transpile .jsx files to .js in .next
// E.g. `import Hello from '../components/hello.jsx'` will become `import Hello from '../components/hello'`
module.exports = function ({types}) {
return {
name: 'remove-dotjsx-from-import',
visitor: {
ImportDeclaration (path) {
const value = path.node.source.value
if (value.slice(-4) === '.jsx') {
path.node.source = types.stringLiteral(value.slice(0, -4))
}
}
}
}
}

View file

@ -1,5 +1,3 @@
const relativeResolve = require('../root-module-relative-path').default(require)
// Resolve styled-jsx plugins // Resolve styled-jsx plugins
function styledJsxOptions (opts) { function styledJsxOptions (opts) {
if (!opts) { if (!opts) {
@ -45,29 +43,12 @@ module.exports = (context, opts = {}) => ({
require.resolve('babel-preset-react') require.resolve('babel-preset-react')
], ],
plugins: [ plugins: [
require.resolve('./plugins/remove-dotjsx-from-import'),
require.resolve('babel-plugin-react-require'), require.resolve('babel-plugin-react-require'),
require.resolve('./plugins/handle-import'), require.resolve('./plugins/handle-import'),
require.resolve('babel-plugin-transform-object-rest-spread'), require.resolve('babel-plugin-transform-object-rest-spread'),
require.resolve('babel-plugin-transform-class-properties'), require.resolve('babel-plugin-transform-class-properties'),
[require.resolve('babel-plugin-transform-runtime'), opts['transform-runtime'] || {}], [require.resolve('babel-plugin-transform-runtime'), opts['transform-runtime'] || {}],
[require.resolve('styled-jsx/babel'), styledJsxOptions(opts['styled-jsx'])], [require.resolve('styled-jsx/babel'), styledJsxOptions(opts['styled-jsx'])],
...plugins, ...plugins
[
require.resolve('babel-plugin-module-resolver'),
{
alias: {
'babel-runtime': relativeResolve('babel-runtime/package'),
'next/link': relativeResolve('../../../lib/link'),
'next/prefetch': relativeResolve('../../../lib/prefetch'),
'next/css': relativeResolve('../../../lib/css'),
'next/dynamic': relativeResolve('../../../lib/dynamic'),
'next/head': relativeResolve('../../../lib/head'),
'next/document': relativeResolve('../../../server/document'),
'next/router': relativeResolve('../../../lib/router'),
'next/error': relativeResolve('../../../lib/error')
}
}
]
] ]
}) })

View file

@ -1,44 +1,42 @@
import { tmpdir } from 'os'
import { join } from 'path' import { join } from 'path'
import fs from 'mz/fs' import fs from 'mz/fs'
import uuid from 'uuid' import uuid from 'uuid'
import del from 'del' import webpack from 'webpack'
import webpack from './webpack' import getConfig from '../config'
import replaceCurrentBuild from './replace' import getBaseWebpackConfig from './webpack'
import md5File from 'md5-file/promise' import md5File from 'md5-file/promise'
export default async function build (dir, conf = null) { export default async function build (dir, conf = null) {
const config = getConfig(dir, conf)
const buildId = uuid.v4() const buildId = uuid.v4()
const tempDir = tmpdir()
const buildDir = join(tempDir, uuid.v4())
try { try {
await fs.access(tempDir, fs.constants.W_OK) await fs.access(dir, fs.constants.W_OK)
} catch (err) { } catch (err) {
console.error(`> Failed, build directory is not writeable. https://err.sh/zeit/next.js/build-dir-not-writeable`) console.error(`> Failed, build directory is not writeable. https://err.sh/zeit/next.js/build-dir-not-writeable`)
throw err throw err
} }
const compiler = await webpack(dir, { buildId, buildDir, conf })
try { try {
const stats = await runCompiler(compiler) const configs = await Promise.all([
await writeBuildStats(buildDir, stats) getBaseWebpackConfig(dir, { buildId, isServer: false, config }),
await writeBuildId(buildDir, buildId) getBaseWebpackConfig(dir, { buildId, isServer: true, config })
])
await runCompiler(configs)
await writeBuildStats(dir, config)
await writeBuildId(dir, buildId, config)
} catch (err) { } catch (err) {
console.error(`> Failed to build on ${buildDir}`) console.error(`> Failed to build`)
throw err throw err
} }
await replaceCurrentBuild(dir, buildDir)
// no need to wait
del(buildDir, { force: true })
} }
function runCompiler (compiler) { function runCompiler (compiler) {
return new Promise((resolve, reject) => { return new Promise(async (resolve, reject) => {
compiler.run((err, stats) => { const webpackCompiler = await webpack(await compiler)
webpackCompiler.run((err, stats) => {
if (err) return reject(err) if (err) return reject(err)
const jsonStats = stats.toJson() const jsonStats = stats.toJson()
@ -55,21 +53,21 @@ function runCompiler (compiler) {
}) })
} }
async function writeBuildStats (dir, stats) { async function writeBuildStats (dir, config) {
// Here we can't use hashes in webpack chunks. // Here we can't use hashes in webpack chunks.
// That's because the "app.js" is not tied to a chunk. // That's because the "app.js" is not tied to a chunk.
// It's created by merging a few assets. (commons.js and main.js) // It's created by merging a few assets. (commons.js and main.js)
// So, we need to generate the hash ourself. // So, we need to generate the hash ourself.
const assetHashMap = { const assetHashMap = {
'app.js': { 'app.js': {
hash: await md5File(join(dir, '.next', 'app.js')) hash: await md5File(join(dir, config.distDir, 'app.js'))
} }
} }
const buildStatsPath = join(dir, '.next', 'build-stats.json') const buildStatsPath = join(dir, config.distDir, 'build-stats.json')
await fs.writeFile(buildStatsPath, JSON.stringify(assetHashMap), 'utf8') await fs.writeFile(buildStatsPath, JSON.stringify(assetHashMap), 'utf8')
} }
async function writeBuildId (dir, buildId) { async function writeBuildId (dir, buildId, config) {
const buildIdPath = join(dir, '.next', 'BUILD_ID') const buildIdPath = join(dir, config.distDir, 'BUILD_ID')
await fs.writeFile(buildIdPath, buildId, 'utf8') await fs.writeFile(buildIdPath, buildId, 'utf8')
} }

View file

@ -8,12 +8,6 @@ module.exports = function (content, sourceMap) {
this.callback(null, `${content} this.callback(null, `${content}
(function (Component, route) { (function (Component, route) {
if (!module.hot) return if (!module.hot) return
if (!__resourceQuery) return
var qs = require('querystring')
var params = qs.parse(__resourceQuery.slice(1))
if (params.entry == null) return
module.hot.accept() module.hot.accept()
Component.__route = route Component.__route = route

View file

@ -0,0 +1,29 @@
import { join, sep } from 'path'
// This plugin modifies the require-ensure code generated by Webpack
// to work with Next.js SSR
export default class NextJsSsrImportPlugin {
constructor ({ dir, dist }) {
this.dir = dir
this.dist = dist
}
apply (compiler) {
compiler.plugin('compilation', (compilation) => {
compilation.mainTemplate.plugin('require-ensure', (code) => {
// Update to load chunks from our custom chunks directory
const chunksDirPath = join(this.dir, this.dist, 'dist')
let updatedCode = code.replace('require("./"', `require("${chunksDirPath}${sep}"`)
// Replace a promise equivalent which runs in the same loop
// If we didn't do this webpack's module loading process block us from
// doing SSR for chunks
updatedCode = updatedCode.replace(
'return Promise.resolve();',
`return require('next/dynamic').SameLoopPromise.resolve();`
)
return updatedCode
})
})
}
}

View file

@ -1,27 +0,0 @@
import { resolve, join } from 'path'
export default class WatchPagesPlugin {
constructor (dir) {
this.dir = resolve(dir, 'pages')
}
apply (compiler) {
compiler.plugin('compilation', (compilation) => {
compilation.plugin('optimize-assets', (assets, callback) => {
// transpile pages/_document.js and descendants,
// but don't need the bundle file
delete assets[join('bundles', 'pages', '_document.js')]
callback()
})
})
compiler.plugin('emit', (compilation, callback) => {
// watch the pages directory
compilation.contextDependencies = [
...compilation.contextDependencies,
this.dir
]
callback()
})
}
}

View file

@ -1,23 +0,0 @@
import mv from 'mv'
import { join } from 'path'
import getConfig from '../config'
export default async function replaceCurrentBuild (dir, buildDir) {
const dist = getConfig(dir).distDir
const _dir = join(dir, dist)
const _buildDir = join(buildDir, '.next')
const oldDir = join(buildDir, '.next.old')
try {
await move(_dir, oldDir)
} catch (err) {
if (err.code !== 'ENOENT') throw err
}
await move(_buildDir, _dir)
return oldDir
}
function move (from, to) {
return new Promise((resolve, reject) =>
mv(from, to, err => err ? reject(err) : resolve()))
}

View file

@ -1,242 +1,45 @@
import { resolve, join, sep } from 'path' import path, {sep} from 'path'
import { createHash } from 'crypto'
import { realpathSync, existsSync } from 'fs'
import webpack from 'webpack' import webpack from 'webpack'
import glob from 'glob-promise' import resolve from 'resolve'
import UglifyJSPlugin from 'uglifyjs-webpack-plugin'
import CaseSensitivePathPlugin from 'case-sensitive-paths-webpack-plugin'
import WriteFilePlugin from 'write-file-webpack-plugin' import WriteFilePlugin from 'write-file-webpack-plugin'
import FriendlyErrorsWebpackPlugin from 'friendly-errors-webpack-plugin' import FriendlyErrorsWebpackPlugin from 'friendly-errors-webpack-plugin'
import CaseSensitivePathPlugin from 'case-sensitive-paths-webpack-plugin' import {getPages} from './webpack/utils'
import UglifyJSPlugin from 'uglifyjs-webpack-plugin'
import UnlinkFilePlugin from './plugins/unlink-file-plugin'
import PagesPlugin from './plugins/pages-plugin'
import DynamicChunksPlugin from './plugins/dynamic-chunks-plugin'
import CombineAssetsPlugin from './plugins/combine-assets-plugin' import CombineAssetsPlugin from './plugins/combine-assets-plugin'
import getConfig from '../config' import PagesPlugin from './plugins/pages-plugin'
import * as babelCore from 'babel-core' import NextJsSsrImportPlugin from './plugins/nextjs-ssr-import'
import DynamicChunksPlugin from './plugins/dynamic-chunks-plugin'
import UnlinkFilePlugin from './plugins/unlink-file-plugin'
import findBabelConfig from './babel/find-config' import findBabelConfig from './babel/find-config'
import rootModuleRelativePath from './root-module-relative-path'
const documentPage = join('pages', '_document.js') const nextDir = path.join(__dirname, '..', '..', '..')
const nextNodeModulesDir = path.join(nextDir, 'node_modules')
const nextPagesDir = path.join(nextDir, 'pages')
const defaultPages = [ const defaultPages = [
'_error.js', '_error.js',
'_document.js' '_document.js'
] ]
const nextPagesDir = join(__dirname, '..', '..', 'pages')
const nextNodeModulesDir = join(__dirname, '..', '..', '..', 'node_modules')
const interpolateNames = new Map(defaultPages.map((p) => { const interpolateNames = new Map(defaultPages.map((p) => {
return [join(nextPagesDir, p), `dist/pages/${p}`] return [path.join(nextPagesDir, p), `dist/bundles/pages/${p}`]
})) }))
const relativeResolve = rootModuleRelativePath(require) function babelConfig (dir, {isServer, dev}) {
async function getPages ({dir, dev, pagesGlobPattern}) {
let pages
if (dev) {
pages = await glob('pages/+(_document|_error).+(js|jsx)', { cwd: dir })
} else {
pages = await glob(pagesGlobPattern, { cwd: dir })
}
return pages
}
function getPageEntries (pages) {
const entries = {}
for (const p of pages) {
entries[join('bundles', p.replace('.jsx', '.js'))] = [`./${p}?entry`]
}
// The default pages (_document.js and _error.js) are only added when they're not provided by the user
for (const p of defaultPages) {
const entryName = join('bundles', 'pages', p)
if (!entries[entryName]) {
entries[entryName] = [join(nextPagesDir, p) + '?entry']
}
}
return entries
}
export default async function createCompiler (dir, { buildId, dev = false, quiet = false, buildDir, conf = null } = {}) {
// Resolve relative path to absolute path
dir = realpathSync(resolve(dir))
// Used to track the amount of pages for webpack commons chunk plugin
let totalPages
// Loads next.config.js and custom configuration provided in custom server initialization
const config = getConfig(dir, conf)
// Middlewares to handle on-demand entries and hot updates in development
const devEntries = dev ? [
join(__dirname, '..', '..', 'client', 'webpack-hot-middleware-client'),
join(__dirname, '..', '..', 'client', 'on-demand-entries-client')
] : []
const mainJS = require.resolve(`../../client/next${dev ? '-dev' : ''}`) // Uses client/next-dev in development for code splitting dev dependencies
const entry = async () => {
// Get entries for pages in production mode. In development only _document and _error are added. Because pages are added by on-demand-entry-handler.
const pages = await getPages({dir, dev, pagesGlobPattern: config.pagesGlobPattern})
const pageEntries = getPageEntries(pages)
// Used for commons chunk calculations
totalPages = pages.length
if (pages.indexOf(documentPage) !== -1) {
totalPages = totalPages - 1
}
const entries = {
'main.js': [
...devEntries, // Adds hot middleware and ondemand entries in development
...config.clientBootstrap || [], // clientBootstrap can be used to load polyfills before code execution
mainJS // Main entrypoint in the client folder
],
...pageEntries
}
return entries
}
const plugins = [
// Defines NODE_ENV as development/production. This is used by some npm modules to determine if they should optimize.
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production')
}),
new CaseSensitivePathPlugin(), // Since on macOS the filesystem is case-insensitive this will make sure your path are case-sensitive
new webpack.IgnorePlugin(/(precomputed)/, /node_modules.+(elliptic)/),
// Provide legacy options to webpack
new webpack.LoaderOptionsPlugin({
options: {
context: dir,
customInterpolateName (url, name, opts) {
return interpolateNames.get(this.resourcePath) || url
}
}
}),
// Writes all generated files to disk, even in development. For SSR.
new WriteFilePlugin({
exitOnErrors: false,
log: false,
// required not to cache removed files
useHashIndex: false
}),
// Moves common modules into commons.js
new webpack.optimize.CommonsChunkPlugin({
name: 'commons',
filename: 'commons.js',
minChunks (module, count) {
// We need to move react-dom explicitly into common chunks.
// Otherwise, if some other page or module uses it, it might
// included in that bundle too.
if (dev && module.context && module.context.indexOf(`${sep}react${sep}`) >= 0) {
return true
}
if (dev && module.context && module.context.indexOf(`${sep}react-dom${sep}`) >= 0) {
return true
}
// In the dev we use on-demand-entries.
// So, it makes no sense to use commonChunks based on the minChunks count.
// Instead, we move all the code in node_modules into each of the pages.
if (dev) {
return false
}
// If there are one or two pages, only move modules to common if they are
// used in all of the pages. Otherwise, move modules used in at-least
// 1/2 of the total pages into commons.
if (totalPages <= 2) {
return count >= totalPages
}
return count >= totalPages * 0.5
}
}),
// This chunk splits out react and react-dom in production to make sure it does not go through uglify. This saved multiple seconds on production builds.
// See https://twitter.com/dan_abramov/status/944040306420408325
new webpack.optimize.CommonsChunkPlugin({
name: 'react',
filename: 'react.js',
minChunks (module, count) {
if (dev) {
return false
}
if (module.resource && module.resource.includes(`${sep}react-dom${sep}`) && count >= 0) {
return true
}
if (module.resource && module.resource.includes(`${sep}react${sep}`) && count >= 0) {
return true
}
return false
}
}),
// This chunk contains all the webpack related code. So, all the changes
// related to that happens to this chunk.
// It won't touch commons.js and that gives us much better re-build perf.
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
filename: 'manifest.js'
}),
// This adds Next.js route definitions to page bundles
new PagesPlugin(),
// Implements support for dynamic imports
new DynamicChunksPlugin()
]
if (dev) {
plugins.push(
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new UnlinkFilePlugin()
)
if (!quiet) {
plugins.push(new FriendlyErrorsWebpackPlugin())
}
} else {
plugins.push(new webpack.IgnorePlugin(/react-hot-loader/))
plugins.push(
// Minifies javascript bundles
new UglifyJSPlugin({
exclude: /react\.js/,
parallel: true,
sourceMap: false,
uglifyOptions: {
compress: {
comparisons: false
}
}
})
)
plugins.push(
// Combines manifest.js commons.js and main.js into app.js in production
new CombineAssetsPlugin({
input: ['manifest.js', 'react.js', 'commons.js', 'main.js'],
output: 'app.js'
}),
)
// Implements scope hoisting which speeds up browser execution of javascript
plugins.push(new webpack.optimize.ModuleConcatenationPlugin())
}
const nodePathList = (process.env.NODE_PATH || '')
.split(process.platform === 'win32' ? ';' : ':')
.filter((p) => !!p)
const mainBabelOptions = { const mainBabelOptions = {
cacheDirectory: true, cacheDirectory: true,
presets: [] presets: [],
plugins: [
dev && !isServer && require.resolve('react-hot-loader/babel')
].filter(Boolean)
} }
const externalBabelConfig = findBabelConfig(dir) const externalBabelConfig = findBabelConfig(dir)
if (externalBabelConfig) { if (externalBabelConfig) {
console.log(`> Using external babel configuration`) // Log it out once
console.log(`> Location: "${externalBabelConfig.loc}"`) if (!isServer) {
console.log(`> Using external babel configuration`)
console.log(`> Location: "${externalBabelConfig.loc}"`)
}
// It's possible to turn off babelrc support via babelrc itself. // It's possible to turn off babelrc support via babelrc itself.
// In that case, we should add our default preset. // In that case, we should add our default preset.
// That's why we need to do this. // That's why we need to do this.
@ -251,188 +54,252 @@ export default async function createCompiler (dir, { buildId, dev = false, quiet
mainBabelOptions.presets.push(require.resolve('./babel/preset')) mainBabelOptions.presets.push(require.resolve('./babel/preset'))
} }
const devLoaders = dev ? [{ return mainBabelOptions
test: /\.(js|jsx)(\?[^?]*)?$/, }
loader: 'hot-self-accept-loader',
include: [
join(dir, 'pages'),
nextPagesDir
]
}, {
test: /\.(js|jsx)(\?[^?]*)?$/,
loader: 'react-hot-loader/webpack',
exclude: /node_modules/
}] : []
const loaders = [{ function externalsConfig (dir, isServer) {
test: /\.json$/, const externals = []
loader: 'json-loader'
}, {
test: /\.(js|jsx|json)(\?[^?]*)?$/,
loader: 'emit-file-loader',
include: [dir, nextPagesDir],
exclude (str) {
return /node_modules/.test(str) && str.indexOf(nextPagesDir) !== 0
},
options: {
name: 'dist/[path][name].[ext]',
// We need to strip off .jsx on the server. Otherwise require without .jsx doesn't work.
interpolateName: (name) => name.replace('.jsx', '.js'),
validateFileName (file) {
const cases = [{from: '.js', to: '.jsx'}, {from: '.jsx', to: '.js'}]
for (const item of cases) { if (!isServer) {
const {from, to} = item return externals
if (file.slice(-(from.length)) !== from) { }
continue
}
const filePath = file.slice(0, -(from.length)) + to // This will externalize all the 'next/xxx' modules to load from
// node_modules always.
if (existsSync(filePath)) { // This is very useful in Next.js development where we use symlinked version
throw new Error(`Both ${from} and ${to} file found. Please make sure you only have one of both.`) // of Next.js or using next/xxx inside test apps.
} externals.push((context, request, callback) => {
} resolve(request, { basedir: dir, preserveSymlinks: true }, (err, res) => {
}, if (err) {
// By default, our babel config does not transpile ES2015 module syntax because return callback()
// webpack knows how to handle them. (That's how it can do tree-shaking)
// But Node.js doesn't know how to handle them. So, we have to transpile them here.
transform ({ content, sourceMap, interpolatedName }) {
// Only handle .js files
if (!(/\.(js|jsx)$/.test(interpolatedName))) {
return { content, sourceMap }
}
const transpiled = babelCore.transform(content, {
babelrc: false,
sourceMaps: dev ? 'both' : false,
// Here we need to resolve all modules to the absolute paths.
// Earlier we did it with the babel-preset.
// But since we don't transpile ES2015 in the preset this is not resolving.
// That's why we need to do it here.
// See more: https://github.com/zeit/next.js/issues/951
plugins: [
require.resolve(join(__dirname, './babel/plugins/remove-dotjsx-from-import.js')),
[require.resolve('babel-plugin-transform-es2015-modules-commonjs')],
[
require.resolve('babel-plugin-module-resolver'),
{
alias: {
'babel-runtime': relativeResolve('babel-runtime/package'),
'next/link': relativeResolve('../../lib/link'),
'next/prefetch': relativeResolve('../../lib/prefetch'),
'next/css': relativeResolve('../../lib/css'),
'next/dynamic': relativeResolve('../../lib/dynamic'),
'next/head': relativeResolve('../../lib/head'),
'next/document': relativeResolve('../../server/document'),
'next/router': relativeResolve('../../lib/router'),
'next/error': relativeResolve('../../lib/error'),
'styled-jsx/style': relativeResolve('styled-jsx/style')
}
}
]
],
inputSourceMap: sourceMap
})
// Strip ?entry to map back to filesystem and work with iTerm, etc.
let { map } = transpiled
let output = transpiled.code
if (map) {
let nodeMap = Object.assign({}, map)
nodeMap.sources = nodeMap.sources.map((source) => source.replace(/\?entry/, ''))
delete nodeMap.sourcesContent
// Output explicit inline source map that source-map-support can pickup via requireHook mode.
// Since these are not formal chunks, the devtool infrastructure in webpack does not output
// a source map for these files.
const sourceMapUrl = new Buffer(JSON.stringify(nodeMap), 'utf-8').toString('base64')
output = `${output}\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${sourceMapUrl}`
}
return {
content: output,
sourceMap: transpiled.map
}
} }
if (res.match(/node_modules/)) {
return callback(null, `commonjs ${request}`)
}
callback()
})
})
return externals
}
export default async function getBaseWebpackConfig (dir, {dev = false, isServer = false, buildId, config}) {
const babelLoaderOptions = babelConfig(dir, {dev, isServer})
const defaultLoaders = {
babel: {
loader: 'babel-loader',
options: babelLoaderOptions
} }
}, { }
loader: 'babel-loader',
include: nextPagesDir, let totalPages
exclude (str) {
return /node_modules/.test(str) && str.indexOf(nextPagesDir) !== 0
},
options: {
babelrc: false,
cacheDirectory: true,
presets: [require.resolve('./babel/preset')]
}
}, {
test: /\.(js|jsx)(\?[^?]*)?$/,
loader: 'babel-loader',
include: [dir],
exclude (str) {
return /node_modules/.test(str)
},
options: mainBabelOptions
}]
let webpackConfig = { let webpackConfig = {
devtool: dev ? 'source-map' : false,
name: isServer ? 'server' : 'client',
cache: true,
target: isServer ? 'node' : 'web',
externals: externalsConfig(dir, isServer),
context: dir, context: dir,
entry, entry: async () => {
const pages = await getPages(dir, {dev, isServer})
totalPages = Object.keys(pages).length
const mainJS = require.resolve(`../../client/next${dev ? '-dev' : ''}`)
const clientConfig = !isServer ? {
'main.js': [
dev && !isServer && path.join(__dirname, '..', '..', 'client', 'webpack-hot-middleware-client'),
dev && !isServer && path.join(__dirname, '..', '..', 'client', 'on-demand-entries-client'),
mainJS
].filter(Boolean)
} : {}
return {
...clientConfig,
...pages
}
},
output: { output: {
path: buildDir ? join(buildDir, '.next') : join(dir, config.distDir), path: path.join(dir, config.distDir, isServer ? 'dist' : ''), // server compilation goes to `.next/dist`
filename: '[name]', filename: '[name]',
libraryTarget: 'commonjs2', libraryTarget: 'commonjs2',
publicPath: `/_next/webpack/`, publicPath: `${config.assetPrefix}/_next/webpack/`,
strictModuleExceptionHandling: true,
devtoolModuleFilenameTemplate ({ resourcePath }) {
const hash = createHash('sha1')
hash.update(Date.now() + '')
const id = hash.digest('hex').slice(0, 7)
// append hash id for cache busting
return `webpack:///${resourcePath}?${id}`
},
// This saves chunks with the name given via require.ensure() // This saves chunks with the name given via require.ensure()
chunkFilename: '[name]-[chunkhash].js' chunkFilename: '[name]-[chunkhash].js',
strictModuleExceptionHandling: true,
devtoolModuleFilenameTemplate: '[absolute-resource-path]'
}, },
performance: { hints: false },
resolve: { resolve: {
alias: {
// This bypasses React's check for production mode. Since we know it is in production this way.
// This allows us to exclude React from being uglified. Saving multiple seconds per build.
'react-dom': dev ? 'react-dom/cjs/react-dom.development.js' : 'react-dom/cjs/react-dom.production.min.js'
},
extensions: ['.js', '.jsx', '.json'], extensions: ['.js', '.jsx', '.json'],
modules: [ modules: [
nextNodeModulesDir, nextNodeModulesDir,
'node_modules', 'node_modules'
...nodePathList ],
] alias: {
next: nextDir,
// This bypasses React's check for production mode. Since we know it is in production this way.
// This allows us to exclude React from being uglified. Saving multiple seconds per build.
react: dev ? 'react/cjs/react.development.js' : 'react/cjs/react.production.min.js',
'react-dom': dev ? 'react-dom/cjs/react-dom.development.js' : 'react-dom/cjs/react-dom.production.min.js'
}
}, },
resolveLoader: { resolveLoader: {
modules: [ modules: [
nextNodeModulesDir, nextNodeModulesDir,
'node_modules', 'node_modules',
join(__dirname, 'loaders'), path.join(__dirname, 'loaders')
...nodePathList
] ]
}, },
plugins,
module: { module: {
rules: [ rules: [
...devLoaders, dev && !isServer && {
...loaders test: /\.(js|jsx)(\?[^?]*)?$/,
] loader: 'hot-self-accept-loader',
include: [
path.join(dir, 'pages'),
nextPagesDir
]
},
{
test: /\.+(js|jsx)$/,
include: [dir],
exclude: /node_modules/,
use: defaultLoaders.babel
}
].filter(Boolean)
}, },
devtool: dev ? 'cheap-module-inline-source-map' : false, plugins: [
performance: { hints: false } new webpack.IgnorePlugin(/(precomputed)/, /node_modules.+(elliptic)/),
dev && new webpack.NoEmitOnErrorsPlugin(),
dev && !isServer && new FriendlyErrorsWebpackPlugin(),
dev && new webpack.NamedModulesPlugin(),
dev && !isServer && new webpack.HotModuleReplacementPlugin(), // Hot module replacement
dev && new UnlinkFilePlugin(),
dev && new CaseSensitivePathPlugin(), // Since on macOS the filesystem is case-insensitive this will make sure your path are case-sensitive
dev && new webpack.LoaderOptionsPlugin({
options: {
context: dir,
customInterpolateName (url, name, opts) {
return interpolateNames.get(this.resourcePath) || url
}
}
}),
dev && new WriteFilePlugin({
exitOnErrors: false,
log: false,
// required not to cache removed files
useHashIndex: false
}),
!dev && new webpack.IgnorePlugin(/react-hot-loader/),
!isServer && !dev && new UglifyJSPlugin({
exclude: /react\.js/,
parallel: true,
sourceMap: false,
uglifyOptions: {
compress: {
arrows: false,
booleans: false,
collapse_vars: false,
comparisons: false,
computed_props: false,
hoist_funs: false,
hoist_props: false,
hoist_vars: false,
if_return: false,
inline: false,
join_vars: false,
keep_infinity: true,
loops: false,
negate_iife: false,
properties: false,
reduce_funcs: false,
reduce_vars: false,
sequences: false,
side_effects: false,
switches: false,
top_retain: false,
toplevel: false,
typeofs: false,
unused: false,
conditionals: false,
dead_code: true,
evaluate: false
}
}
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production')
}),
!isServer && new CombineAssetsPlugin({
input: ['manifest.js', 'react.js', 'commons.js', 'main.js'],
output: 'app.js'
}),
!dev && new webpack.optimize.ModuleConcatenationPlugin(),
!isServer && new PagesPlugin(),
!isServer && new DynamicChunksPlugin(),
isServer && new NextJsSsrImportPlugin({ dir, dist: config.distDir }),
!isServer && new webpack.optimize.CommonsChunkPlugin({
name: `commons`,
filename: `commons.js`,
minChunks (module, count) {
// We need to move react-dom explicitly into common chunks.
// Otherwise, if some other page or module uses it, it might
// included in that bundle too.
if (dev && module.context && module.context.indexOf(`${sep}react${sep}`) >= 0) {
return true
}
if (dev && module.context && module.context.indexOf(`${sep}react-dom${sep}`) >= 0) {
return true
}
// In the dev we use on-demand-entries.
// So, it makes no sense to use commonChunks based on the minChunks count.
// Instead, we move all the code in node_modules into each of the pages.
if (dev) {
return false
}
// If there are one or two pages, only move modules to common if they are
// used in all of the pages. Otherwise, move modules used in at-least
// 1/2 of the total pages into commons.
if (totalPages <= 2) {
return count >= totalPages
}
return count >= totalPages * 0.5
}
}),
!isServer && new webpack.optimize.CommonsChunkPlugin({
name: 'react',
filename: 'react.js',
minChunks (module, count) {
if (dev) {
return false
}
if (module.resource && module.resource.includes(`${sep}react-dom${sep}`) && count >= 0) {
return true
}
if (module.resource && module.resource.includes(`${sep}react${sep}`) && count >= 0) {
return true
}
return false
}
}),
!isServer && new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
filename: 'manifest.js'
})
].filter(Boolean)
} }
if (config.webpack) { if (typeof config.webpack === 'function') {
console.log(`> Using "webpack" config function defined in ${config.configOrigin}.`) webpackConfig = config.webpack(webpackConfig, {dir, dev, isServer, buildId, config, defaultLoaders})
webpackConfig = await config.webpack(webpackConfig, { buildId, dev })
} }
return webpack(webpackConfig)
return webpackConfig
} }

View file

@ -0,0 +1,68 @@
import path from 'path'
import glob from 'glob-promise'
const nextPagesDir = path.join(__dirname, '..', '..', '..', 'pages')
export async function getPages (dir, {dev, isServer}) {
const pageFiles = await getPagePaths(dir, {dev, isServer})
return getPageEntries(pageFiles, {isServer})
}
async function getPagePaths (dir, {dev, isServer}) {
let pages
if (dev) {
pages = await glob(isServer ? 'pages/+(_document|_error).+(js|jsx|ts|tsx)' : 'pages/_error.+(js|jsx|ts|tsx)', { cwd: dir })
} else {
pages = await glob(isServer ? 'pages/**/*.+(js|jsx|ts|tsx)' : 'pages/**/!(_document)*.+(js|jsx|ts|tsx)', { cwd: dir })
}
return pages
}
// Convert page path into single entry
export function createEntry (filePath, name) {
const parsedPath = path.parse(filePath)
let entryName = name || filePath
// This makes sure we compile `pages/blog/index.js` to `pages/blog.js`.
// Excludes `pages/index.js` from this rule since we do want `/` to route to `pages/index.js`
if (parsedPath.dir !== 'pages' && parsedPath.name === 'index') {
entryName = `${parsedPath.dir}.js`
}
// Makes sure supported extensions are stripped off. The outputted file should always be `.js`
entryName = entryName.replace(/\.+(jsx|tsx|ts)/, '.js')
return {
name: path.join('bundles', entryName),
files: [parsedPath.root ? filePath : `./${filePath}`] // The entry always has to be an array.
}
}
// Convert page paths into entries
export function getPageEntries (pagePaths, {isServer}) {
const entries = {}
for (const filePath of pagePaths) {
const entry = createEntry(filePath)
entries[entry.name] = entry.files
}
const errorPagePath = path.join(nextPagesDir, '_error.js')
const errorPageEntry = createEntry(errorPagePath, 'pages/_error.js') // default error.js
if (!entries[errorPageEntry.name]) {
entries[errorPageEntry.name] = errorPageEntry.files
}
if (isServer) {
const documentPagePath = path.join(nextPagesDir, '_document.js')
const documentPageEntry = createEntry(documentPagePath, 'pages/_document.js')
if (!entries[documentPageEntry.name]) {
entries[documentPageEntry.name] = documentPageEntry.files
}
}
return entries
}

View file

@ -5,12 +5,10 @@ const cache = new Map()
const defaultConfig = { const defaultConfig = {
webpack: null, webpack: null,
webpackDevMiddleware: null, webpackDevMiddleware: null,
poweredByHeader: true,
distDir: '.next', distDir: '.next',
assetPrefix: '', assetPrefix: '',
configOrigin: 'default', configOrigin: 'default',
useFileSystemPublicRoutes: true, useFileSystemPublicRoutes: true
pagesGlobPattern: 'pages/**/*.+(js|jsx)'
} }
export default function getConfig (dir, customConfig) { export default function getConfig (dir, customConfig) {
@ -34,6 +32,9 @@ function loadConfig (dir, customConfig) {
if (path && path.length) { if (path && path.length) {
const userConfigModule = require(path) const userConfigModule = require(path)
userConfig = userConfigModule.default || userConfigModule userConfig = userConfigModule.default || userConfigModule
if (userConfig.poweredByHeader === true || userConfig.poweredByHeader === false) {
console.warn('> the `poweredByHeader` option has been removed https://err.sh/zeit/next.js/powered-by-header-option-removed')
}
userConfig.configOrigin = 'next.config.js' userConfig.configOrigin = 'next.config.js'
} }

View file

@ -4,7 +4,7 @@ import htmlescape from 'htmlescape'
import flush from 'styled-jsx/server' import flush from 'styled-jsx/server'
const Fragment = React.Fragment || function Fragment ({ children }) { const Fragment = React.Fragment || function Fragment ({ children }) {
return children return <div>{children}</div>
} }
export default class Document extends Component { export default class Document extends Component {
@ -106,7 +106,6 @@ export class Main extends Component {
render () { render () {
const { html, errorHtml } = this.context._documentProps const { html, errorHtml } = this.context._documentProps
return ( return (
<Fragment> <Fragment>
<div id='__next' dangerouslySetInnerHTML={{ __html: html }} /> <div id='__next' dangerouslySetInnerHTML={{ __html: html }} />

View file

@ -2,12 +2,14 @@ import { join, relative, sep } from 'path'
import WebpackDevMiddleware from 'webpack-dev-middleware' import WebpackDevMiddleware from 'webpack-dev-middleware'
import WebpackHotMiddleware from 'webpack-hot-middleware' import WebpackHotMiddleware from 'webpack-hot-middleware'
import onDemandEntryHandler from './on-demand-entry-handler' import onDemandEntryHandler from './on-demand-entry-handler'
import webpack from './build/webpack' import webpack from 'webpack'
import getBaseWebpackConfig from './build/webpack'
import clean from './build/clean' import clean from './build/clean'
import getConfig from './config' import getConfig from './config'
import UUID from 'uuid' import UUID from 'uuid'
import { import {
IS_BUNDLED_PAGE IS_BUNDLED_PAGE,
addCorsSupport
} from './utils' } from './utils'
export default class HotReloader { export default class HotReloader {
@ -34,6 +36,15 @@ export default class HotReloader {
} }
async run (req, res) { async run (req, res) {
// Usually CORS support is not needed for the hot-reloader (this is dev only feature)
// With when the app runs for multi-zones support behind a proxy,
// the current page is trying to access this URL via assetPrefix.
// That's when the CORS support is needed.
const { preflight } = addCorsSupport(req, res)
if (preflight) {
return
}
for (const fn of this.middlewares) { for (const fn of this.middlewares) {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
fn(req, res, (err) => { fn(req, res, (err) => {
@ -45,15 +56,19 @@ export default class HotReloader {
} }
async start () { async start () {
const [compiler] = await Promise.all([ await clean(this.dir)
webpack(this.dir, { buildId: this.buildId, dev: true, quiet: this.quiet }),
clean(this.dir) const configs = await Promise.all([
getBaseWebpackConfig(this.dir, { dev: true, isServer: false, config: this.config }),
getBaseWebpackConfig(this.dir, { dev: true, isServer: true, config: this.config })
]) ])
const compiler = webpack(configs)
const buildTools = await this.prepareBuildTools(compiler) const buildTools = await this.prepareBuildTools(compiler)
this.assignBuildTools(buildTools) this.assignBuildTools(buildTools)
this.stats = await this.waitUntilValid() this.stats = (await this.waitUntilValid()).stats[0]
} }
async stop (webpackDevMiddleware) { async stop (webpackDevMiddleware) {
@ -71,11 +86,15 @@ export default class HotReloader {
async reload () { async reload () {
this.stats = null this.stats = null
const [compiler] = await Promise.all([ await clean(this.dir)
webpack(this.dir, { buildId: this.buildId, dev: true, quiet: this.quiet }),
clean(this.dir) const configs = await Promise.all([
getBaseWebpackConfig(this.dir, { dev: true, isServer: false, config: this.config }),
getBaseWebpackConfig(this.dir, { dev: true, isServer: true, config: this.config })
]) ])
const compiler = webpack(configs)
const buildTools = await this.prepareBuildTools(compiler) const buildTools = await this.prepareBuildTools(compiler)
this.stats = await this.waitUntilValid(buildTools.webpackDevMiddleware) this.stats = await this.waitUntilValid(buildTools.webpackDevMiddleware)
@ -97,25 +116,28 @@ export default class HotReloader {
} }
async prepareBuildTools (compiler) { async prepareBuildTools (compiler) {
compiler.plugin('after-emit', (compilation, callback) => { // This flushes require.cache after emitting the files. Providing 'hot reloading' of server files.
const { assets } = compilation compiler.compilers.forEach((singleCompiler) => {
singleCompiler.plugin('after-emit', (compilation, callback) => {
const { assets } = compilation
if (this.prevAssets) { if (this.prevAssets) {
for (const f of Object.keys(assets)) { for (const f of Object.keys(assets)) {
deleteCache(assets[f].existsAt) deleteCache(assets[f].existsAt)
} }
for (const f of Object.keys(this.prevAssets)) { for (const f of Object.keys(this.prevAssets)) {
if (!assets[f]) { if (!assets[f]) {
deleteCache(this.prevAssets[f].existsAt) deleteCache(this.prevAssets[f].existsAt)
}
} }
} }
} this.prevAssets = assets
this.prevAssets = assets
callback() callback()
})
}) })
compiler.plugin('done', (stats) => { compiler.compilers[0].plugin('done', (stats) => {
const { compilation } = stats const { compilation } = stats
const chunkNames = new Set( const chunkNames = new Set(
compilation.chunks compilation.chunks
@ -193,12 +215,13 @@ export default class HotReloader {
const webpackDevMiddleware = WebpackDevMiddleware(compiler, webpackDevMiddlewareConfig) const webpackDevMiddleware = WebpackDevMiddleware(compiler, webpackDevMiddlewareConfig)
const webpackHotMiddleware = WebpackHotMiddleware(compiler, { const webpackHotMiddleware = WebpackHotMiddleware(compiler.compilers[0], {
path: '/_next/webpack-hmr', path: '/_next/webpack-hmr',
log: false, log: false,
heartbeat: 2500 heartbeat: 2500
}) })
const onDemandEntries = onDemandEntryHandler(webpackDevMiddleware, compiler, {
const onDemandEntries = onDemandEntryHandler(webpackDevMiddleware, compiler.compilers, {
dir: this.dir, dir: this.dir,
dev: true, dev: true,
reload: this.reload.bind(this), reload: this.reload.bind(this),
@ -249,8 +272,8 @@ export default class HotReloader {
this.webpackHotMiddleware.publish({ action, data: args }) this.webpackHotMiddleware.publish({ action, data: args })
} }
ensurePage (page) { async ensurePage (page) {
return this.onDemandEntries.ensurePage(page) await this.onDemandEntries.ensurePage(page)
} }
} }

View file

@ -1,3 +1,4 @@
require('@zeit/source-map-support').install()
import { resolve, join, sep } from 'path' import { resolve, join, sep } from 'path'
import { parse as parseUrl } from 'url' import { parse as parseUrl } from 'url'
import { parse as parseQs } from 'querystring' import { parse as parseQs } from 'querystring'
@ -11,28 +12,11 @@ import {
renderScriptError renderScriptError
} from './render' } from './render'
import Router from './router' import Router from './router'
import { getAvailableChunks } from './utils' import { getAvailableChunks, isInternalUrl } from './utils'
import getConfig from './config' import getConfig from './config'
// We need to go up one more level since we are in the `dist` directory // We need to go up one more level since we are in the `dist` directory
import pkg from '../../package' import pkg from '../../package'
import reactPkg from 'react/package' import * as asset from '../lib/asset'
// TODO: Remove this in Next.js 5
if (!(/^16\./.test(reactPkg.version))) {
const message = `
Error: Next.js 4 requires React 16.
Install React 16 with:
npm remove react react-dom
npm install --save react@16 react-dom@16
`
console.error(message)
process.exit(1)
}
const internalPrefixes = [
/^\/_next\//,
/^\/static\//
]
const blockedPages = { const blockedPages = {
'/_document': true, '/_document': true,
@ -41,14 +25,6 @@ const blockedPages = {
export default class Server { export default class Server {
constructor ({ dir = '.', dev = false, staticMarkup = false, quiet = false, conf = null } = {}) { constructor ({ dir = '.', dev = false, staticMarkup = false, quiet = false, conf = null } = {}) {
// When in dev mode, remap the inline source maps that we generate within the webpack portion
// of the build.
if (dev) {
require('source-map-support').install({
hookRequire: true
})
}
this.dir = resolve(dir) this.dir = resolve(dir)
this.dev = dev this.dev = dev
this.quiet = quiet this.quiet = quiet
@ -74,6 +50,9 @@ export default class Server {
availableChunks: dev ? {} : getAvailableChunks(this.dir, this.dist) availableChunks: dev ? {} : getAvailableChunks(this.dir, this.dist)
} }
// With this, static assets will work across zones
asset.setAssetPrefix(this.config.assetPrefix)
this.defineRoutes() this.defineRoutes()
} }
@ -182,6 +161,23 @@ export default class Server {
await this.serveStatic(req, res, p) await this.serveStatic(req, res, p)
}, },
'/_next/:buildId/page/:path*.js.map': async (req, res, params) => {
const paths = params.path || ['']
const page = `/${paths.join('/')}`
if (this.dev) {
try {
await this.hotReloader.ensurePage(page)
} catch (err) {
await this.render404(req, res)
}
}
const dist = getConfig(this.dir).distDir
const path = join(this.dir, dist, 'bundles', 'pages', `${page}.js.map`)
await serveStatic(req, res, path)
},
'/_next/:buildId/page/_error*': async (req, res, params) => { '/_next/:buildId/page/_error*': async (req, res, params) => {
if (!this.handleBuildId(params.buildId, res)) { if (!this.handleBuildId(params.buildId, res)) {
const error = new Error('INVALID_BUILD_ID') const error = new Error('INVALID_BUILD_ID')
@ -196,9 +192,6 @@ export default class Server {
'/_next/:buildId/page/:path*.js': async (req, res, params) => { '/_next/:buildId/page/:path*.js': async (req, res, params) => {
const paths = params.path || [''] const paths = params.path || ['']
// URL is asks for ${page}.js (to support loading assets from static dirs)
// But there's no .js in the actual page.
// So, we need to remove .js to get the page name.
const page = `/${paths.join('/')}` const page = `/${paths.join('/')}`
if (!this.handleBuildId(params.buildId, res)) { if (!this.handleBuildId(params.buildId, res)) {
@ -222,11 +215,13 @@ export default class Server {
} }
} }
let p = join(this.dir, this.dist, 'bundles', 'pages', paths.join('/')) const p = join(this.dir, this.dist, 'bundles', 'pages', `${page}.js`)
if (!fs.existsSync(`${p}.js`)) { await this.serveStatic(req, res, p)
p = join(p, 'index') // It's possible to have index.js in a subfolder },
}
await this.serveStatic(req, res, `${p}.js`) '/_next/static/:path*': async (req, res, params) => {
const p = join(this.dist, 'static', ...(params.path || []))
await this.serveStatic(req, res, p)
}, },
// It's very important keep this route's param optional. // It's very important keep this route's param optional.
@ -293,7 +288,7 @@ export default class Server {
} }
async render (req, res, pathname, query, parsedUrl) { async render (req, res, pathname, query, parsedUrl) {
if (this.isInternalUrl(req)) { if (isInternalUrl(req.url)) {
return this.handleRequest(req, res, parsedUrl) return this.handleRequest(req, res, parsedUrl)
} }
@ -301,10 +296,8 @@ export default class Server {
return await this.render404(req, res, parsedUrl) return await this.render404(req, res, parsedUrl)
} }
if (this.config.poweredByHeader) {
res.setHeader('X-Powered-By', `Next.js ${pkg.version}`)
}
const html = await this.renderToHTML(req, res, pathname, query) const html = await this.renderToHTML(req, res, pathname, query)
res.setHeader('X-Powered-By', `Next.js ${pkg.version}`)
return sendHTML(req, res, html, req.method, this.renderOpts) return sendHTML(req, res, html, req.method, this.renderOpts)
} }
@ -318,7 +311,8 @@ export default class Server {
} }
try { try {
return await renderToHTML(req, res, pathname, query, this.renderOpts) const out = await renderToHTML(req, res, pathname, query, this.renderOpts)
return out
} catch (err) { } catch (err) {
if (err.code === 'ENOENT') { if (err.code === 'ENOENT') {
res.statusCode = 404 res.statusCode = 404
@ -393,16 +387,6 @@ export default class Server {
return true return true
} }
isInternalUrl (req) {
for (const prefix of internalPrefixes) {
if (prefix.test(req.url)) {
return true
}
}
return false
}
readBuildId () { readBuildId () {
const buildIdPath = join(this.dir, this.dist, 'BUILD_ID') const buildIdPath = join(this.dir, this.dist, 'BUILD_ID')
const buildId = fs.readFileSync(buildIdPath, 'utf8') const buildId = fs.readFileSync(buildIdPath, 'utf8')

View file

@ -1,20 +1,21 @@
import DynamicEntryPlugin from 'webpack/lib/DynamicEntryPlugin' import DynamicEntryPlugin from 'webpack/lib/DynamicEntryPlugin'
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import { join } from 'path' import { join, relative } from 'path'
import { parse } from 'url' import { parse } from 'url'
import resolvePath from './resolve'
import touch from 'touch' import touch from 'touch'
import resolvePath from './resolve'
import {createEntry} from './build/webpack/utils'
import { MATCH_ROUTE_NAME, IS_BUNDLED_PAGE } from './utils' import { MATCH_ROUTE_NAME, IS_BUNDLED_PAGE } from './utils'
const ADDED = Symbol('added') const ADDED = Symbol('added')
const BUILDING = Symbol('building') const BUILDING = Symbol('building')
const BUILT = Symbol('built') const BUILT = Symbol('built')
export default function onDemandEntryHandler (devMiddleware, compiler, { export default function onDemandEntryHandler (devMiddleware, compilers, {
dir, dir,
dev, dev,
reload, reload,
maxInactiveAge = 1000 * 25, maxInactiveAge = 1000 * 60,
pagesBufferLength = 2 pagesBufferLength = 2
}) { }) {
let entries = {} let entries = {}
@ -25,81 +26,90 @@ export default function onDemandEntryHandler (devMiddleware, compiler, {
let reloading = false let reloading = false
let stopped = false let stopped = false
let reloadCallbacks = new EventEmitter() let reloadCallbacks = new EventEmitter()
// Keep the names of compilers which are building pages at a given moment.
const currentBuilders = new Set()
compiler.plugin('make', function (compilation, done) { compilers.forEach(compiler => {
invalidator.startBuilding() compiler.plugin('make', function (compilation, done) {
invalidator.startBuilding()
currentBuilders.add(compiler.name)
const allEntries = Object.keys(entries).map((page) => { const allEntries = Object.keys(entries).map((page) => {
const { name, entry } = entries[page] const { name, entry } = entries[page]
entries[page].status = BUILDING entries[page].status = BUILDING
return addEntry(compilation, this.context, name, entry) return addEntry(compilation, this.context, name, entry)
})
Promise.all(allEntries)
.then(() => done())
.catch(done)
}) })
Promise.all(allEntries) compiler.plugin('done', function (stats) {
.then(() => done()) // Wait until all the compilers mark the build as done.
.catch(done) currentBuilders.delete(compiler.name)
}) if (currentBuilders.size !== 0) return
compiler.plugin('done', function (stats) { const { compilation } = stats
const { compilation } = stats const hardFailedPages = compilation.errors
const hardFailedPages = compilation.errors .filter(e => {
.filter(e => { // Make sure to only pick errors which marked with missing modules
// Make sure to only pick errors which marked with missing modules const hasNoModuleFoundError = /ENOENT/.test(e.message) || /Module not found/.test(e.message)
const hasNoModuleFoundError = /ENOENT/.test(e.message) || /Module not found/.test(e.message) if (!hasNoModuleFoundError) return false
if (!hasNoModuleFoundError) return false
// The page itself is missing. So this is a failed page. // The page itself is missing. So this is a failed page.
if (IS_BUNDLED_PAGE.test(e.module.name)) return true if (IS_BUNDLED_PAGE.test(e.module.name)) return true
// No dependencies means this is a top level page. // No dependencies means this is a top level page.
// So this is a failed page. // So this is a failed page.
return e.module.dependencies.length === 0 return e.module.dependencies.length === 0
}) })
.map(e => e.module.chunks) .map(e => e.module.chunks)
.reduce((a, b) => [...a, ...b], []) .reduce((a, b) => [...a, ...b], [])
.map(c => { .map(c => {
const pageName = MATCH_ROUTE_NAME.exec(c.name)[1] const pageName = MATCH_ROUTE_NAME.exec(c.name)[1]
return normalizePage(`/${pageName}`) return normalizePage(`/${pageName}`)
})
// Call all the doneCallbacks
Object.keys(entries).forEach((page) => {
const entryInfo = entries[page]
if (entryInfo.status !== BUILDING) return
// With this, we are triggering a filesystem based watch trigger
// It'll memorize some timestamp related info related to common files used
// in the page
// That'll reduce the page building time significantly.
if (!touchedAPage) {
setTimeout(() => {
touch.sync(entryInfo.pathname)
}, 1000)
touchedAPage = true
}
entryInfo.status = BUILT
entries[page].lastActiveTime = Date.now()
doneCallbacks.emit(page)
}) })
// Call all the doneCallbacks invalidator.doneBuilding(compiler.name)
Object.keys(entries).forEach((page) => {
const entryInfo = entries[page]
if (entryInfo.status !== BUILDING) return
// With this, we are triggering a filesystem based watch trigger if (hardFailedPages.length > 0 && !reloading) {
// It'll memorize some timestamp related info related to common files used console.log(`> Reloading webpack due to inconsistant state of pages(s): ${hardFailedPages.join(', ')}`)
// in the page reloading = true
// That'll reduce the page building time significantly. reload()
if (!touchedAPage) { .then(() => {
setTimeout(() => { console.log('> Webpack reloaded.')
touch.sync(entryInfo.pathname) reloadCallbacks.emit('done')
}, 1000) stop()
touchedAPage = true })
.catch(err => {
console.error(`> Webpack reloading failed: ${err.message}`)
console.error(err.stack)
process.exit(1)
})
} }
entryInfo.status = BUILT
entries[page].lastActiveTime = Date.now()
doneCallbacks.emit(page)
}) })
invalidator.doneBuilding()
if (hardFailedPages.length > 0 && !reloading) {
console.log(`> Reloading webpack due to inconsistant state of pages(s): ${hardFailedPages.join(', ')}`)
reloading = true
reload()
.then(() => {
console.log('> Webpack reloaded.')
reloadCallbacks.emit('done')
stop()
})
.catch(err => {
console.error(`> Webpack reloading failed: ${err.message}`)
console.error(err.stack)
process.exit(1)
})
}
}) })
const disposeHandler = setInterval(function () { const disposeHandler = setInterval(function () {
@ -130,9 +140,7 @@ export default function onDemandEntryHandler (devMiddleware, compiler, {
const pagePath = join(dir, 'pages', page) const pagePath = join(dir, 'pages', page)
const pathname = await resolvePath(pagePath) const pathname = await resolvePath(pagePath)
const name = join('bundles', pathname.substring(dir.length)) const {name, files} = createEntry(relative(dir, pathname))
const entry = [`${pathname}?entry`]
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const entryInfo = entries[page] const entryInfo = entries[page]
@ -144,19 +152,19 @@ export default function onDemandEntryHandler (devMiddleware, compiler, {
} }
if (entryInfo.status === BUILDING) { if (entryInfo.status === BUILDING) {
doneCallbacks.on(page, processCallback) doneCallbacks.once(page, handleCallback)
return return
} }
} }
console.log(`> Building page: ${page}`) console.log(`> Building page: ${page}`)
entries[page] = { name, entry, pathname, status: ADDED } entries[page] = { name, entry: files, pathname, status: ADDED }
doneCallbacks.on(page, processCallback) doneCallbacks.once(page, handleCallback)
invalidator.invalidate() invalidator.invalidate()
function processCallback (err) { function handleCallback (err) {
if (err) return reject(err) if (err) return reject(err)
resolve() resolve()
} }
@ -216,6 +224,7 @@ export default function onDemandEntryHandler (devMiddleware, compiler, {
} }
} }
// Based on https://github.com/webpack/webpack/blob/master/lib/DynamicEntryPlugin.js#L29-L37
function addEntry (compilation, context, name, entry) { function addEntry (compilation, context, name, entry) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const dep = DynamicEntryPlugin.createDependency(entry, name) const dep = DynamicEntryPlugin.createDependency(entry, name)
@ -272,6 +281,7 @@ function sendJson (res, payload) {
class Invalidator { class Invalidator {
constructor (devMiddleware) { constructor (devMiddleware) {
this.devMiddleware = devMiddleware this.devMiddleware = devMiddleware
// contains an array of types of compilers currently building
this.building = false this.building = false
this.rebuildAgain = false this.rebuildAgain = false
} }
@ -296,6 +306,7 @@ class Invalidator {
doneBuilding () { doneBuilding () {
this.building = false this.building = false
if (this.rebuildAgain) { if (this.rebuildAgain) {
this.rebuildAgain = false this.rebuildAgain = false
this.invalidate() this.invalidate()

View file

@ -6,7 +6,6 @@ import generateETag from 'etag'
import fresh from 'fresh' import fresh from 'fresh'
import requireModule from './require' import requireModule from './require'
import getConfig from './config' import getConfig from './config'
import resolvePath from './resolve'
import { Router } from '../lib/router' import { Router } from '../lib/router'
import { loadGetInitialProps } from '../lib/utils' import { loadGetInitialProps } from '../lib/utils'
import { getAvailableChunks } from './utils' import { getAvailableChunks } from './utils'
@ -53,9 +52,12 @@ async function doRender (req, res, pathname, query, {
const dist = getConfig(dir).distDir const dist = getConfig(dir).distDir
const pagePath = join(dir, dist, 'dist', 'bundles', 'pages', page)
const documentPath = join(dir, dist, 'dist', 'bundles', 'pages', '_document')
let [Component, Document] = await Promise.all([ let [Component, Document] = await Promise.all([
requireModule(join(dir, dist, 'dist', 'pages', page)), requireModule(pagePath),
requireModule(join(dir, dist, 'dist', 'pages', '_document')) requireModule(documentPath)
]) ])
Component = Component.default || Component Component = Component.default || Component
Document = Document.default || Document Document = Document.default || Document
@ -120,22 +122,6 @@ async function doRender (req, res, pathname, query, {
return '<!DOCTYPE html>' + renderToStaticMarkup(doc) return '<!DOCTYPE html>' + renderToStaticMarkup(doc)
} }
export async function renderScript (req, res, page, opts) {
try {
const dist = getConfig(opts.dir).distDir
const path = join(opts.dir, dist, 'bundles', 'pages', page)
const realPath = await resolvePath(path)
await serveStatic(req, res, realPath)
} catch (err) {
if (err.code === 'ENOENT') {
renderScriptError(req, res, page, err, {}, opts)
return
}
throw err
}
}
export async function renderScriptError (req, res, page, error, customFields, { dev }) { export async function renderScriptError (req, res, page, error, customFields, { dev }) {
// Asks CDNs and others to not to cache the errored page // Asks CDNs and others to not to cache the errored page
res.setHeader('Cache-Control', 'no-store, must-revalidate') res.setHeader('Cache-Control', 'no-store, must-revalidate')

View file

@ -28,12 +28,16 @@ function getPaths (id) {
if (i.slice(-3) === '.js') return [i] if (i.slice(-3) === '.js') return [i]
if (i.slice(-4) === '.jsx') return [i] if (i.slice(-4) === '.jsx') return [i]
if (i.slice(-4) === '.tsx') return [i]
if (i.slice(-3) === '.ts') return [i]
if (i.slice(-5) === '.json') return [i] if (i.slice(-5) === '.json') return [i]
if (i[i.length - 1] === sep) { if (i[i.length - 1] === sep) {
return [ return [
i + 'index.js', i + 'index.js',
i + 'index.jsx', i + 'index.jsx',
i + 'index.ts',
i + 'index.tsx',
i + 'index.json' i + 'index.json'
] ]
} }
@ -43,6 +47,10 @@ function getPaths (id) {
join(i, 'index.js'), join(i, 'index.js'),
i + '.jsx', i + '.jsx',
join(i, 'index.jsx'), join(i, 'index.jsx'),
i + '.tsx',
join(i, 'index.tsx'),
i + '.ts',
join(i, 'index.ts'),
i + '.json', i + '.json',
join(i, 'index.json') join(i, 'index.json')
] ]

View file

@ -1,8 +1,8 @@
import { join } from 'path' import { join } from 'path'
import { readdirSync, existsSync } from 'fs' import { readdirSync, existsSync } from 'fs'
export const IS_BUNDLED_PAGE = /^bundles[/\\]pages.*\.(js|jsx)$/ export const IS_BUNDLED_PAGE = /^bundles[/\\]pages.*\.js$/
export const MATCH_ROUTE_NAME = /^bundles[/\\]pages[/\\](.*)\.(js|jsx)$/ export const MATCH_ROUTE_NAME = /^bundles[/\\]pages[/\\](.*)\.js$/
export function getAvailableChunks (dir, dist) { export function getAvailableChunks (dir, dist) {
const chunksDir = join(dir, dist, 'chunks') const chunksDir = join(dir, dist, 'chunks')
@ -20,3 +20,37 @@ export function getAvailableChunks (dir, dist) {
return chunksMap return chunksMap
} }
const internalPrefixes = [
/^\/_next\//,
/^\/static\//
]
export function isInternalUrl (url) {
for (const prefix of internalPrefixes) {
if (prefix.test(url)) {
return true
}
}
return false
}
export function addCorsSupport (req, res) {
if (!req.headers.origin) {
return { preflight: false }
}
res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
res.setHeader('Access-Control-Request-Method', req.headers.origin)
res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET')
res.setHeader('Access-Control-Allow-Headers', req.headers.origin)
if (req.method === 'OPTIONS') {
res.writeHead(200)
res.end()
return { preflight: true }
}
return { preflight: false }
}

1
test/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
!node_modules

View file

@ -11,17 +11,17 @@ export default (context, render) => {
return cheerio.load(html) return cheerio.load(html)
} }
it('should render dynmaic import components', async () => { it('should render dynamic import components', async () => {
const $ = await get$('/dynamic/ssr') const $ = await get$('/dynamic/ssr')
expect($('p').text()).toBe('Hello World 1') expect($('p').text()).toBe('Hello World 1')
}) })
it('should stop render dynmaic import components', async () => { it('should stop render dynamic import components', async () => {
const $ = await get$('/dynamic/no-ssr') const $ = await get$('/dynamic/no-ssr')
expect($('p').text()).toBe('loading...') expect($('p').text()).toBe('loading...')
}) })
it('should stop render dynmaic import components with custom loading', async () => { it('should stop render dynamic import components with custom loading', async () => {
const $ = await get$('/dynamic/no-ssr-custom-loading') const $ = await get$('/dynamic/no-ssr-custom-loading')
expect($('p').text()).toBe('LOADING') expect($('p').text()).toBe('LOADING')
}) })

View file

@ -145,26 +145,6 @@ describe('Production Usage', () => {
await app.render(req, res, req.url) await app.render(req, res, req.url)
expect(headers['X-Powered-By']).toEqual(`Next.js ${pkg.version}`) expect(headers['X-Powered-By']).toEqual(`Next.js ${pkg.version}`)
}) })
it('should not set it when poweredByHeader==false', async () => {
const req = { url: '/stateless', headers: {} }
const originalConfigValue = app.config.poweredByHeader
app.config.poweredByHeader = false
const res = {
getHeader () {
return false
},
setHeader (key, value) {
if (key === 'X-Powered-By') {
throw new Error('Should not set the X-Powered-By header')
}
},
end () {}
}
await app.render(req, res, req.url)
app.config.poweredByHeader = originalConfigValue
})
}) })
dynamicImportTests(context, (p, q) => renderViaHTTP(context.appPort, p, q)) dynamicImportTests(context, (p, q) => renderViaHTTP(context.appPort, p, q))

1
test/node_modules/next generated vendored Symbolic link
View file

@ -0,0 +1 @@
../..

View file

@ -1,31 +0,0 @@
module.exports = function (wallaby) {
return {
files: [
'server/**/*.js',
'client/**/*.js',
'lib/**/*.js',
'dist/**/*.js',
'test/**/*.*',
'!test/**/*.test.js'
],
tests: [
'test/**/*.test.js',
'!test/integration/**/*.test.js'
],
compilers: {
'**/*.js': wallaby.compilers.babel()
},
env: {
type: 'node',
runner: 'node',
params: {
env: 'NODE_PATH=test/lib'
}
},
testFramework: 'jest'
}
}

1090
yarn.lock

File diff suppressed because it is too large Load diff