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:
parent
202ceca1af
commit
e093441bad
|
@ -1,11 +1,9 @@
|
|||
#!/usr/bin/env node
|
||||
import 'source-map-support/register'
|
||||
import { resolve, join } from 'path'
|
||||
import parseArgs from 'minimist'
|
||||
import { existsSync, readFileSync } from 'fs'
|
||||
import Server from '../server'
|
||||
import { printAndExit } from '../lib/utils'
|
||||
import pkgUp from 'pkg-up'
|
||||
|
||||
const argv = parseArgs(process.argv.slice(2), {
|
||||
alias: {
|
||||
|
@ -64,7 +62,7 @@ srv.start(argv.port, argv.hostname)
|
|||
.catch((err) => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
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 nextScript = Object.entries(appPackage.scripts).find(scriptLine => scriptLine[1] === 'next')
|
||||
if (nextScript) errorMessage += `\nUse \`npm run ${nextScript[0]} -- -p <some other port>\`.`
|
||||
|
|
|
@ -6,6 +6,7 @@ import EventEmitter from '../lib/EventEmitter'
|
|||
import App from '../lib/app'
|
||||
import { loadGetInitialProps, getURL } from '../lib/utils'
|
||||
import PageLoader from '../lib/page-loader'
|
||||
import * as asset from '../lib/asset'
|
||||
|
||||
// Polyfill Promise globally
|
||||
// This is needed because Webpack2's dynamic loading(common chunks) code
|
||||
|
@ -29,6 +30,9 @@ const {
|
|||
location
|
||||
} = window
|
||||
|
||||
// With this, static assets will work across zones
|
||||
asset.setAssetPrefix(assetPrefix)
|
||||
|
||||
const asPath = getURL()
|
||||
|
||||
const pageLoader = new PageLoader(buildId, assetPrefix)
|
||||
|
@ -93,10 +97,7 @@ export default async ({ ErrorDebugComponent: passedDebugComponent, stripAnsi: pa
|
|||
}
|
||||
|
||||
export async function render (props) {
|
||||
// There are some errors we should ignore.
|
||||
// Next.js rendering logic knows how to handle them.
|
||||
// These are specially 404 errors
|
||||
if (props.err && !props.err.ignore) {
|
||||
if (props.err) {
|
||||
await renderError(props.err)
|
||||
return
|
||||
}
|
||||
|
@ -159,7 +160,8 @@ async function doRender ({ Component, props, hash, err, emitter: emitterProp = e
|
|||
|
||||
let isInitialRender = true
|
||||
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)
|
||||
isInitialRender = false
|
||||
} else {
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import 'react-hot-loader/patch'
|
||||
import stripAnsi from 'strip-ansi'
|
||||
import initNext, * as next from './'
|
||||
import ErrorDebugComponent from '../lib/error-debug'
|
||||
import initOnDemandEntries from './on-demand-entries-client'
|
||||
import initWebpackHMR from './webpack-hot-middleware-client'
|
||||
|
||||
require('@zeit/source-map-support/browser-source-map-support')
|
||||
|
||||
window.next = next
|
||||
|
||||
initNext({ ErrorDebugComponent, stripAnsi })
|
||||
|
|
|
@ -3,6 +3,12 @@
|
|||
import Router from '../lib/router'
|
||||
import fetch from 'unfetch'
|
||||
|
||||
const {
|
||||
__NEXT_DATA__: {
|
||||
assetPrefix
|
||||
}
|
||||
} = window
|
||||
|
||||
export default () => {
|
||||
Router.ready(() => {
|
||||
Router.router.events.on('routeChangeComplete', ping)
|
||||
|
@ -10,16 +16,16 @@ export default () => {
|
|||
|
||||
async function ping () {
|
||||
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, {
|
||||
credentials: 'same-origin'
|
||||
credentials: 'omit'
|
||||
})
|
||||
const payload = await res.json()
|
||||
if (payload.invalid) {
|
||||
// Payload can be invalid even if the page is not exists.
|
||||
// So, we need to make sure it's exists before reloading.
|
||||
const pageRes = await fetch(location.href, {
|
||||
credentials: 'same-origin'
|
||||
credentials: 'omit'
|
||||
})
|
||||
if (pageRes.status === 200) {
|
||||
location.reload()
|
||||
|
|
|
@ -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'
|
||||
|
||||
const {
|
||||
__NEXT_DATA__: {
|
||||
assetPrefix
|
||||
}
|
||||
} = window
|
||||
|
||||
export default () => {
|
||||
webpackHotMiddlewareClient.setOptionsAndConnect({
|
||||
overlay: false,
|
||||
reload: true,
|
||||
path: `${assetPrefix}/_next/webpack-hmr`
|
||||
})
|
||||
|
||||
const handlers = {
|
||||
reload (route) {
|
||||
if (route === '/_error') {
|
||||
|
|
15
errors/powered-by-header-option-removed.md
Normal file
15
errors/powered-by-header-option-removed.md
Normal 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)
|
3
examples/hello-world/pages/about2.js
Normal file
3
examples/hello-world/pages/about2.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default () => (
|
||||
<div>About 2</div>
|
||||
)
|
3
examples/hello-world/pages/day/index.js
Normal file
3
examples/hello-world/pages/day/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default () => (
|
||||
<div>Hello Day</div>
|
||||
)
|
71
examples/with-zones/README.md
Normal file
71
examples/with-zones/README.md
Normal 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
2
examples/with-zones/blog/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.next
|
||||
node_modules
|
6
examples/with-zones/blog/next.config.js
Normal file
6
examples/with-zones/blog/next.config.js
Normal 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'
|
||||
}
|
3
examples/with-zones/blog/now.json
Normal file
3
examples/with-zones/blog/now.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"alias": "with-zones-blog.now.sh"
|
||||
}
|
14
examples/with-zones/blog/package.json
Normal file
14
examples/with-zones/blog/package.json
Normal 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"
|
||||
}
|
5
examples/with-zones/blog/pages/blog.js
Normal file
5
examples/with-zones/blog/pages/blog.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default () => (
|
||||
<div>
|
||||
This is our blog
|
||||
</div>
|
||||
)
|
2
examples/with-zones/home/.gitignore
vendored
Normal file
2
examples/with-zones/home/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.next
|
||||
node_modules
|
5
examples/with-zones/home/components/Header.js
Normal file
5
examples/with-zones/home/components/Header.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default () => (
|
||||
<div>
|
||||
<h2>The Company</h2>
|
||||
</div>
|
||||
)
|
6
examples/with-zones/home/next.config.js
Normal file
6
examples/with-zones/home/next.config.js
Normal 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'
|
||||
}
|
3
examples/with-zones/home/now.json
Normal file
3
examples/with-zones/home/now.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"alias": "with-zones-home.now.sh"
|
||||
}
|
14
examples/with-zones/home/package.json
Normal file
14
examples/with-zones/home/package.json
Normal 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"
|
||||
}
|
10
examples/with-zones/home/pages/about.js
Normal file
10
examples/with-zones/home/pages/about.js
Normal 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>
|
||||
)
|
15
examples/with-zones/home/pages/index.js
Normal file
15
examples/with-zones/home/pages/index.js
Normal 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>
|
||||
)
|
BIN
examples/with-zones/home/static/nextjs.png
Normal file
BIN
examples/with-zones/home/static/nextjs.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 68 KiB |
BIN
examples/with-zones/home/static/zeit.png
Normal file
BIN
examples/with-zones/home/static/zeit.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
20
examples/with-zones/package.json
Normal file
20
examples/with-zones/package.json
Normal 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"
|
||||
}
|
6
examples/with-zones/rules-dev.json
Normal file
6
examples/with-zones/rules-dev.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"rules": [
|
||||
{"pathname": "/blog", "method":["GET", "POST", "OPTIONS"], "dest": "http://localhost:5000"},
|
||||
{"pathname": "/**", "dest": "http://localhost:4000"}
|
||||
]
|
||||
}
|
6
examples/with-zones/rules-prod.json
Normal file
6
examples/with-zones/rules-prod.json
Normal 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
10
lib/asset.js
Normal 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
|
||||
}
|
|
@ -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`)
|
|
@ -160,6 +160,11 @@ export function flushChunks () {
|
|||
}
|
||||
|
||||
export class SameLoopPromise {
|
||||
static resolve (value) {
|
||||
const promise = new SameLoopPromise((done) => done(value))
|
||||
return promise
|
||||
}
|
||||
|
||||
constructor (cb) {
|
||||
this.onResultCallbacks = []
|
||||
this.onErrorCallbacks = []
|
||||
|
|
|
@ -33,7 +33,7 @@ function reduceComponents (components) {
|
|||
.filter(unique())
|
||||
.reverse()
|
||||
.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 })
|
||||
})
|
||||
}
|
||||
|
|
|
@ -153,6 +153,7 @@ export default class Link extends Component {
|
|||
function isLocal (href) {
|
||||
const url = parse(href, false, true)
|
||||
const origin = parse(getLocationOrigin(), false, true)
|
||||
|
||||
return !url.host ||
|
||||
(url.protocol === origin.protocol && url.host === origin.host)
|
||||
}
|
||||
|
|
|
@ -129,7 +129,6 @@ export default class Router {
|
|||
}
|
||||
|
||||
this.abortComponentLoad(as)
|
||||
const { pathname, query } = parse(url, true)
|
||||
|
||||
// If the url change is only related to a hash change
|
||||
// We should not proceed. We should only change the state.
|
||||
|
@ -139,6 +138,8 @@ export default class Router {
|
|||
return
|
||||
}
|
||||
|
||||
const { pathname, query } = parse(url, true)
|
||||
|
||||
// If asked to change the current URL we should reload the current page
|
||||
// (not location.reload() but reload getInitalProps and other Next.js stuffs)
|
||||
// We also need to set the method = replaceState always
|
||||
|
@ -209,10 +210,6 @@ export default class Router {
|
|||
|
||||
this.components[route] = routeInfo
|
||||
} catch (err) {
|
||||
if (err.cancelled) {
|
||||
return { error: err }
|
||||
}
|
||||
|
||||
if (err.buildIdMismatched) {
|
||||
// Now we need to reload the page or do the action asked by the user
|
||||
_notifyBuildIdMismatch(as)
|
||||
|
@ -223,9 +220,21 @@ export default class Router {
|
|||
}
|
||||
|
||||
if (err.statusCode === 404) {
|
||||
// Indicate main error display logic to
|
||||
// ignore rendering this error as a runtime error.
|
||||
err.ignore = true
|
||||
// If there's 404 error for the page, it could be due to two reasons.
|
||||
// 1. Page is not exists
|
||||
// 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
|
||||
|
@ -301,7 +310,6 @@ export default class Router {
|
|||
cancelled = true
|
||||
}
|
||||
|
||||
try {
|
||||
const Component = await this.fetchRoute(route)
|
||||
|
||||
if (cancelled) {
|
||||
|
@ -315,15 +323,6 @@ export default class Router {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
async getInitialProps (Component, ctx) {
|
||||
|
|
27
package.json
27
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "next",
|
||||
"version": "4.4.0-canary.3",
|
||||
"version": "5.0.0-universal-alpha.14",
|
||||
"description": "Minimalistic framework for server-rendered React applications",
|
||||
"main": "./dist/server/next.js",
|
||||
"license": "MIT",
|
||||
|
@ -18,6 +18,7 @@
|
|||
"dynamic.js",
|
||||
"prefetch.js",
|
||||
"router.js",
|
||||
"asset.js",
|
||||
"error.js"
|
||||
],
|
||||
"bin": {
|
||||
|
@ -29,8 +30,9 @@
|
|||
"pretestonly": "taskr pretest",
|
||||
"testonly": "cross-env NODE_PATH=test/lib jest \\.test.js",
|
||||
"posttestonly": "taskr posttest",
|
||||
"testall": "npm run testonly -- --coverage --forceExit --runInBand --verbose --bail",
|
||||
"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",
|
||||
"lint": "standard 'bin/*' 'client/**/*.js' 'examples/**/*.js' 'lib/**/*.js' 'pages/**/*.js' 'server/**/*.js' 'test/**/*.js'",
|
||||
"prepublish": "npm run release",
|
||||
|
@ -48,15 +50,13 @@
|
|||
"bin/*": "standard"
|
||||
},
|
||||
"dependencies": {
|
||||
"@zeit/source-map-support": "0.6.0",
|
||||
"ansi-html": "0.0.7",
|
||||
"babel-core": "6.26.0",
|
||||
"babel-generator": "6.26.0",
|
||||
"babel-loader": "7.1.2",
|
||||
"babel-plugin-module-resolver": "2.7.1",
|
||||
"babel-plugin-react-require": "3.0.0",
|
||||
"babel-plugin-syntax-dynamic-import": "6.18.0",
|
||||
"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-react-jsx-source": "6.22.0",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.8",
|
||||
|
@ -69,10 +69,11 @@
|
|||
"cross-spawn": "5.1.0",
|
||||
"del": "3.0.0",
|
||||
"etag": "1.8.1",
|
||||
"find-up": "2.1.0",
|
||||
"fresh": "0.5.2",
|
||||
"friendly-errors-webpack-plugin": "1.6.1",
|
||||
"glob": "7.1.2",
|
||||
"glob-promise": "3.2.0",
|
||||
"glob-promise": "3.3.0",
|
||||
"hoist-non-react-statics": "2.3.1",
|
||||
"htmlescape": "1.1.1",
|
||||
"http-status": "1.0.1",
|
||||
|
@ -87,25 +88,25 @@
|
|||
"pkg-up": "2.0.0",
|
||||
"prop-types": "15.6.0",
|
||||
"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",
|
||||
"resolve": "1.5.0",
|
||||
"send": "0.16.1",
|
||||
"source-map-support": "0.4.18",
|
||||
"strip-ansi": "3.0.1",
|
||||
"styled-jsx": "2.2.1",
|
||||
"touch": "3.1.0",
|
||||
"uglifyjs-webpack-plugin": "1.1.6",
|
||||
"unfetch": "3.0.0",
|
||||
"url": "0.11.0",
|
||||
"uuid": "3.1.0",
|
||||
"walk": "2.3.9",
|
||||
"webpack": "3.6.0",
|
||||
"webpack": "3.10.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",
|
||||
"xss-filters": "1.2.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"uglifyjs-webpack-plugin": "^1.1.1",
|
||||
"@taskr/babel": "1.1.0",
|
||||
"@taskr/clear": "1.1.0",
|
||||
"@taskr/esnext": "1.1.0",
|
||||
|
@ -129,8 +130,8 @@
|
|||
"node-notifier": "5.1.2",
|
||||
"nyc": "11.2.1",
|
||||
"portfinder": "1.0.13",
|
||||
"react": "16.0.0",
|
||||
"react-dom": "16.0.0",
|
||||
"react": "16.2.0",
|
||||
"react-dom": "16.2.0",
|
||||
"standard": "9.0.2",
|
||||
"taskr": "1.1.0",
|
||||
"wd": "1.4.1"
|
||||
|
|
34
readme.md
34
readme.md
|
@ -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.
|
||||
|
||||
#### 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)
|
||||
|
||||
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)
|
||||
|
||||
module.exports = {
|
||||
webpack: (config, { buildId, dev }) => {
|
||||
webpack: (config, { buildId, dev, isServer, defaultLoaders }) => {
|
||||
// Perform customizations to webpack 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
|
||||
|
||||
|
@ -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.
|
||||
</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>
|
||||
<summary>What syntactic features are transpiled? How do I change them?</summary>
|
||||
|
||||
|
|
|
@ -16,16 +16,6 @@ const TYPE_IMPORT = 'Import'
|
|||
|
||||
const buildImport = (args) => (template(`
|
||||
(
|
||||
typeof require.resolveWeak !== 'function' ?
|
||||
new (require('next/dynamic').SameLoopPromise)((resolve, reject) => {
|
||||
eval('require.ensure = function (deps, callback) { callback(require) }')
|
||||
require.ensure([], (require) => {
|
||||
let m = require(SOURCE)
|
||||
m.__webpackChunkName = '${args.name}'
|
||||
resolve(m);
|
||||
}, 'chunks/${args.name}');
|
||||
})
|
||||
:
|
||||
new (require('next/dynamic').SameLoopPromise)((resolve, reject) => {
|
||||
const weakId = require.resolveWeak(SOURCE)
|
||||
try {
|
||||
|
@ -36,6 +26,7 @@ const buildImport = (args) => (template(`
|
|||
require.ensure([], (require) => {
|
||||
try {
|
||||
let m = require(SOURCE)
|
||||
m.__webpackChunkName = '${args.name}'
|
||||
resolve(m)
|
||||
} catch(error) {
|
||||
reject(error)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
const relativeResolve = require('../root-module-relative-path').default(require)
|
||||
|
||||
// Resolve styled-jsx plugins
|
||||
function styledJsxOptions (opts) {
|
||||
if (!opts) {
|
||||
|
@ -45,29 +43,12 @@ module.exports = (context, opts = {}) => ({
|
|||
require.resolve('babel-preset-react')
|
||||
],
|
||||
plugins: [
|
||||
require.resolve('./plugins/remove-dotjsx-from-import'),
|
||||
require.resolve('babel-plugin-react-require'),
|
||||
require.resolve('./plugins/handle-import'),
|
||||
require.resolve('babel-plugin-transform-object-rest-spread'),
|
||||
require.resolve('babel-plugin-transform-class-properties'),
|
||||
[require.resolve('babel-plugin-transform-runtime'), opts['transform-runtime'] || {}],
|
||||
[require.resolve('styled-jsx/babel'), styledJsxOptions(opts['styled-jsx'])],
|
||||
...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')
|
||||
}
|
||||
}
|
||||
]
|
||||
...plugins
|
||||
]
|
||||
})
|
||||
|
|
|
@ -1,44 +1,42 @@
|
|||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import fs from 'mz/fs'
|
||||
import uuid from 'uuid'
|
||||
import del from 'del'
|
||||
import webpack from './webpack'
|
||||
import replaceCurrentBuild from './replace'
|
||||
import webpack from 'webpack'
|
||||
import getConfig from '../config'
|
||||
import getBaseWebpackConfig from './webpack'
|
||||
import md5File from 'md5-file/promise'
|
||||
|
||||
export default async function build (dir, conf = null) {
|
||||
const config = getConfig(dir, conf)
|
||||
const buildId = uuid.v4()
|
||||
const tempDir = tmpdir()
|
||||
const buildDir = join(tempDir, uuid.v4())
|
||||
|
||||
try {
|
||||
await fs.access(tempDir, fs.constants.W_OK)
|
||||
await fs.access(dir, fs.constants.W_OK)
|
||||
} catch (err) {
|
||||
console.error(`> Failed, build directory is not writeable. https://err.sh/zeit/next.js/build-dir-not-writeable`)
|
||||
throw err
|
||||
}
|
||||
|
||||
const compiler = await webpack(dir, { buildId, buildDir, conf })
|
||||
|
||||
try {
|
||||
const stats = await runCompiler(compiler)
|
||||
await writeBuildStats(buildDir, stats)
|
||||
await writeBuildId(buildDir, buildId)
|
||||
const configs = await Promise.all([
|
||||
getBaseWebpackConfig(dir, { buildId, isServer: false, config }),
|
||||
getBaseWebpackConfig(dir, { buildId, isServer: true, config })
|
||||
])
|
||||
|
||||
await runCompiler(configs)
|
||||
|
||||
await writeBuildStats(dir, config)
|
||||
await writeBuildId(dir, buildId, config)
|
||||
} catch (err) {
|
||||
console.error(`> Failed to build on ${buildDir}`)
|
||||
console.error(`> Failed to build`)
|
||||
throw err
|
||||
}
|
||||
|
||||
await replaceCurrentBuild(dir, buildDir)
|
||||
|
||||
// no need to wait
|
||||
del(buildDir, { force: true })
|
||||
}
|
||||
|
||||
function runCompiler (compiler) {
|
||||
return new Promise((resolve, reject) => {
|
||||
compiler.run((err, stats) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const webpackCompiler = await webpack(await compiler)
|
||||
webpackCompiler.run((err, stats) => {
|
||||
if (err) return reject(err)
|
||||
|
||||
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.
|
||||
// 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)
|
||||
// So, we need to generate the hash ourself.
|
||||
const assetHashMap = {
|
||||
'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')
|
||||
}
|
||||
|
||||
async function writeBuildId (dir, buildId) {
|
||||
const buildIdPath = join(dir, '.next', 'BUILD_ID')
|
||||
async function writeBuildId (dir, buildId, config) {
|
||||
const buildIdPath = join(dir, config.distDir, 'BUILD_ID')
|
||||
await fs.writeFile(buildIdPath, buildId, 'utf8')
|
||||
}
|
||||
|
|
|
@ -8,12 +8,6 @@ module.exports = function (content, sourceMap) {
|
|||
this.callback(null, `${content}
|
||||
(function (Component, route) {
|
||||
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()
|
||||
Component.__route = route
|
||||
|
||||
|
|
29
server/build/plugins/nextjs-ssr-import.js
Normal file
29
server/build/plugins/nextjs-ssr-import.js
Normal 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
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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()))
|
||||
}
|
|
@ -1,113 +1,184 @@
|
|||
import { resolve, join, sep } from 'path'
|
||||
import { createHash } from 'crypto'
|
||||
import { realpathSync, existsSync } from 'fs'
|
||||
import path, {sep} from 'path'
|
||||
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 FriendlyErrorsWebpackPlugin from 'friendly-errors-webpack-plugin'
|
||||
import CaseSensitivePathPlugin from 'case-sensitive-paths-webpack-plugin'
|
||||
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 {getPages} from './webpack/utils'
|
||||
import CombineAssetsPlugin from './plugins/combine-assets-plugin'
|
||||
import getConfig from '../config'
|
||||
import * as babelCore from 'babel-core'
|
||||
import PagesPlugin from './plugins/pages-plugin'
|
||||
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 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 = [
|
||||
'_error.js',
|
||||
'_document.js'
|
||||
]
|
||||
const nextPagesDir = join(__dirname, '..', '..', 'pages')
|
||||
const nextNodeModulesDir = join(__dirname, '..', '..', '..', 'node_modules')
|
||||
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}) {
|
||||
const mainBabelOptions = {
|
||||
cacheDirectory: true,
|
||||
presets: [],
|
||||
plugins: [
|
||||
dev && !isServer && require.resolve('react-hot-loader/babel')
|
||||
].filter(Boolean)
|
||||
}
|
||||
|
||||
async function getPages ({dir, dev, pagesGlobPattern}) {
|
||||
let pages
|
||||
|
||||
if (dev) {
|
||||
pages = await glob('pages/+(_document|_error).+(js|jsx)', { cwd: dir })
|
||||
const externalBabelConfig = findBabelConfig(dir)
|
||||
if (externalBabelConfig) {
|
||||
// Log it out once
|
||||
if (!isServer) {
|
||||
console.log(`> Using external babel configuration`)
|
||||
console.log(`> Location: "${externalBabelConfig.loc}"`)
|
||||
}
|
||||
// It's possible to turn off babelrc support via babelrc itself.
|
||||
// In that case, we should add our default preset.
|
||||
// That's why we need to do this.
|
||||
const { options } = externalBabelConfig
|
||||
mainBabelOptions.babelrc = options.babelrc !== false
|
||||
} else {
|
||||
pages = await glob(pagesGlobPattern, { cwd: dir })
|
||||
mainBabelOptions.babelrc = false
|
||||
}
|
||||
|
||||
return pages
|
||||
// Add our default preset if the no "babelrc" found.
|
||||
if (!mainBabelOptions.babelrc) {
|
||||
mainBabelOptions.presets.push(require.resolve('./babel/preset'))
|
||||
}
|
||||
|
||||
return mainBabelOptions
|
||||
}
|
||||
|
||||
function getPageEntries (pages) {
|
||||
const entries = {}
|
||||
for (const p of pages) {
|
||||
entries[join('bundles', p.replace('.jsx', '.js'))] = [`./${p}?entry`]
|
||||
function externalsConfig (dir, isServer) {
|
||||
const externals = []
|
||||
|
||||
if (!isServer) {
|
||||
return externals
|
||||
}
|
||||
|
||||
// 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']
|
||||
}
|
||||
// This will externalize all the 'next/xxx' modules to load from
|
||||
// node_modules always.
|
||||
// This is very useful in Next.js development where we use symlinked version
|
||||
// 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) {
|
||||
return callback()
|
||||
}
|
||||
|
||||
return entries
|
||||
if (res.match(/node_modules/)) {
|
||||
return callback(null, `commonjs ${request}`)
|
||||
}
|
||||
|
||||
callback()
|
||||
})
|
||||
})
|
||||
|
||||
return externals
|
||||
}
|
||||
|
||||
export default async function createCompiler (dir, { buildId, dev = false, quiet = false, buildDir, conf = null } = {}) {
|
||||
// Resolve relative path to absolute path
|
||||
dir = realpathSync(resolve(dir))
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = {
|
||||
let webpackConfig = {
|
||||
devtool: dev ? 'source-map' : false,
|
||||
name: isServer ? 'server' : 'client',
|
||||
cache: true,
|
||||
target: isServer ? 'node' : 'web',
|
||||
externals: externalsConfig(dir, isServer),
|
||||
context: dir,
|
||||
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': [
|
||||
...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
|
||||
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: {
|
||||
path: path.join(dir, config.distDir, isServer ? 'dist' : ''), // server compilation goes to `.next/dist`
|
||||
filename: '[name]',
|
||||
libraryTarget: 'commonjs2',
|
||||
publicPath: `${config.assetPrefix}/_next/webpack/`,
|
||||
// This saves chunks with the name given via require.ensure()
|
||||
chunkFilename: '[name]-[chunkhash].js',
|
||||
strictModuleExceptionHandling: true,
|
||||
devtoolModuleFilenameTemplate: '[absolute-resource-path]'
|
||||
},
|
||||
performance: { hints: false },
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx', '.json'],
|
||||
modules: [
|
||||
nextNodeModulesDir,
|
||||
'node_modules'
|
||||
],
|
||||
...pageEntries
|
||||
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'
|
||||
}
|
||||
|
||||
return entries
|
||||
},
|
||||
resolveLoader: {
|
||||
modules: [
|
||||
nextNodeModulesDir,
|
||||
'node_modules',
|
||||
path.join(__dirname, 'loaders')
|
||||
]
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
dev && !isServer && {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
].filter(Boolean)
|
||||
},
|
||||
plugins: [
|
||||
new webpack.IgnorePlugin(/(precomputed)/, /node_modules.+(elliptic)/),
|
||||
// Provide legacy options to webpack
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
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) {
|
||||
|
@ -115,17 +186,63 @@ export default async function createCompiler (dir, { buildId, dev = false, quiet
|
|||
}
|
||||
}
|
||||
}),
|
||||
// Writes all generated files to disk, even in development. For SSR.
|
||||
new WriteFilePlugin({
|
||||
dev && 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',
|
||||
!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
|
||||
|
@ -154,9 +271,7 @@ export default async function createCompiler (dir, { buildId, dev = false, quiet
|
|||
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({
|
||||
!isServer && new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'react',
|
||||
filename: 'react.js',
|
||||
minChunks (module, count) {
|
||||
|
@ -175,264 +290,16 @@ export default async function createCompiler (dir, { buildId, dev = false, quiet
|
|||
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({
|
||||
!isServer && 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())
|
||||
].filter(Boolean)
|
||||
}
|
||||
|
||||
const nodePathList = (process.env.NODE_PATH || '')
|
||||
.split(process.platform === 'win32' ? ';' : ':')
|
||||
.filter((p) => !!p)
|
||||
|
||||
const mainBabelOptions = {
|
||||
cacheDirectory: true,
|
||||
presets: []
|
||||
if (typeof config.webpack === 'function') {
|
||||
webpackConfig = config.webpack(webpackConfig, {dir, dev, isServer, buildId, config, defaultLoaders})
|
||||
}
|
||||
|
||||
const externalBabelConfig = findBabelConfig(dir)
|
||||
if (externalBabelConfig) {
|
||||
console.log(`> Using external babel configuration`)
|
||||
console.log(`> Location: "${externalBabelConfig.loc}"`)
|
||||
// It's possible to turn off babelrc support via babelrc itself.
|
||||
// In that case, we should add our default preset.
|
||||
// That's why we need to do this.
|
||||
const { options } = externalBabelConfig
|
||||
mainBabelOptions.babelrc = options.babelrc !== false
|
||||
} else {
|
||||
mainBabelOptions.babelrc = false
|
||||
}
|
||||
|
||||
// Add our default preset if the no "babelrc" found.
|
||||
if (!mainBabelOptions.babelrc) {
|
||||
mainBabelOptions.presets.push(require.resolve('./babel/preset'))
|
||||
}
|
||||
|
||||
const devLoaders = dev ? [{
|
||||
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 = [{
|
||||
test: /\.json$/,
|
||||
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) {
|
||||
const {from, to} = item
|
||||
if (file.slice(-(from.length)) !== from) {
|
||||
continue
|
||||
}
|
||||
|
||||
const filePath = file.slice(0, -(from.length)) + to
|
||||
|
||||
if (existsSync(filePath)) {
|
||||
throw new Error(`Both ${from} and ${to} file found. Please make sure you only have one of both.`)
|
||||
}
|
||||
}
|
||||
},
|
||||
// By default, our babel config does not transpile ES2015 module syntax because
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
loader: 'babel-loader',
|
||||
include: nextPagesDir,
|
||||
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 = {
|
||||
context: dir,
|
||||
entry,
|
||||
output: {
|
||||
path: buildDir ? join(buildDir, '.next') : join(dir, config.distDir),
|
||||
filename: '[name]',
|
||||
libraryTarget: 'commonjs2',
|
||||
publicPath: `/_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()
|
||||
chunkFilename: '[name]-[chunkhash].js'
|
||||
},
|
||||
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'],
|
||||
modules: [
|
||||
nextNodeModulesDir,
|
||||
'node_modules',
|
||||
...nodePathList
|
||||
]
|
||||
},
|
||||
resolveLoader: {
|
||||
modules: [
|
||||
nextNodeModulesDir,
|
||||
'node_modules',
|
||||
join(__dirname, 'loaders'),
|
||||
...nodePathList
|
||||
]
|
||||
},
|
||||
plugins,
|
||||
module: {
|
||||
rules: [
|
||||
...devLoaders,
|
||||
...loaders
|
||||
]
|
||||
},
|
||||
devtool: dev ? 'cheap-module-inline-source-map' : false,
|
||||
performance: { hints: false }
|
||||
}
|
||||
|
||||
if (config.webpack) {
|
||||
console.log(`> Using "webpack" config function defined in ${config.configOrigin}.`)
|
||||
webpackConfig = await config.webpack(webpackConfig, { buildId, dev })
|
||||
}
|
||||
return webpack(webpackConfig)
|
||||
return webpackConfig
|
||||
}
|
||||
|
|
68
server/build/webpack/utils.js
Normal file
68
server/build/webpack/utils.js
Normal 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
|
||||
}
|
|
@ -5,12 +5,10 @@ const cache = new Map()
|
|||
const defaultConfig = {
|
||||
webpack: null,
|
||||
webpackDevMiddleware: null,
|
||||
poweredByHeader: true,
|
||||
distDir: '.next',
|
||||
assetPrefix: '',
|
||||
configOrigin: 'default',
|
||||
useFileSystemPublicRoutes: true,
|
||||
pagesGlobPattern: 'pages/**/*.+(js|jsx)'
|
||||
useFileSystemPublicRoutes: true
|
||||
}
|
||||
|
||||
export default function getConfig (dir, customConfig) {
|
||||
|
@ -34,6 +32,9 @@ function loadConfig (dir, customConfig) {
|
|||
if (path && path.length) {
|
||||
const userConfigModule = require(path)
|
||||
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'
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import htmlescape from 'htmlescape'
|
|||
import flush from 'styled-jsx/server'
|
||||
|
||||
const Fragment = React.Fragment || function Fragment ({ children }) {
|
||||
return children
|
||||
return <div>{children}</div>
|
||||
}
|
||||
|
||||
export default class Document extends Component {
|
||||
|
@ -106,7 +106,6 @@ export class Main extends Component {
|
|||
|
||||
render () {
|
||||
const { html, errorHtml } = this.context._documentProps
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div id='__next' dangerouslySetInnerHTML={{ __html: html }} />
|
||||
|
|
|
@ -2,12 +2,14 @@ import { join, relative, sep } from 'path'
|
|||
import WebpackDevMiddleware from 'webpack-dev-middleware'
|
||||
import WebpackHotMiddleware from 'webpack-hot-middleware'
|
||||
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 getConfig from './config'
|
||||
import UUID from 'uuid'
|
||||
import {
|
||||
IS_BUNDLED_PAGE
|
||||
IS_BUNDLED_PAGE,
|
||||
addCorsSupport
|
||||
} from './utils'
|
||||
|
||||
export default class HotReloader {
|
||||
|
@ -34,6 +36,15 @@ export default class HotReloader {
|
|||
}
|
||||
|
||||
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) {
|
||||
await new Promise((resolve, reject) => {
|
||||
fn(req, res, (err) => {
|
||||
|
@ -45,15 +56,19 @@ export default class HotReloader {
|
|||
}
|
||||
|
||||
async start () {
|
||||
const [compiler] = await Promise.all([
|
||||
webpack(this.dir, { buildId: this.buildId, dev: true, quiet: this.quiet }),
|
||||
clean(this.dir)
|
||||
await 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)
|
||||
this.assignBuildTools(buildTools)
|
||||
|
||||
this.stats = await this.waitUntilValid()
|
||||
this.stats = (await this.waitUntilValid()).stats[0]
|
||||
}
|
||||
|
||||
async stop (webpackDevMiddleware) {
|
||||
|
@ -71,11 +86,15 @@ export default class HotReloader {
|
|||
async reload () {
|
||||
this.stats = null
|
||||
|
||||
const [compiler] = await Promise.all([
|
||||
webpack(this.dir, { buildId: this.buildId, dev: true, quiet: this.quiet }),
|
||||
clean(this.dir)
|
||||
await 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)
|
||||
this.stats = await this.waitUntilValid(buildTools.webpackDevMiddleware)
|
||||
|
||||
|
@ -97,7 +116,9 @@ export default class HotReloader {
|
|||
}
|
||||
|
||||
async prepareBuildTools (compiler) {
|
||||
compiler.plugin('after-emit', (compilation, callback) => {
|
||||
// This flushes require.cache after emitting the files. Providing 'hot reloading' of server files.
|
||||
compiler.compilers.forEach((singleCompiler) => {
|
||||
singleCompiler.plugin('after-emit', (compilation, callback) => {
|
||||
const { assets } = compilation
|
||||
|
||||
if (this.prevAssets) {
|
||||
|
@ -114,8 +135,9 @@ export default class HotReloader {
|
|||
|
||||
callback()
|
||||
})
|
||||
})
|
||||
|
||||
compiler.plugin('done', (stats) => {
|
||||
compiler.compilers[0].plugin('done', (stats) => {
|
||||
const { compilation } = stats
|
||||
const chunkNames = new Set(
|
||||
compilation.chunks
|
||||
|
@ -193,12 +215,13 @@ export default class HotReloader {
|
|||
|
||||
const webpackDevMiddleware = WebpackDevMiddleware(compiler, webpackDevMiddlewareConfig)
|
||||
|
||||
const webpackHotMiddleware = WebpackHotMiddleware(compiler, {
|
||||
const webpackHotMiddleware = WebpackHotMiddleware(compiler.compilers[0], {
|
||||
path: '/_next/webpack-hmr',
|
||||
log: false,
|
||||
heartbeat: 2500
|
||||
})
|
||||
const onDemandEntries = onDemandEntryHandler(webpackDevMiddleware, compiler, {
|
||||
|
||||
const onDemandEntries = onDemandEntryHandler(webpackDevMiddleware, compiler.compilers, {
|
||||
dir: this.dir,
|
||||
dev: true,
|
||||
reload: this.reload.bind(this),
|
||||
|
@ -249,8 +272,8 @@ export default class HotReloader {
|
|||
this.webpackHotMiddleware.publish({ action, data: args })
|
||||
}
|
||||
|
||||
ensurePage (page) {
|
||||
return this.onDemandEntries.ensurePage(page)
|
||||
async ensurePage (page) {
|
||||
await this.onDemandEntries.ensurePage(page)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
require('@zeit/source-map-support').install()
|
||||
import { resolve, join, sep } from 'path'
|
||||
import { parse as parseUrl } from 'url'
|
||||
import { parse as parseQs } from 'querystring'
|
||||
|
@ -11,28 +12,11 @@ import {
|
|||
renderScriptError
|
||||
} from './render'
|
||||
import Router from './router'
|
||||
import { getAvailableChunks } from './utils'
|
||||
import { getAvailableChunks, isInternalUrl } from './utils'
|
||||
import getConfig from './config'
|
||||
// We need to go up one more level since we are in the `dist` directory
|
||||
import pkg from '../../package'
|
||||
import reactPkg from 'react/package'
|
||||
|
||||
// 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\//
|
||||
]
|
||||
import * as asset from '../lib/asset'
|
||||
|
||||
const blockedPages = {
|
||||
'/_document': true,
|
||||
|
@ -41,14 +25,6 @@ const blockedPages = {
|
|||
|
||||
export default class Server {
|
||||
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.dev = dev
|
||||
this.quiet = quiet
|
||||
|
@ -74,6 +50,9 @@ export default class Server {
|
|||
availableChunks: dev ? {} : getAvailableChunks(this.dir, this.dist)
|
||||
}
|
||||
|
||||
// With this, static assets will work across zones
|
||||
asset.setAssetPrefix(this.config.assetPrefix)
|
||||
|
||||
this.defineRoutes()
|
||||
}
|
||||
|
||||
|
@ -182,6 +161,23 @@ export default class Server {
|
|||
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) => {
|
||||
if (!this.handleBuildId(params.buildId, res)) {
|
||||
const error = new Error('INVALID_BUILD_ID')
|
||||
|
@ -196,9 +192,6 @@ export default class Server {
|
|||
|
||||
'/_next/:buildId/page/:path*.js': async (req, res, params) => {
|
||||
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('/')}`
|
||||
|
||||
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('/'))
|
||||
if (!fs.existsSync(`${p}.js`)) {
|
||||
p = join(p, 'index') // It's possible to have index.js in a subfolder
|
||||
}
|
||||
await this.serveStatic(req, res, `${p}.js`)
|
||||
const p = join(this.dir, this.dist, 'bundles', 'pages', `${page}.js`)
|
||||
await this.serveStatic(req, res, p)
|
||||
},
|
||||
|
||||
'/_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.
|
||||
|
@ -293,7 +288,7 @@ export default class Server {
|
|||
}
|
||||
|
||||
async render (req, res, pathname, query, parsedUrl) {
|
||||
if (this.isInternalUrl(req)) {
|
||||
if (isInternalUrl(req.url)) {
|
||||
return this.handleRequest(req, res, parsedUrl)
|
||||
}
|
||||
|
||||
|
@ -301,10 +296,8 @@ export default class Server {
|
|||
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)
|
||||
res.setHeader('X-Powered-By', `Next.js ${pkg.version}`)
|
||||
return sendHTML(req, res, html, req.method, this.renderOpts)
|
||||
}
|
||||
|
||||
|
@ -318,7 +311,8 @@ export default class Server {
|
|||
}
|
||||
|
||||
try {
|
||||
return await renderToHTML(req, res, pathname, query, this.renderOpts)
|
||||
const out = await renderToHTML(req, res, pathname, query, this.renderOpts)
|
||||
return out
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
res.statusCode = 404
|
||||
|
@ -393,16 +387,6 @@ export default class Server {
|
|||
return true
|
||||
}
|
||||
|
||||
isInternalUrl (req) {
|
||||
for (const prefix of internalPrefixes) {
|
||||
if (prefix.test(req.url)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
readBuildId () {
|
||||
const buildIdPath = join(this.dir, this.dist, 'BUILD_ID')
|
||||
const buildId = fs.readFileSync(buildIdPath, 'utf8')
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
import DynamicEntryPlugin from 'webpack/lib/DynamicEntryPlugin'
|
||||
import { EventEmitter } from 'events'
|
||||
import { join } from 'path'
|
||||
import { join, relative } from 'path'
|
||||
import { parse } from 'url'
|
||||
import resolvePath from './resolve'
|
||||
import touch from 'touch'
|
||||
import resolvePath from './resolve'
|
||||
import {createEntry} from './build/webpack/utils'
|
||||
import { MATCH_ROUTE_NAME, IS_BUNDLED_PAGE } from './utils'
|
||||
|
||||
const ADDED = Symbol('added')
|
||||
const BUILDING = Symbol('building')
|
||||
const BUILT = Symbol('built')
|
||||
|
||||
export default function onDemandEntryHandler (devMiddleware, compiler, {
|
||||
export default function onDemandEntryHandler (devMiddleware, compilers, {
|
||||
dir,
|
||||
dev,
|
||||
reload,
|
||||
maxInactiveAge = 1000 * 25,
|
||||
maxInactiveAge = 1000 * 60,
|
||||
pagesBufferLength = 2
|
||||
}) {
|
||||
let entries = {}
|
||||
|
@ -25,9 +26,13 @@ export default function onDemandEntryHandler (devMiddleware, compiler, {
|
|||
let reloading = false
|
||||
let stopped = false
|
||||
let reloadCallbacks = new EventEmitter()
|
||||
// Keep the names of compilers which are building pages at a given moment.
|
||||
const currentBuilders = new Set()
|
||||
|
||||
compilers.forEach(compiler => {
|
||||
compiler.plugin('make', function (compilation, done) {
|
||||
invalidator.startBuilding()
|
||||
currentBuilders.add(compiler.name)
|
||||
|
||||
const allEntries = Object.keys(entries).map((page) => {
|
||||
const { name, entry } = entries[page]
|
||||
|
@ -41,6 +46,10 @@ export default function onDemandEntryHandler (devMiddleware, compiler, {
|
|||
})
|
||||
|
||||
compiler.plugin('done', function (stats) {
|
||||
// Wait until all the compilers mark the build as done.
|
||||
currentBuilders.delete(compiler.name)
|
||||
if (currentBuilders.size !== 0) return
|
||||
|
||||
const { compilation } = stats
|
||||
const hardFailedPages = compilation.errors
|
||||
.filter(e => {
|
||||
|
@ -83,7 +92,7 @@ export default function onDemandEntryHandler (devMiddleware, compiler, {
|
|||
doneCallbacks.emit(page)
|
||||
})
|
||||
|
||||
invalidator.doneBuilding()
|
||||
invalidator.doneBuilding(compiler.name)
|
||||
|
||||
if (hardFailedPages.length > 0 && !reloading) {
|
||||
console.log(`> Reloading webpack due to inconsistant state of pages(s): ${hardFailedPages.join(', ')}`)
|
||||
|
@ -101,6 +110,7 @@ export default function onDemandEntryHandler (devMiddleware, compiler, {
|
|||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const disposeHandler = setInterval(function () {
|
||||
if (stopped) return
|
||||
|
@ -130,9 +140,7 @@ export default function onDemandEntryHandler (devMiddleware, compiler, {
|
|||
|
||||
const pagePath = join(dir, 'pages', page)
|
||||
const pathname = await resolvePath(pagePath)
|
||||
const name = join('bundles', pathname.substring(dir.length))
|
||||
|
||||
const entry = [`${pathname}?entry`]
|
||||
const {name, files} = createEntry(relative(dir, pathname))
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const entryInfo = entries[page]
|
||||
|
@ -144,19 +152,19 @@ export default function onDemandEntryHandler (devMiddleware, compiler, {
|
|||
}
|
||||
|
||||
if (entryInfo.status === BUILDING) {
|
||||
doneCallbacks.on(page, processCallback)
|
||||
doneCallbacks.once(page, handleCallback)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`> Building page: ${page}`)
|
||||
|
||||
entries[page] = { name, entry, pathname, status: ADDED }
|
||||
doneCallbacks.on(page, processCallback)
|
||||
entries[page] = { name, entry: files, pathname, status: ADDED }
|
||||
doneCallbacks.once(page, handleCallback)
|
||||
|
||||
invalidator.invalidate()
|
||||
|
||||
function processCallback (err) {
|
||||
function handleCallback (err) {
|
||||
if (err) return reject(err)
|
||||
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) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dep = DynamicEntryPlugin.createDependency(entry, name)
|
||||
|
@ -272,6 +281,7 @@ function sendJson (res, payload) {
|
|||
class Invalidator {
|
||||
constructor (devMiddleware) {
|
||||
this.devMiddleware = devMiddleware
|
||||
// contains an array of types of compilers currently building
|
||||
this.building = false
|
||||
this.rebuildAgain = false
|
||||
}
|
||||
|
@ -296,6 +306,7 @@ class Invalidator {
|
|||
|
||||
doneBuilding () {
|
||||
this.building = false
|
||||
|
||||
if (this.rebuildAgain) {
|
||||
this.rebuildAgain = false
|
||||
this.invalidate()
|
||||
|
|
|
@ -6,7 +6,6 @@ import generateETag from 'etag'
|
|||
import fresh from 'fresh'
|
||||
import requireModule from './require'
|
||||
import getConfig from './config'
|
||||
import resolvePath from './resolve'
|
||||
import { Router } from '../lib/router'
|
||||
import { loadGetInitialProps } from '../lib/utils'
|
||||
import { getAvailableChunks } from './utils'
|
||||
|
@ -53,9 +52,12 @@ async function doRender (req, res, pathname, query, {
|
|||
|
||||
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([
|
||||
requireModule(join(dir, dist, 'dist', 'pages', page)),
|
||||
requireModule(join(dir, dist, 'dist', 'pages', '_document'))
|
||||
requireModule(pagePath),
|
||||
requireModule(documentPath)
|
||||
])
|
||||
Component = Component.default || Component
|
||||
Document = Document.default || Document
|
||||
|
@ -120,22 +122,6 @@ async function doRender (req, res, pathname, query, {
|
|||
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 }) {
|
||||
// Asks CDNs and others to not to cache the errored page
|
||||
res.setHeader('Cache-Control', 'no-store, must-revalidate')
|
||||
|
|
|
@ -28,12 +28,16 @@ function getPaths (id) {
|
|||
|
||||
if (i.slice(-3) === '.js') 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[i.length - 1] === sep) {
|
||||
return [
|
||||
i + 'index.js',
|
||||
i + 'index.jsx',
|
||||
i + 'index.ts',
|
||||
i + 'index.tsx',
|
||||
i + 'index.json'
|
||||
]
|
||||
}
|
||||
|
@ -43,6 +47,10 @@ function getPaths (id) {
|
|||
join(i, 'index.js'),
|
||||
i + '.jsx',
|
||||
join(i, 'index.jsx'),
|
||||
i + '.tsx',
|
||||
join(i, 'index.tsx'),
|
||||
i + '.ts',
|
||||
join(i, 'index.ts'),
|
||||
i + '.json',
|
||||
join(i, 'index.json')
|
||||
]
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { join } from 'path'
|
||||
import { readdirSync, existsSync } from 'fs'
|
||||
|
||||
export const IS_BUNDLED_PAGE = /^bundles[/\\]pages.*\.(js|jsx)$/
|
||||
export const MATCH_ROUTE_NAME = /^bundles[/\\]pages[/\\](.*)\.(js|jsx)$/
|
||||
export const IS_BUNDLED_PAGE = /^bundles[/\\]pages.*\.js$/
|
||||
export const MATCH_ROUTE_NAME = /^bundles[/\\]pages[/\\](.*)\.js$/
|
||||
|
||||
export function getAvailableChunks (dir, dist) {
|
||||
const chunksDir = join(dir, dist, 'chunks')
|
||||
|
@ -20,3 +20,37 @@ export function getAvailableChunks (dir, dist) {
|
|||
|
||||
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
1
test/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
!node_modules
|
|
@ -11,17 +11,17 @@ export default (context, render) => {
|
|||
return cheerio.load(html)
|
||||
}
|
||||
|
||||
it('should render dynmaic import components', async () => {
|
||||
it('should render dynamic import components', async () => {
|
||||
const $ = await get$('/dynamic/ssr')
|
||||
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')
|
||||
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')
|
||||
expect($('p').text()).toBe('LOADING')
|
||||
})
|
||||
|
|
|
@ -145,26 +145,6 @@ describe('Production Usage', () => {
|
|||
await app.render(req, res, req.url)
|
||||
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))
|
||||
|
|
1
test/node_modules/next
generated
vendored
Symbolic link
1
test/node_modules/next
generated
vendored
Symbolic link
|
@ -0,0 +1 @@
|
|||
../..
|
31
wallaby.js
31
wallaby.js
|
@ -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'
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue