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

Merge branch 'canary' into master

This commit is contained in:
Tim Neutkens 2018-02-19 15:24:52 +01:00 committed by GitHub
commit b75a88790a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
85 changed files with 1422 additions and 414 deletions

View file

@ -21,6 +21,7 @@ const {
__NEXT_DATA__: { __NEXT_DATA__: {
props, props,
err, err,
page,
pathname, pathname,
query, query,
buildId, buildId,
@ -76,7 +77,7 @@ export default async ({ ErrorDebugComponent: passedDebugComponent, stripAnsi: pa
ErrorComponent = await pageLoader.loadPage('/_error') ErrorComponent = await pageLoader.loadPage('/_error')
try { try {
Component = await pageLoader.loadPage(pathname) Component = await pageLoader.loadPage(page)
} catch (err) { } catch (err) {
console.error(stripAnsi(`${err.message}\n${err.stack}`)) console.error(stripAnsi(`${err.message}\n${err.stack}`))
Component = ErrorComponent Component = ErrorComponent

View file

@ -1,15 +0,0 @@
# 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

@ -5,9 +5,9 @@ import React, { Children } from 'react'
const ActiveLink = ({ router, children, ...props }) => { const ActiveLink = ({ router, children, ...props }) => {
const child = Children.only(children) const child = Children.only(children)
let className = child.props.className || '' let className = child.props.className || null
if (router.pathname === props.href && props.activeClassName) { if (router.pathname === props.href && props.activeClassName) {
className = `${className} ${props.activeClassName}`.trim() className = `${className !== null ? className : ''} ${props.activeClassName}`.trim()
} }
delete props.activeClassName delete props.activeClassName

View file

@ -0,0 +1,14 @@
{
"name": "custom-server",
"version": "1.0.0",
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
},
"dependencies": {
"next": "latest",
"react": "^16.0.0",
"react-dom": "^16.0.0"
}
}

View file

@ -0,0 +1,34 @@
[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/custom-server)
# Custom server example
## How to use
### Download manually
Download the example [or clone the repo](https://github.com/zeit/next.js):
```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/custom-charset
cd custom-charset
```
Install it and run:
```bash
npm install
npm run dev
```
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))
```bash
now
```
## The idea behind the example
The HTTP/1.1 specification says - if charset is not set in the http header then the browser defaults use ISO-8859-1.
For languages like Polish, Albanian, Hungarian, Czech, Slovak, Slovene, there will be broken characters encoding from SSR.
You can overwrite Content-Type in getInitialProps. But if you want to handle it as a server side concern, you can use this as an simple example.

View file

@ -0,0 +1,3 @@
import React from 'react'
export default () => <div>áéíóöúü</div>

View file

@ -0,0 +1,21 @@
const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')
const port = parseInt(process.env.PORT, 10) || 3000
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
app.prepare()
.then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url, true)
res.setHeader('Content-Type', 'text/html; charset=iso-8859-2')
handle(req, res, parsedUrl)
})
.listen(port, (err) => {
if (err) throw err
console.log(`> Ready on http://localhost:${port}`)
})
})

View file

@ -45,26 +45,32 @@ function getCacheKey (req) {
return `${req.url}` return `${req.url}`
} }
function renderAndCache (req, res, pagePath, queryParams) { async function renderAndCache (req, res, pagePath, queryParams) {
const key = getCacheKey(req) const key = getCacheKey(req)
// If we have a page in the cache, let's serve it // If we have a page in the cache, let's serve it
if (ssrCache.has(key)) { if (ssrCache.has(key)) {
console.log(`CACHE HIT: ${key}`) res.setHeader('x-cache', 'HIT')
res.send(ssrCache.get(key)) res.send(ssrCache.get(key))
return return
} }
// If not let's render the page into HTML try {
app.renderToHTML(req, res, pagePath, queryParams) // If not let's render the page into HTML
.then((html) => { const html = await app.renderToHTML(req, res, pagePath, queryParams)
// Let's cache this page
console.log(`CACHE MISS: ${key}`)
ssrCache.set(key, html)
// Something is wrong with the request, let's skip the cache
if (res.statusCode !== 200) {
res.send(html) res.send(html)
}) return
.catch((err) => { }
app.renderError(err, req, res, pagePath, queryParams)
}) // Let's cache this page
ssrCache.set(key, html)
res.setHeader('x-cache', 'MISS')
res.send(html)
} catch (err) {
app.renderError(err, req, res, pagePath, queryParams)
}
} }

View file

@ -8,7 +8,7 @@
}, },
"dependencies": { "dependencies": {
"next": "latest", "next": "latest",
"react": "latest, "react": "latest",
"react-dom": "latest" "react-dom": "latest"
}, },
"devDependencies": { "devDependencies": {

View file

@ -25,7 +25,7 @@ class Index extends React.Component {
// Force a reload of all the current queries now that the user is // Force a reload of all the current queries now that the user is
// logged in, so we don't accidentally leave any state around. // logged in, so we don't accidentally leave any state around.
this.props.client.resetStore().then(() => { this.props.client.cache.reset().then(() => {
// Redirect to a more useful page when signed out // Redirect to a more useful page when signed out
redirect({}, '/signin') redirect({}, '/signin')
}) })

View file

@ -18,7 +18,7 @@ const test = require('ava')
* Apollo that we can clear, etc * Apollo that we can clear, etc
*/ */
const apolloFilePath = require.resolve('../lib/init-apollo') const apolloFilePath = require.resolve('../lib/initApollo')
test.beforeEach(() => { test.beforeEach(() => {
// Clean up the cache // Clean up the cache

View file

@ -47,5 +47,3 @@ In this simple example, we integrate Apollo seamlessly with Next by wrapping our
On initial page load, while on the server and inside `getInitialProps`, we invoke the Apollo method, [`getDataFromTree`](http://dev.apollodata.com/react/server-side-rendering.html#getDataFromTree). This method returns a promise; at the point in which the promise resolves, our Apollo Client store is completely initialized. On initial page load, while on the server and inside `getInitialProps`, we invoke the Apollo method, [`getDataFromTree`](http://dev.apollodata.com/react/server-side-rendering.html#getDataFromTree). This method returns a promise; at the point in which the promise resolves, our Apollo Client store is completely initialized.
This example relies on [graph.cool](https://www.graph.cool) for its GraphQL backend. This example relies on [graph.cool](https://www.graph.cool) for its GraphQL backend.
*Note: Apollo uses Redux internally; if you're interested in integrating the client with your existing Redux store check out the [`with-apollo-and-redux`](https://github.com/zeit/next.js/tree/master/examples/with-apollo-and-redux) example.*

View file

@ -0,0 +1,41 @@
[![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-draft-js)
# DraftJS Medium editor inspiration
## How to use
### Using `create-next-app`
Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example:
```
npm i -g create-next-app
create-next-app --example with-draft-js
```
### Download manually
Download the example [or clone the repo](https://github.com/zeit/next.js):
```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-draft-js
cd with-draft-js
```
Install it and run:
```bash
npm install
npm run dev
```
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)):
```bash
now
```
## The idea behind the example
Have you ever wanted to have an editor like medium.com in your Next.js app? DraftJS is avalaible for SSR, but some plugins like the toolbar are using `window`, which does not work when doing SSR.
This example aims to provides a fully customizable example of the famous medium editor with DraftJS. The goal was to get it as customizable as possible, and fully working with Next.js without using the react-no-ssr package.

View file

@ -0,0 +1,17 @@
{
"name": "with-draft-js",
"version": "1.0.0",
"author": "Asten Mies",
"license": "ISC",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"draft-js": "0.10.5",
"next": "5.0.0",
"react": "16.2.0",
"react-dom": "16.2.0"
}
}

View file

@ -0,0 +1,270 @@
import React from 'react'
import {
Editor,
EditorState,
RichUtils,
convertToRaw,
convertFromRaw
} from 'draft-js'
export default class App extends React.Component {
constructor (props) {
super(props)
this.state = {
editorState: EditorState.createWithContent(convertFromRaw(initialData)),
showToolbar: false,
windowWidth: 0,
toolbarMeasures: {
w: 0,
h: 0
},
selectionMeasures: {
w: 0,
h: 0
},
selectionCoordinates: {
x: 0,
y: 0
},
toolbarCoordinates: {
x: 0,
y: 0
},
showRawData: false
}
this.focus = () => this.editor.focus()
this.onChange = (editorState) => this.setState({editorState})
}
onClickEditor = () => {
this.focus()
this.checkSelectedText()
}
// 1- Check if some text is selected
checkSelectedText = () => {
if (typeof window !== 'undefined') {
const text = window.getSelection().toString()
if (text !== '') {
// 1-a Define the selection coordinates
this.setSelectionXY()
} else {
// Hide the toolbar if nothing is selected
this.setState({
showToolbar: false
})
}
}
}
// 2- Identify the selection coordinates
setSelectionXY = () => {
var r = window.getSelection().getRangeAt(0).getBoundingClientRect()
var relative = document.body.parentNode.getBoundingClientRect()
// 2-a Set the selection coordinates in the state
this.setState({
selectionCoordinates: r,
windowWidth: relative.width,
selectionMeasures: {
w: r.width,
h: r.height
}
}, () => this.showToolbar())
}
// 3- Show the toolbar
showToolbar = () => {
this.setState({
showToolbar: true
}, () => this.measureToolbar())
}
// 4- The toolbar was hidden until now
measureToolbar = () => {
// 4-a Define the toolbar width and height, as it is now visible
this.setState({
toolbarMeasures: {
w: this.elemWidth,
h: this.elemHeight
}
}, () => this.setToolbarXY())
}
// 5- Now that we have all measures, define toolbar coordinates
setToolbarXY = () => {
let coordinates = {}
const { selectionMeasures, selectionCoordinates, toolbarMeasures, windowWidth } = this.state
const hiddenTop = selectionCoordinates.y < toolbarMeasures.h
const hiddenRight = windowWidth - selectionCoordinates.x < toolbarMeasures.w / 2
const hiddenLeft = selectionCoordinates.x < toolbarMeasures.w / 2
const normalX = selectionCoordinates.x - (toolbarMeasures.w / 2) + (selectionMeasures.w / 2)
const normalY = selectionCoordinates.y - toolbarMeasures.h
const invertedY = selectionCoordinates.y + selectionMeasures.h
const moveXToLeft = windowWidth - toolbarMeasures.w
const moveXToRight = 0
coordinates = {
x: normalX,
y: normalY
}
if (hiddenTop) {
coordinates.y = invertedY
}
if (hiddenRight) {
coordinates.x = moveXToLeft
}
if (hiddenLeft) {
coordinates.x = moveXToRight
}
this.setState({
toolbarCoordinates: coordinates
})
}
handleKeyCommand = (command) => {
const {editorState} = this.state
const newState = RichUtils.handleKeyCommand(editorState, command)
if (newState) {
this.onChange(newState)
return true
}
return false
}
toggleToolbar = (inlineStyle) => {
this.onChange(
RichUtils.toggleInlineStyle(
this.state.editorState,
inlineStyle
)
)
}
render () {
const {editorState} = this.state
// Make sure we're not on the ssr
if (typeof window !== 'undefined') {
// Let's stick the toolbar to the selection
// when the window is resized
window.addEventListener('resize', this.checkSelectedText)
}
const toolbarStyle = {
display: this.state.showToolbar ? 'block' : 'none',
backgroundColor: 'black',
color: 'white',
position: 'absolute',
left: this.state.toolbarCoordinates.x,
top: this.state.toolbarCoordinates.y,
zIndex: 999,
padding: 10
}
return (
<div>
<div
ref={(elem) => {
this.elemWidth = elem ? elem.clientWidth : 0
this.elemHeight = elem ? elem.clientHeight : 0
}}
style={toolbarStyle}
>
<ToolBar
editorState={editorState}
onToggle={this.toggleToolbar}
/>
</div>
<div onClick={this.onClickEditor} onBlur={this.checkSelectedText}>
<Editor
customStyleMap={styleMap}
editorState={editorState}
handleKeyCommand={this.handleKeyCommand}
onChange={this.onChange}
placeholder='Tell a story...'
spellCheck={false}
ref={(element) => { this.editor = element }}
/>
</div>
<div style={{ marginTop: 40 }}>
<button onClick={() => this.setState({showRawData: !this.state.showRawData})}>
{!this.state.showRawData ? 'Show' : 'Hide'} Raw Data
</button><br />
{this.state.showRawData && JSON.stringify(convertToRaw(editorState.getCurrentContent()))}
</div>
</div>
)
}
}
// Custom overrides for each style
const styleMap = {
CODE: {
backgroundColor: 'rgba(0, 0, 0, 0.05)',
fontFamily: '"Inconsolata", "Menlo", "Consolas", monospace',
fontSize: 16,
padding: 4
},
BOLD: {
color: '#395296',
fontWeight: 'bold'
},
ANYCUSTOMSTYLE: {
color: '#00e400'
}
}
class ToolbarButton extends React.Component {
constructor () {
super()
this.onToggle = (e) => {
e.preventDefault()
this.props.onToggle(this.props.style)
}
}
render () {
const buttonStyle = {
padding: 10
}
return (
<span onMouseDown={this.onToggle} style={buttonStyle}>
{ this.props.label }
</span>
)
}
}
var toolbarItems = [
{label: 'Bold', style: 'BOLD'},
{label: 'Italic', style: 'ITALIC'},
{label: 'Underline', style: 'UNDERLINE'},
{label: 'Code', style: 'CODE'},
{label: 'Surprise', style: 'ANYCUSTOMSTYLE'}
]
const ToolBar = (props) => {
var currentStyle = props.editorState.getCurrentInlineStyle()
return (
<div>
{toolbarItems.map(toolbarItem =>
<ToolbarButton
key={toolbarItem.label}
active={currentStyle.has(toolbarItem.style)}
label={toolbarItem.label}
onToggle={props.onToggle}
style={toolbarItem.style}
/>
)}
</div>
)
}
const initialData = {'blocks': [{'key': '16d0k', 'text': 'You can edit this text.', 'type': 'unstyled', 'depth': 0, 'inlineStyleRanges': [{'offset': 0, 'length': 23, 'style': 'BOLD'}], 'entityRanges': [], 'data': {}}, {'key': '98peq', 'text': '', 'type': 'unstyled', 'depth': 0, 'inlineStyleRanges': [], 'entityRanges': [], 'data': {}}, {'key': 'ecmnc', 'text': 'Luke Skywalker has vanished. In his absence, the sinister FIRST ORDER has risen from the ashes of the Empire and will not rest until Skywalker, the last Jedi, has been destroyed.', 'type': 'unstyled', 'depth': 0, 'inlineStyleRanges': [{'offset': 0, 'length': 14, 'style': 'BOLD'}, {'offset': 133, 'length': 9, 'style': 'BOLD'}], 'entityRanges': [], 'data': {}}, {'key': 'fe2gn', 'text': '', 'type': 'unstyled', 'depth': 0, 'inlineStyleRanges': [], 'entityRanges': [], 'data': {}}, {'key': '4481k', 'text': 'With the support of the REPUBLIC, General Leia Organa leads a brave RESISTANCE. She is desperate to find her brother Luke and gain his help in restoring peace and justice to the galaxy.', 'type': 'unstyled', 'depth': 0, 'inlineStyleRanges': [{'offset': 34, 'length': 19, 'style': 'BOLD'}, {'offset': 117, 'length': 4, 'style': 'BOLD'}, {'offset': 68, 'length': 10, 'style': 'ANYCUSTOMSTYLE'}], 'entityRanges': [], 'data': {}}], 'entityMap': {}}

View file

@ -23,10 +23,11 @@ cd with-firebase-authentication
``` ```
Set up firebase: Set up firebase:
- create a project - Create a project at the [Firebase console](https://console.firebase.google.com/).
- get your service account credentials and client credentials and set both in firebaseCredentials.js - Get your account credentials from the Firebase console at *settings>service accounts*, where you can click on *generate new private key* and download the credentials as a json file. It will contain keys such as `project_id`, `client_email` and `client id`. Now copy them into your project in the `credentials/server.js` file.
- set your firebase database url in server.js - Get your authentication credentials from the Firebase console under *authentication>users>web setup*. It will include keys like `apiKey`, `authDomain` and `databaseUrl` and it goes into your project in `credentials/client.js`.
- on the firebase Authentication console, select Google as your provider - Copy the `databaseUrl` key you got in the last step into `server.js` in the corresponding line.
- Back at the Firebase web console, go to *authentication>signup method* and select *Google*.
Install it and run: Install it and run:

View file

@ -10,8 +10,8 @@
"body-parser": "^1.17.1", "body-parser": "^1.17.1",
"express": "^4.14.0", "express": "^4.14.0",
"express-session": "^1.15.2", "express-session": "^1.15.2",
"firebase": "^3.7.5", "firebase": "^4.9.1",
"firebase-admin": "^4.2.0", "firebase-admin": "^5.8.2",
"isomorphic-unfetch": "2.0.0", "isomorphic-unfetch": "2.0.0",
"next": "latest", "next": "latest",
"react": "^16.0.0", "react": "^16.0.0",

View file

@ -4,7 +4,7 @@ import { renderStatic } from 'glamor/server'
export default class MyDocument extends Document { export default class MyDocument extends Document {
static async getInitialProps ({ renderPage }) { static async getInitialProps ({ renderPage }) {
const page = renderPage() const page = renderPage()
const styles = renderStatic(() => page.html) const styles = renderStatic(() => page.html || page.errorHtml)
return { ...page, ...styles } return { ...page, ...styles }
} }

View file

@ -4,7 +4,7 @@ import { renderStatic } from 'glamor/server'
export default class MyDocument extends Document { export default class MyDocument extends Document {
static async getInitialProps ({ renderPage }) { static async getInitialProps ({ renderPage }) {
const page = renderPage() const page = renderPage()
const styles = renderStatic(() => page.html) const styles = renderStatic(() => page.html || page.errorHtml)
return { ...page, ...styles } return { ...page, ...styles }
} }

View file

@ -22,8 +22,5 @@
"react": "^16.0.0", "react": "^16.0.0",
"react-dom": "^16.0.0", "react-dom": "^16.0.0",
"sass-loader": "^6.0.6" "sass-loader": "^6.0.6"
},
"devDependencies": {
"now": "^8.3.10"
} }
} }

View file

@ -14,7 +14,7 @@ const Store = types
// mobx-state-tree doesn't allow anonymous callbacks changing data // mobx-state-tree doesn't allow anonymous callbacks changing data
// pass off to another action instead // pass off to another action instead
self.update() self.update()
}) }, 1000)
} }
function update () { function update () {

View file

@ -14,7 +14,7 @@ class Store {
this.timer = setInterval(() => { this.timer = setInterval(() => {
this.lastUpdate = Date.now() this.lastUpdate = Date.now()
this.light = true this.light = true
}) }, 1000)
} }
stop = () => clearInterval(this.timer) stop = () => clearInterval(this.timer)

View file

@ -0,0 +1,40 @@
[![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-next-css)
# next-css example
## How to use
### Using `create-next-app`
Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example:
```
npm i -g create-next-app
create-next-app --example with-next-css with-next-css-app
```
### Download manually
Download the example [or clone the repo](https://github.com/zeit/next.js):
```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-next-css
cd with-next-css
```
Install it and run:
```bash
npm install
npm run dev
```
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))
```bash
now
```
## The idea behind the example
This example demonstrates how to use the [next-css plugin](https://github.com/zeit/next-plugins/tree/master/packages/next-css) It includes patterns for with and without CSS Modules, with PostCSS and with additional webpack configurations on top of the next-css plugin.

View file

@ -0,0 +1,16 @@
const withCSS = require('@zeit/next-css')
/* Without CSS Modules, with PostCSS */
module.exports = withCSS()
/* With CSS Modules */
// module.exports = withCSS({ cssModules: true })
/* With additional configuration on top of CSS Modules */
/*
module.exports = withCSS({
cssModules: true,
webpack: function (config) {
return config;
}
});
*/

View file

@ -0,0 +1,16 @@
{
"name": "with-css-modules",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@zeit/next-css": "0.0.7",
"next": "5.0.0",
"react": "16.2.0",
"react-dom": "16.2.0"
}
}

View file

@ -0,0 +1,23 @@
/*
In production the stylesheet is compiled to .next/static/style.css and served from /_next/static/style.css
You have to include it into the page using either next/head or a custom _document.js, as is being done in this file.
*/
import Document, { Head, Main, NextScript } from 'next/document'
export default class MyDocument extends Document {
render () {
return (
<html>
<Head>
<link rel='stylesheet' href='/_next/static/style.css' />
</Head>
<body>
<Main />
<NextScript />
</body>
</html>
)
}
}

View file

@ -0,0 +1,13 @@
/* Without CSS Modules, maybe with PostCSS */
import '../style.css'
export default () => <div className='example'>O Hai world!</div>
/* With CSS Modules */
/*
import css from "../style.css"
export default () => <div className={css.example}>Hello World, I am being styled using CSS Modules!</div>
*/

View file

@ -0,0 +1,16 @@
.example {
font-size: 50px;
color: papayawhip;
}
/* Post-CSS */
/*
:root {
--some-color: red;
}
.example {
color: var(--some-color);
}
*/

View file

@ -0,0 +1,2 @@
const withSass = require('@zeit/next-sass')
module.exports = withSass()

View file

@ -0,0 +1,14 @@
{
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@zeit/next-sass": "0.0.9",
"next": "^5.0.0",
"node-sass": "^4.7.2",
"react": "^16.2.0",
"react-dom": "^16.2.0"
}
}

View file

@ -0,0 +1,26 @@
/*
In production the stylesheet is compiled to .next/static/style.css.
The file will be served from /_next/static/style.css
You could include it into the page using either next/head or a custom _document.js.
*/
import Document, { Head, Main, NextScript } from 'next/document'
export default class MyDocument extends Document {
render () {
return (
<html>
<Head>
<link
rel='stylesheet'
href='/_next/static/style.css'
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</html>
)
}
}

View file

@ -0,0 +1,6 @@
import '../styles/style.scss'
export default () =>
<div className='example'>
Hello World!
</div>

View file

@ -0,0 +1,43 @@
[![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-jest)
# Example app with next-sass
## How to use
### Using `create-next-app`
Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example:
```bash
npm i -g create-next-app
create-next-app --example with-next-sass with-next-sass-app
```
### Download manually
Download the example [or clone the repo](https://github.com/zeit/next.js):
```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-next-sass
cd with-next-sass
```
Install it and run:
```bash
npm install
npm run build
npm run start
```
The dev mode is also support via `npm run dev`
## The idea behind the example
This example features:
* An app with next-sass
This example uses next-sass without css-modules. The config can be found in `next.config.js`, change `withSass()` to `withSass({cssModules: true})` if you use css-modules. Then in the code, you import the stylesheet as `import style '../styles/style.scss'` and use it like `<div className={style.example}>`.
[Learn more](https://github.com/zeit/next-plugins/tree/master/packages/next-sass)

View file

@ -0,0 +1,4 @@
$color: #2ecc71;
.example {
background-color: $color;
}

View file

@ -3,7 +3,11 @@ module.exports = {
const originalEntry = cfg.entry const originalEntry = cfg.entry
cfg.entry = async () => { cfg.entry = async () => {
const entries = await originalEntry() const entries = await originalEntry()
entries['main.js'].unshift('./client/polyfills.js')
if (entries['main.js']) {
entries['main.js'].unshift('./client/polyfills.js')
}
return entries return entries
} }

View file

@ -11,15 +11,15 @@
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"express": "4.15.3", "express": "4.16.2",
"i18next": "8.4.2", "i18next": "10.4.1",
"i18next-browser-languagedetector": "2.0.0", "i18next-browser-languagedetector": "2.1.0",
"i18next-express-middleware": "1.0.5", "i18next-express-middleware": "1.0.10",
"i18next-node-fs-backend": "1.0.0", "i18next-node-fs-backend": "1.0.0",
"i18next-xhr-backend": "1.4.2", "i18next-xhr-backend": "1.5.1",
"next": "latest", "next": "5.0.0",
"react": "16.2.0", "react": "16.2.0",
"react-dom": "16.2.0", "react-dom": "16.2.0",
"react-i18next": "4.6.3" "react-i18next": "7.3.6"
} }
} }

View file

@ -2,7 +2,6 @@ const express = require('express')
const path = require('path') const path = require('path')
const next = require('next') const next = require('next')
const port = parseInt(process.env.PORT, 10) || 3000
const dev = process.env.NODE_ENV !== 'production' const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev }) const app = next({ dev })
const handle = app.getRequestHandler() const handle = app.getRequestHandler()
@ -17,6 +16,7 @@ i18n
.use(Backend) .use(Backend)
.use(i18nextMiddleware.LanguageDetector) .use(i18nextMiddleware.LanguageDetector)
.init({ .init({
fallbackLng: 'en',
preload: ['en', 'de'], // preload all langages preload: ['en', 'de'], // preload all langages
ns: ['common', 'home', 'page2'], // need to preload all the namespaces ns: ['common', 'home', 'page2'], // need to preload all the namespaces
backend: { backend: {
@ -41,9 +41,9 @@ i18n
// use next.js // use next.js
server.get('*', (req, res) => handle(req, res)) server.get('*', (req, res) => handle(req, res))
server.listen(port, (err) => { server.listen(3000, (err) => {
if (err) throw err if (err) throw err
console.log(`> Ready on http://localhost:${port}`) console.log('> Ready on http://localhost:3000')
}) })
}) })
}) })

View file

@ -0,0 +1,4 @@
{
"presets": ["next/babel"],
"plugins": ["react-native-web"]
}

View file

@ -0,0 +1,42 @@
[![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-react-native-web)
## How to use
### Using `create-next-app`
Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example:
```
npm i -g create-next-app
create-next-app --example with-react-native-web
```
### Download manually
Download the example [or clone the repo](https://github.com/zeit/next.js):
```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-react-native-web
cd with-react-native-web
```
Install it and run:
```bash
npm install
npm run dev
```
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))
```bash
now
```
## The idea behind the example
This example features how to use [react-native-web](https://github.com/necolas/react-native-web) to bring the platform-agnostic Components and APIs of React Native to the web.
> **High-quality user interfaces**: React Native for Web makes it easy to create fast, adaptive web UIs in JavaScript. It provides native-like interactions, support for multiple input modes (touch, mouse, keyboard), optimized vendor-prefixed styles, built-in support for RTL layout, built-in accessibility, and integrates with React Dev Tools.
>
> **Write once, render anywhere**: React Native for Web interoperates with existing React DOM components and is compatible with the majority of the React Native API. You can develop new components for native and web without rewriting existing code. React Native for Web can also render to HTML and critical CSS on the server using Node.js.

View file

@ -0,0 +1,17 @@
{
"name": "with-react-native-web",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "latest",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-native-web": "^0.4.0"
},
"devDependencies": {
"babel-plugin-react-native-web": "^0.4.0"
}
}

View file

@ -0,0 +1,45 @@
import Document, { Head, Main, NextScript } from 'next/document'
import React from 'react'
import { AppRegistry } from 'react-native-web'
let index = 0
// Force Next-generated DOM elements to fill their parent's height.
// Not required for using of react-native-web, but helps normalize
// layout for top-level wrapping elements.
const normalizeNextElements = `
body > div:first-child,
#__next {
height: 100%;
}
`
export default class MyDocument extends Document {
static async getInitialProps ({ renderPage }) {
AppRegistry.registerComponent('Main', () => Main)
const { getStyleElement } = AppRegistry.getApplication('Main')
const page = renderPage()
const styles = [
<style
key={index++}
dangerouslySetInnerHTML={{ __html: normalizeNextElements }}
/>,
getStyleElement()
]
return { ...page, styles }
}
render () {
return (
<html style={{ height: '100%', width: '100%' }}>
<Head>
<title>react-native-web</title>
</Head>
<body style={{ height: '100%', width: '100%', overflowY: 'scroll' }}>
<Main />
<NextScript />
</body>
</html>
)
}
}

View file

@ -0,0 +1,20 @@
import React from 'react'
import { StyleSheet, Text, View } from 'react-native'
const styles = StyleSheet.create({
container: {
alignItems: 'center',
height: '100%',
justifyContent: 'center'
},
text: {
alignItems: 'center',
fontSize: 24
}
})
export default props => (
<View style={styles.container}>
<Text style={styles.text}>Welcome to Next.js!</Text>
</View>
)

View file

@ -40,7 +40,7 @@ Run BuckleScript build system `bsb -w` and `next -w` separately. For the sake
of simple convention, `npm run dev` run both `bsb` and `next` concurrently. of simple convention, `npm run dev` run both `bsb` and `next` concurrently.
However, this doesn't offer the full [colorful and very, very, veeeery nice However, this doesn't offer the full [colorful and very, very, veeeery nice
error error
output](https://reasonml.github.io/community/blog/#way-way-waaaay-nicer-error-messages) output](https://reasonml.github.io/blog/2017/08/25/way-nicer-error-messages.html)
experience that ReasonML can offer, don't miss it! experience that ReasonML can offer, don't miss it!
## The idea behind the example ## The idea behind the example

View file

@ -1,6 +1,6 @@
import { interval } from 'rxjs/observable/interval' import { interval } from 'rxjs/observable/interval'
import { of } from 'rxjs/observable/of' import { of } from 'rxjs/observable/of'
import { takeUntil, mergeMap, catchError } from 'rxjs/operators' import { takeUntil, mergeMap, catchError, map } from 'rxjs/operators'
import { combineEpics, ofType } from 'redux-observable' import { combineEpics, ofType } from 'redux-observable'
import ajax from 'universal-rx-request' // because standard AjaxObservable only works in browser import ajax from 'universal-rx-request' // because standard AjaxObservable only works in browser
@ -13,11 +13,9 @@ export const fetchUserEpic = (action$, store) =>
mergeMap(action => { mergeMap(action => {
return interval(3000).pipe( return interval(3000).pipe(
mergeMap(x => mergeMap(x =>
of( actions.fetchCharacter({
actions.fetchCharacter({ isServer: store.getState().isServer
isServer: store.getState().isServer })
})
)
), ),
takeUntil(action$.ofType(types.STOP_FETCHING_CHARACTERS)) takeUntil(action$.ofType(types.STOP_FETCHING_CHARACTERS))
) )
@ -31,12 +29,10 @@ export const fetchCharacterEpic = (action$, store) =>
ajax({ ajax({
url: `https://swapi.co/api/people/${store.getState().nextCharacterId}` url: `https://swapi.co/api/people/${store.getState().nextCharacterId}`
}).pipe( }).pipe(
mergeMap(response => map(response =>
of( actions.fetchCharacterSuccess(
actions.fetchCharacterSuccess( response.body,
response.body, store.getState().isServer
store.getState().isServer
)
) )
), ),
catchError(error => catchError(error =>

View file

@ -61,7 +61,7 @@ The second example, under `components/add-count.js`, shows a simple add counter
## What changed with next-redux-saga ## What changed with next-redux-saga
The digital clock is updated every 800ms using the `runClockSaga` found in `saga.js`. The digital clock is updated every second using the `runClockSaga` found in `saga.js`.
All pages are also being wrapped by `next-redux-saga` using a helper function from `store.js`: All pages are also being wrapped by `next-redux-saga` using a helper function from `store.js`:

View file

@ -1,11 +1,12 @@
import React from 'react' import React from 'react'
import {increment, loadData, startClock} from '../actions' import {increment, loadData, startClock, tickClock} from '../actions'
import {withReduxSaga} from '../store' import {withReduxSaga} from '../store'
import Page from '../components/page' import Page from '../components/page'
class Counter extends React.Component { class Counter extends React.Component {
static async getInitialProps ({store}) { static async getInitialProps ({store, isServer}) {
store.dispatch(tickClock(isServer))
store.dispatch(increment()) store.dispatch(increment())
if (!store.getState().placeholderData) { if (!store.getState().placeholderData) {
store.dispatch(loadData()) store.dispatch(loadData())

View file

@ -1,11 +1,12 @@
import React from 'react' import React from 'react'
import {increment, startClock} from '../actions' import {increment, startClock, tickClock} from '../actions'
import {withReduxSaga} from '../store' import {withReduxSaga} from '../store'
import Page from '../components/page' import Page from '../components/page'
class Counter extends React.Component { class Counter extends React.Component {
static async getInitialProps ({store}) { static async getInitialProps ({store, isServer}) {
store.dispatch(tickClock(isServer))
store.dispatch(increment()) store.dispatch(increment())
} }

View file

@ -13,7 +13,7 @@ function * runClockSaga () {
yield take(actionTypes.START_CLOCK) yield take(actionTypes.START_CLOCK)
while (true) { while (true) {
yield put(tickClock(false)) yield put(tickClock(false))
yield call(delay, 800) yield call(delay, 1000)
} }
} }

View file

@ -32,7 +32,7 @@ export const serverRenderClock = (isServer) => dispatch => {
} }
export const startClock = () => dispatch => { export const startClock = () => dispatch => {
return setInterval(() => dispatch({ type: actionTypes.TICK, light: true, ts: Date.now() }), 800) return setInterval(() => dispatch({ type: actionTypes.TICK, light: true, ts: Date.now() }), 1000)
} }
export const addCount = () => dispatch => { export const addCount = () => dispatch => {

View file

@ -1 +0,0 @@
*.js

View file

@ -31,4 +31,3 @@ npm install
npm run dev npm run dev
``` ```
Output JS files are aside the related TypeScript ones.

View file

@ -0,0 +1,2 @@
const withTypescript = require('@zeit/next-typescript')
module.exports = withTypescript()

View file

@ -1,22 +1,20 @@
{ {
"name": "with-typescript", "name": "with-typescript-plugin",
"version": "1.0.0", "version": "1.0.0",
"scripts": { "scripts": {
"dev": "concurrently \"tsc --pretty --watch\" \"next\"", "dev": "next",
"prebuild": "tsc",
"build": "next build", "build": "next build",
"start": "next start" "start": "next start"
}, },
"dependencies": { "dependencies": {
"next": "latest", "next": "^5.0.0",
"react": "^16.1.0", "react": "^16.2.0",
"react-dom": "^16.1.0" "react-dom": "^16.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/next": "^2.4.5", "@types/next": "^2.4.7",
"@types/react": "^16.0.22", "@types/react": "^16.0.36",
"concurrently": "^3.5.0", "@zeit/next-typescript": "0.0.8",
"tslint": "^5.8.0", "typescript": "^2.7.1"
"typescript": "^2.6.1"
} }
} }

View file

@ -1,8 +1,26 @@
{ {
"compileOnSave": false,
"compilerOptions": { "compilerOptions": {
"jsx": "react-native", "target": "esnext",
"module": "commonjs", "module": "esnext",
"strict": true, "jsx": "preserve",
"target": "es2017" "allowJs": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"removeComments": false,
"preserveConstEnums": true,
"sourceMap": true,
"skipLibCheck": true,
"baseUrl": ".",
"typeRoots": [
"./node_modules/@types"
],
"lib": [
"dom",
"es2015",
"es2016"
]
} }
} }

View file

@ -1,10 +0,0 @@
{
"defaultSeverity": "error",
"extends": ["tslint:recommended"],
"jsRules": {},
"rules": {
"quotemark": [true, "single", "jsx-double"],
"semicolon": [true, "never"]
},
"rulesDirectory": []
}

View file

@ -2,11 +2,11 @@ const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const { ANALYZE } = process.env const { ANALYZE } = process.env
module.exports = { module.exports = {
webpack: function (config) { webpack: function (config, { isServer }) {
if (ANALYZE) { if (ANALYZE) {
config.plugins.push(new BundleAnalyzerPlugin({ config.plugins.push(new BundleAnalyzerPlugin({
analyzerMode: 'server', analyzerMode: 'server',
analyzerPort: 8888, analyzerPort: isServer ? 8888 : 8889,
openAnalyzer: true openAnalyzer: true
})) }))
} }

View file

@ -6,7 +6,7 @@
"start": "next start -p 5000" "start": "next start -p 5000"
}, },
"dependencies": { "dependencies": {
"next": "zones", "next": "latest",
"react": "^16.0.0", "react": "^16.0.0",
"react-dom": "^16.0.0" "react-dom": "^16.0.0"
}, },

View file

@ -11,7 +11,7 @@ export default function withRouter (ComposedComponent) {
router: PropTypes.object router: PropTypes.object
} }
static displayName = `withRoute(${displayName})` static displayName = `withRouter(${displayName})`
render () { render () {
const props = { const props = {

View file

@ -1,6 +1,6 @@
{ {
"name": "next", "name": "next",
"version": "5.0.0", "version": "5.0.1-canary.6",
"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",
@ -90,12 +90,12 @@
"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": "4.0.0-beta.18", "react-hot-loader": "4.0.0-beta.23",
"recursive-copy": "2.0.6", "recursive-copy": "2.0.6",
"resolve": "1.5.0", "resolve": "1.5.0",
"send": "0.16.1", "send": "0.16.1",
"strip-ansi": "3.0.1", "strip-ansi": "3.0.1",
"styled-jsx": "2.2.3", "styled-jsx": "2.2.5",
"touch": "3.1.0", "touch": "3.1.0",
"uglifyjs-webpack-plugin": "1.1.6", "uglifyjs-webpack-plugin": "1.1.6",
"unfetch": "3.0.0", "unfetch": "3.0.0",
@ -105,6 +105,7 @@
"webpack": "3.10.0", "webpack": "3.10.0",
"webpack-dev-middleware": "1.12.0", "webpack-dev-middleware": "1.12.0",
"webpack-hot-middleware": "2.21.0", "webpack-hot-middleware": "2.21.0",
"webpack-sources": "1.1.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"
}, },

View file

@ -169,9 +169,9 @@ To use more sophisticated CSS-in-JS solutions, you typically have to implement s
To support importing `.css` `.scss` or `.less` files you can use these modules, which configure sensible defaults for server rendered applications. 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-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-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-less](https://github.com/zeit/next-plugins/tree/master/packages/next-less)
### Static file serving (e.g.: images) ### Static file serving (e.g.: images)
@ -1037,6 +1037,17 @@ module.exports = {
This is development-only feature. If you want to cache SSR pages in production, please see [SSR-caching](https://github.com/zeit/next.js/tree/canary/examples/ssr-caching) example. This is development-only feature. If you want to cache SSR pages in production, please see [SSR-caching](https://github.com/zeit/next.js/tree/canary/examples/ssr-caching) example.
#### Configuring extensions looked for when resolving pages in `pages`
Aimed at modules like [`@zeit/next-typescript`](https://github.com/zeit/next-plugins/tree/master/packages/next-typescript), that add support for pages ending in `.ts`. `pageExtensions` allows you to configure the extensions looked for in the `pages` directory when resolving pages.
```js
// next.config.js
module.exports = {
pageExtensions: ['jsx', 'js']
}
```
### Customizing webpack config ### Customizing webpack config
<p><details> <p><details>
@ -1077,6 +1088,21 @@ Some commonly asked for features are available as modules:
*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* *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*
Multiple configurations can be combined together with function composition. For example:
```js
const withTypescript = require('@zeit/next-typescript')
const withSass = require('@zeit/next-sass')
module.exports = withTypescript(withSass({
webpack(config, options) {
// Further custom configuration here
return config
}
}))
```
### Customizing babel config ### Customizing babel config
<p><details> <p><details>
@ -1094,7 +1120,8 @@ Here's an example `.babelrc` file:
```json ```json
{ {
"presets": ["next/babel", "env"] "presets": ["next/babel"],
"plugins": []
} }
``` ```
@ -1267,6 +1294,7 @@ For the production deployment, you can use the [path alias](https://zeit.co/docs
- [Setting up 301 redirects](https://www.raygesualdo.com/posts/301-redirects-with-nextjs/) - [Setting up 301 redirects](https://www.raygesualdo.com/posts/301-redirects-with-nextjs/)
- [Dealing with SSR and server only modules](https://arunoda.me/blog/ssr-and-server-only-modules) - [Dealing with SSR and server only modules](https://arunoda.me/blog/ssr-and-server-only-modules)
- [Building with React-Material-UI-Next-Express-Mongoose-Mongodb](https://github.com/builderbook/builderbook)
## FAQ ## FAQ

View file

@ -1,8 +0,0 @@
import { resolve } from 'path'
import del from 'del'
import getConfig from '../config'
export default function clean (dir) {
const dist = getConfig(dir).distDir
return del(resolve(dir, dist), { force: true })
}

View file

@ -1,22 +1,19 @@
import { join } from 'path' import { join, resolve, relative, dirname } from 'path'
// This plugin modifies the require-ensure code generated by Webpack // This plugin modifies the require-ensure code generated by Webpack
// to work with Next.js SSR // to work with Next.js SSR
export default class NextJsSsrImportPlugin { export default class NextJsSsrImportPlugin {
constructor ({ dir, dist }) {
this.dir = dir
this.dist = dist
}
apply (compiler) { apply (compiler) {
compiler.plugin('compilation', (compilation) => { compiler.plugin('compilation', (compilation) => {
compilation.mainTemplate.plugin('require-ensure', (code) => { compilation.mainTemplate.plugin('require-ensure', (code, chunk) => {
// Update to load chunks from our custom chunks directory // Update to load chunks from our custom chunks directory
const chunksDirPath = join(this.dir, this.dist, 'dist') const outputPath = resolve('/')
const pagePath = join('/', dirname(chunk.name))
const relativePathToBaseDir = relative(pagePath, outputPath)
// Make sure even in windows, the path looks like in unix // Make sure even in windows, the path looks like in unix
// Node.js require system will convert it accordingly // Node.js require system will convert it accordingly
const chunksDirPathNormalized = chunksDirPath.replace(/\\/g, '/') const relativePathToBaseDirNormalized = relativePathToBaseDir.replace(/\\/g, '/')
let updatedCode = code.replace('require("./"', `require("${chunksDirPathNormalized}/"`) let updatedCode = code.replace('require("./"', `require("${relativePathToBaseDirNormalized}/"`)
// Replace a promise equivalent which runs in the same loop // Replace a promise equivalent which runs in the same loop
// If we didn't do this webpack's module loading process block us from // If we didn't do this webpack's module loading process block us from

View file

@ -1,26 +0,0 @@
// Next.js needs to use module.resolve to generate paths to modules it includes,
// but those paths need to be relative to something so that they're portable
// across directories and machines.
//
// This function returns paths relative to the top-level 'node_modules'
// directory found in the path. If none is found, returns the complete path.
import { sep } from 'path'
const RELATIVE_START = `node_modules${sep}`
// Pass in the module's `require` object since it's module-specific.
export default (moduleRequire) => (path) => {
// package.json removed because babel-runtime is resolved as
// "babel-runtime/package"
const absolutePath = moduleRequire.resolve(path)
.replace(/[\\/]package\.json$/, '')
const relativeStartIndex = absolutePath.indexOf(RELATIVE_START)
if (relativeStartIndex === -1) {
return absolutePath
}
return absolutePath.substring(relativeStartIndex + RELATIVE_START.length)
}

View file

@ -71,7 +71,15 @@ function externalsConfig (dir, isServer) {
} }
// Webpack itself has to be compiled because it doesn't always use module relative paths // Webpack itself has to be compiled because it doesn't always use module relative paths
if (res.match(/node_modules[/\\].*\.js/) && !res.match(/node_modules[/\\]webpack/)) { if (res.match(/node_modules[/\\]next[/\\]dist[/\\]pages/)) {
return callback()
}
if (res.match(/node_modules[/\\]webpack/)) {
return callback()
}
if (res.match(/node_modules[/\\].*\.js/)) {
return callback(null, `commonjs ${request}`) return callback(null, `commonjs ${request}`)
} }
@ -107,7 +115,7 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
externals: externalsConfig(dir, isServer), externals: externalsConfig(dir, isServer),
context: dir, context: dir,
entry: async () => { entry: async () => {
const pages = await getPages(dir, {dev, isServer}) const pages = await getPages(dir, {dev, isServer, pageExtensions: config.pageExtensions.join('|')})
totalPages = Object.keys(pages).length totalPages = Object.keys(pages).length
const mainJS = require.resolve(`../../client/next${dev ? '-dev' : ''}`) const mainJS = require.resolve(`../../client/next${dev ? '-dev' : ''}`)
const clientConfig = !isServer ? { const clientConfig = !isServer ? {
@ -141,10 +149,15 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
], ],
alias: { alias: {
next: nextDir, next: nextDir,
// This bypasses React's check for production mode. Since we know it is in production this way. // React already does something similar to this.
// This allows us to exclude React from being uglified. Saving multiple seconds per build. // But if the user has react-devtools, it'll throw an error showing that
react: dev ? 'react/cjs/react.development.js' : 'react/cjs/react.production.min.js', // we haven't done dead code elimination (via uglifyjs).
'react-dom': dev ? 'react-dom/cjs/react-dom.development.js' : 'react-dom/cjs/react-dom.production.min.js' // We purposly do not uglify React code to save the build time.
// (But it didn't increase the overall build size)
// Here we are doing an exact match with '$'
// So, you can still require nested modules like `react-dom/server`
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: {
@ -242,7 +255,7 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
!dev && new webpack.optimize.ModuleConcatenationPlugin(), !dev && new webpack.optimize.ModuleConcatenationPlugin(),
!isServer && new PagesPlugin(), !isServer && new PagesPlugin(),
!isServer && new DynamicChunksPlugin(), !isServer && new DynamicChunksPlugin(),
isServer && new NextJsSsrImportPlugin({ dir, dist: config.distDir }), isServer && new NextJsSsrImportPlugin(),
!isServer && new webpack.optimize.CommonsChunkPlugin({ !isServer && new webpack.optimize.CommonsChunkPlugin({
name: `commons`, name: `commons`,
filename: `commons.js`, filename: `commons.js`,
@ -250,11 +263,11 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
// We need to move react-dom explicitly into common chunks. // We need to move react-dom explicitly into common chunks.
// Otherwise, if some other page or module uses it, it might // Otherwise, if some other page or module uses it, it might
// included in that bundle too. // included in that bundle too.
if (dev && module.context && module.context.indexOf(`${sep}react${sep}`) >= 0) { if (module.context && module.context.indexOf(`${sep}react${sep}`) >= 0) {
return true return true
} }
if (dev && module.context && module.context.indexOf(`${sep}react-dom${sep}`) >= 0) { if (module.context && module.context.indexOf(`${sep}react-dom${sep}`) >= 0) {
return true return true
} }
@ -301,7 +314,7 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
} }
if (typeof config.webpack === 'function') { if (typeof config.webpack === 'function') {
webpackConfig = config.webpack(webpackConfig, {dir, dev, isServer, buildId, config, defaultLoaders}) webpackConfig = config.webpack(webpackConfig, {dir, dev, isServer, buildId, config, defaultLoaders, totalPages})
} }
return webpackConfig return webpackConfig

View file

@ -3,26 +3,28 @@ import glob from 'glob-promise'
const nextPagesDir = path.join(__dirname, '..', '..', '..', 'pages') const nextPagesDir = path.join(__dirname, '..', '..', '..', 'pages')
export async function getPages (dir, {dev, isServer}) { export async function getPages (dir, {dev, isServer, pageExtensions}) {
const pageFiles = await getPagePaths(dir, {dev, isServer}) const pageFiles = await getPagePaths(dir, {dev, isServer, pageExtensions})
return getPageEntries(pageFiles, {isServer}) return getPageEntries(pageFiles, {isServer, pageExtensions})
} }
async function getPagePaths (dir, {dev, isServer}) { async function getPagePaths (dir, {dev, isServer, pageExtensions}) {
let pages let pages
if (dev) { if (dev) {
pages = await glob(isServer ? 'pages/+(_document|_error).+(js|jsx|ts|tsx)' : 'pages/_error.+(js|jsx|ts|tsx)', { cwd: dir }) // In development we only compile _document.js and _error.js when starting, since they're always needed. All other pages are compiled with on demand entries
pages = await glob(isServer ? `pages/+(_document|_error).+(${pageExtensions})` : `pages/_error.+(${pageExtensions})`, { cwd: dir })
} else { } else {
pages = await glob(isServer ? 'pages/**/*.+(js|jsx|ts|tsx)' : 'pages/**/!(_document)*.+(js|jsx|ts|tsx)', { cwd: dir }) // In production get all pages from the pages directory
pages = await glob(isServer ? `pages/**/*.+(${pageExtensions})` : `pages/**/!(_document)*.+(${pageExtensions})`, { cwd: dir })
} }
return pages return pages
} }
// Convert page path into single entry // Convert page path into single entry
export function createEntry (filePath, name) { export function createEntry (filePath, {name, pageExtensions} = {}) {
const parsedPath = path.parse(filePath) const parsedPath = path.parse(filePath)
let entryName = name || filePath let entryName = name || filePath
@ -33,7 +35,9 @@ export function createEntry (filePath, name) {
} }
// Makes sure supported extensions are stripped off. The outputted file should always be `.js` // Makes sure supported extensions are stripped off. The outputted file should always be `.js`
entryName = entryName.replace(/\.+(jsx|tsx|ts)/, '.js') if (pageExtensions) {
entryName = entryName.replace(new RegExp(`\\.+(${pageExtensions})$`), '.js')
}
return { return {
name: path.join('bundles', entryName), name: path.join('bundles', entryName),
@ -42,23 +46,23 @@ export function createEntry (filePath, name) {
} }
// Convert page paths into entries // Convert page paths into entries
export function getPageEntries (pagePaths, {isServer}) { export function getPageEntries (pagePaths, {isServer = false, pageExtensions} = {}) {
const entries = {} const entries = {}
for (const filePath of pagePaths) { for (const filePath of pagePaths) {
const entry = createEntry(filePath) const entry = createEntry(filePath, {pageExtensions})
entries[entry.name] = entry.files entries[entry.name] = entry.files
} }
const errorPagePath = path.join(nextPagesDir, '_error.js') const errorPagePath = path.join(nextPagesDir, '_error.js')
const errorPageEntry = createEntry(errorPagePath, 'pages/_error.js') // default error.js const errorPageEntry = createEntry(errorPagePath, {name: 'pages/_error.js'}) // default error.js
if (!entries[errorPageEntry.name]) { if (!entries[errorPageEntry.name]) {
entries[errorPageEntry.name] = errorPageEntry.files entries[errorPageEntry.name] = errorPageEntry.files
} }
if (isServer) { if (isServer) {
const documentPagePath = path.join(nextPagesDir, '_document.js') const documentPagePath = path.join(nextPagesDir, '_document.js')
const documentPageEntry = createEntry(documentPagePath, 'pages/_document.js') const documentPageEntry = createEntry(documentPagePath, {name: 'pages/_document.js'}) // default _document.js
if (!entries[documentPageEntry.name]) { if (!entries[documentPageEntry.name]) {
entries[documentPageEntry.name] = documentPageEntry.files entries[documentPageEntry.name] = documentPageEntry.files
} }

View file

@ -5,10 +5,12 @@ 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,
pageExtensions: ['jsx', 'js'] // jsx before js because otherwise regex matching will match js first
} }
export default function getConfig (dir, customConfig) { export default function getConfig (dir, customConfig) {
@ -32,9 +34,6 @@ 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

@ -84,12 +84,12 @@ export class Head extends Component {
render () { render () {
const { head, styles, __NEXT_DATA__ } = this.context._documentProps const { head, styles, __NEXT_DATA__ } = this.context._documentProps
const { pathname, buildId, assetPrefix } = __NEXT_DATA__ const { page, pathname, buildId, assetPrefix } = __NEXT_DATA__
const pagePathname = getPagePathname(pathname) const pagePathname = getPagePathname(pathname)
return <head {...this.props}> return <head {...this.props}>
{(head || []).map((h, i) => React.cloneElement(h, { key: h.key || i }))} {(head || []).map((h, i) => React.cloneElement(h, { key: h.key || i }))}
<link rel='preload' href={`${assetPrefix}/_next/${buildId}/page${pagePathname}`} as='script' /> {page !== '/_error' && <link rel='preload' href={`${assetPrefix}/_next/${buildId}/page${pagePathname}`} as='script' />}
<link rel='preload' href={`${assetPrefix}/_next/${buildId}/page/_error.js`} as='script' /> <link rel='preload' href={`${assetPrefix}/_next/${buildId}/page/_error.js`} as='script' />
{this.getPreloadDynamicChunks()} {this.getPreloadDynamicChunks()}
{this.getPreloadMainLinks()} {this.getPreloadMainLinks()}
@ -173,7 +173,7 @@ export class NextScript extends Component {
render () { render () {
const { staticMarkup, __NEXT_DATA__, chunks } = this.context._documentProps const { staticMarkup, __NEXT_DATA__, chunks } = this.context._documentProps
const { pathname, buildId, assetPrefix } = __NEXT_DATA__ const { page, pathname, buildId, assetPrefix } = __NEXT_DATA__
const pagePathname = getPagePathname(pathname) const pagePathname = getPagePathname(pathname)
__NEXT_DATA__.chunks = chunks.names __NEXT_DATA__.chunks = chunks.names
@ -193,9 +193,18 @@ export class NextScript extends Component {
__NEXT_REGISTER_CHUNK = function (chunkName, fn) { __NEXT_REGISTER_CHUNK = function (chunkName, fn) {
__NEXT_LOADED_CHUNKS__.push({ chunkName: chunkName, fn: fn }) __NEXT_LOADED_CHUNKS__.push({ chunkName: chunkName, fn: fn })
} }
${page === '_error' && `
__NEXT_REGISTER_PAGE(${htmlescape(pathname)}, function() {
var error = new Error('Page does not exist: ${htmlescape(pathname)}')
error.statusCode = 404
return { error: error }
})
`}
` `
}} />} }} />}
<script async id={`__NEXT_PAGE__${pathname}`} type='text/javascript' src={`${assetPrefix}/_next/${buildId}/page${pagePathname}`} /> {page !== '/_error' && <script async id={`__NEXT_PAGE__${pathname}`} type='text/javascript' src={`${assetPrefix}/_next/${buildId}/page${pagePathname}`} />}
<script async id={`__NEXT_PAGE__/_error`} type='text/javascript' src={`${assetPrefix}/_next/${buildId}/page/_error.js`} /> <script async id={`__NEXT_PAGE__/_error`} type='text/javascript' src={`${assetPrefix}/_next/${buildId}/page/_error.js`} />
{staticMarkup ? null : this.getDynamicChunks()} {staticMarkup ? null : this.getDynamicChunks()}
{staticMarkup ? null : this.getScripts()} {staticMarkup ? null : this.getScripts()}

View file

@ -49,6 +49,15 @@ export default async function (dir, options, configuration) {
) )
} }
// Copy .next/static directory
if (existsSync(join(nextDir, 'static'))) {
log(' copying "static build" directory')
await cp(
join(nextDir, 'static'),
join(outDir, '_next', 'static')
)
}
// Copy dynamic import chunks // Copy dynamic import chunks
if (existsSync(join(nextDir, 'chunks'))) { if (existsSync(join(nextDir, 'chunks'))) {
log(' copying dynamic import chunks') log(' copying dynamic import chunks')
@ -76,6 +85,7 @@ export default async function (dir, options, configuration) {
// Start the rendering process // Start the rendering process
const renderOpts = { const renderOpts = {
dir, dir,
dist: config.distDir,
buildStats, buildStats,
buildId, buildId,
nextExport: true, nextExport: true,

View file

@ -1,11 +1,10 @@
import { join, relative, sep } from 'path' 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 del from 'del'
import onDemandEntryHandler from './on-demand-entry-handler' import onDemandEntryHandler from './on-demand-entry-handler'
import webpack from 'webpack' import webpack from 'webpack'
import getBaseWebpackConfig from './build/webpack' import getBaseWebpackConfig from './build/webpack'
import clean from './build/clean'
import getConfig from './config'
import UUID from 'uuid' import UUID from 'uuid'
import { import {
IS_BUNDLED_PAGE, IS_BUNDLED_PAGE,
@ -13,7 +12,7 @@ import {
} from './utils' } from './utils'
export default class HotReloader { export default class HotReloader {
constructor (dir, { quiet, conf } = {}) { constructor (dir, { quiet, config } = {}) {
this.dir = dir this.dir = dir
this.quiet = quiet this.quiet = quiet
this.middlewares = [] this.middlewares = []
@ -32,7 +31,7 @@ export default class HotReloader {
// it should be the same value. // it should be the same value.
this.buildId = UUID.v4() this.buildId = UUID.v4()
this.config = getConfig(dir, conf) this.config = config
} }
async run (req, res) { async run (req, res) {
@ -55,8 +54,12 @@ export default class HotReloader {
} }
} }
async clean () {
return del(join(this.dir, this.config.distDir), { force: true })
}
async start () { async start () {
await clean(this.dir) await this.clean()
const configs = await Promise.all([ const configs = await Promise.all([
getBaseWebpackConfig(this.dir, { dev: true, isServer: false, config: this.config }), getBaseWebpackConfig(this.dir, { dev: true, isServer: false, config: this.config }),
@ -86,7 +89,7 @@ export default class HotReloader {
async reload () { async reload () {
this.stats = null this.stats = null
await clean(this.dir) await this.clean()
const configs = await Promise.all([ const configs = await Promise.all([
getBaseWebpackConfig(this.dir, { dev: true, isServer: false, config: this.config }), getBaseWebpackConfig(this.dir, { dev: true, isServer: false, config: this.config }),
@ -225,6 +228,7 @@ export default class HotReloader {
dir: this.dir, dir: this.dir,
dev: true, dev: true,
reload: this.reload.bind(this), reload: this.reload.bind(this),
pageExtensions: this.config.pageExtensions,
...this.config.onDemandEntries ...this.config.onDemandEntries
}) })

View file

@ -32,11 +32,12 @@ export default class Server {
this.dev = dev this.dev = dev
this.quiet = quiet this.quiet = quiet
this.router = new Router() this.router = new Router()
this.hotReloader = dev ? this.getHotReloader(this.dir, { quiet, conf }) : null
this.http = null this.http = null
this.config = getConfig(this.dir, conf) this.config = getConfig(this.dir, conf)
this.dist = this.config.distDir this.dist = this.config.distDir
this.hotReloader = dev ? this.getHotReloader(this.dir, { quiet, config: this.config }) : null
if (dev) { if (dev) {
updateNotifier(pkg, 'next') updateNotifier(pkg, 'next')
} }
@ -51,6 +52,7 @@ export default class Server {
dev, dev,
staticMarkup, staticMarkup,
dir: this.dir, dir: this.dir,
dist: this.dist,
hotReloader: this.hotReloader, hotReloader: this.hotReloader,
buildStats: this.buildStats, buildStats: this.buildStats,
buildId: this.buildId, buildId: this.buildId,
@ -183,8 +185,7 @@ export default class Server {
} }
} }
const dist = getConfig(this.dir).distDir const path = join(this.dir, this.dist, 'bundles', 'pages', `${page}.js.map`)
const path = join(this.dir, dist, 'bundles', 'pages', `${page}.js.map`)
await serveStatic(req, res, path) await serveStatic(req, res, path)
}, },
@ -241,7 +242,7 @@ export default class Server {
}, },
'/_next/static/:path*': async (req, res, params) => { '/_next/static/:path*': async (req, res, params) => {
const p = join(this.dist, 'static', ...(params.path || [])) const p = join(this.dir, this.dist, 'static', ...(params.path || []))
await this.serveStatic(req, res, p) await this.serveStatic(req, res, p)
}, },
@ -322,7 +323,9 @@ export default class Server {
return return
} }
res.setHeader('X-Powered-By', `Next.js ${pkg.version}`) if (this.config.poweredByHeader) {
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)
} }

View file

@ -1,9 +1,10 @@
import DynamicEntryPlugin from 'webpack/lib/DynamicEntryPlugin' import DynamicEntryPlugin from 'webpack/lib/DynamicEntryPlugin'
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import { join, relative } from 'path' import { join } from 'path'
import { parse } from 'url' import { parse } from 'url'
import touch from 'touch' import touch from 'touch'
import resolvePath from './resolve' import glob from 'glob-promise'
import {normalizePagePath, pageNotFoundError} from './require'
import {createEntry} from './build/webpack/utils' import {createEntry} from './build/webpack/utils'
import { MATCH_ROUTE_NAME, IS_BUNDLED_PAGE } from './utils' import { MATCH_ROUTE_NAME, IS_BUNDLED_PAGE } from './utils'
@ -15,6 +16,7 @@ export default function onDemandEntryHandler (devMiddleware, compilers, {
dir, dir,
dev, dev,
reload, reload,
pageExtensions,
maxInactiveAge = 1000 * 60, maxInactiveAge = 1000 * 60,
pagesBufferLength = 2 pagesBufferLength = 2
}) { }) {
@ -37,7 +39,7 @@ export default function onDemandEntryHandler (devMiddleware, compilers, {
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, compiler.context, name, entry)
}) })
Promise.all(allEntries) Promise.all(allEntries)
@ -139,10 +141,26 @@ export default function onDemandEntryHandler (devMiddleware, compilers, {
async ensurePage (page) { async ensurePage (page) {
await this.waitUntilReloaded() await this.waitUntilReloaded()
page = normalizePage(page) page = normalizePage(page)
let normalizedPagePath
try {
normalizedPagePath = normalizePagePath(page)
} catch (err) {
console.error(err)
throw pageNotFoundError(normalizedPagePath)
}
const pagePath = join(dir, 'pages', page) const extensions = pageExtensions.join('|')
const pathname = await resolvePath(pagePath) const paths = await glob(`pages/{${normalizedPagePath}/index,${normalizedPagePath}}.+(${extensions})`, {cwd: dir})
const {name, files} = createEntry(relative(dir, pathname))
if (paths.length === 0) {
throw pageNotFoundError(normalizedPagePath)
}
const relativePathToPage = paths[0]
const pathname = join(dir, relativePathToPage)
const {name, files} = createEntry(relativePathToPage, {pageExtensions: extensions})
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const entryInfo = entries[page] const entryInfo = entries[page]

View file

@ -4,8 +4,7 @@ import { renderToString, renderToStaticMarkup } from 'react-dom/server'
import send from 'send' import send from 'send'
import generateETag from 'etag' import generateETag from 'etag'
import fresh from 'fresh' import fresh from 'fresh'
import requireModule from './require' import requirePage from './require'
import getConfig from './config'
import { Router } from '../lib/router' import { Router } from '../lib/router'
import { loadGetInitialProps, isResSent } from '../lib/utils' import { loadGetInitialProps, isResSent } from '../lib/utils'
import { getAvailableChunks } from './utils' import { getAvailableChunks } from './utils'
@ -30,7 +29,7 @@ export async function renderError (err, req, res, pathname, query, opts) {
} }
export function renderErrorToHTML (err, req, res, pathname, query, opts = {}) { export function renderErrorToHTML (err, req, res, pathname, query, opts = {}) {
return doRender(req, res, pathname, query, { ...opts, err, page: '_error' }) return doRender(req, res, pathname, query, { ...opts, err, page: '/_error' })
} }
async function doRender (req, res, pathname, query, { async function doRender (req, res, pathname, query, {
@ -41,6 +40,7 @@ async function doRender (req, res, pathname, query, {
hotReloader, hotReloader,
assetPrefix, assetPrefix,
availableChunks, availableChunks,
dist,
dir = process.cwd(), dir = process.cwd(),
dev = false, dev = false,
staticMarkup = false, staticMarkup = false,
@ -48,17 +48,14 @@ async function doRender (req, res, pathname, query, {
} = {}) { } = {}) {
page = page || pathname page = page || pathname
await ensurePage(page, { dir, hotReloader }) if (hotReloader) { // In dev mode we use on demand entries to compile the page before rendering
await ensurePage(page, { dir, hotReloader })
}
const dist = getConfig(dir).distDir
const pagePath = join(dir, dist, 'dist', 'bundles', 'pages', page)
const documentPath = join(dir, dist, 'dist', 'bundles', 'pages', '_document') const documentPath = join(dir, dist, 'dist', 'bundles', 'pages', '_document')
let [Component, Document] = await Promise.all([ let Component = requirePage(page, {dir, dist})
requireModule(pagePath), let Document = require(documentPath)
requireModule(documentPath)
])
Component = Component.default || Component Component = Component.default || Component
Document = Document.default || Document Document = Document.default || Document
const asPath = req.url const asPath = req.url
@ -105,7 +102,8 @@ async function doRender (req, res, pathname, query, {
const doc = createElement(Document, { const doc = createElement(Document, {
__NEXT_DATA__: { __NEXT_DATA__: {
props, props,
pathname, page, // the rendered page
pathname, // the requested path
query, query,
buildId, buildId,
buildStats, buildStats,
@ -224,8 +222,7 @@ export function serveStatic (req, res, path) {
} }
async function ensurePage (page, { dir, hotReloader }) { async function ensurePage (page, { dir, hotReloader }) {
if (!hotReloader) return if (page === '/_error') return
if (page === '_error' || page === '_document') return
await hotReloader.ensurePage(page) await hotReloader.ensurePage(page)
} }

View file

@ -1,6 +1,63 @@
import resolve from './resolve' import {join, parse, normalize, sep} from 'path'
export default async function requireModule (path) { export function pageNotFoundError (page) {
const f = await resolve(path) const err = new Error(`Cannot find module for page: ${page}`)
return require(f) err.code = 'ENOENT'
return err
}
export function normalizePagePath (page) {
// If the page is `/` we need to append `/index`, otherwise the returned directory root will be bundles instead of pages
if (page === '/') {
page = '/index'
}
// Resolve on anything that doesn't start with `/`
if (page[0] !== '/') {
page = `/${page}`
}
// Windows compatibility
if (sep !== '/') {
page = page.replace(/\//g, sep)
}
// Throw when using ../ etc in the pathname
const resolvedPage = normalize(page)
if (page !== resolvedPage) {
throw new Error('Requested and resolved page mismatch')
}
return page
}
export function getPagePath (page, {dir, dist}) {
const pageBundlesPath = join(dir, dist, 'dist', 'bundles', 'pages')
try {
page = normalizePagePath(page)
} catch (err) {
console.error(err)
throw pageNotFoundError(page)
}
const pagePath = join(pageBundlesPath, page) // Path to the page that is to be loaded
// Don't allow wandering outside of the bundles directory
const pathDir = parse(pagePath).dir
if (pathDir.indexOf(pageBundlesPath) !== 0) {
console.error('Resolved page path goes outside of bundles path')
throw pageNotFoundError(page)
}
return pagePath
}
export default function requirePage (page, {dir, dist}) {
const pagePath = getPagePath(page, {dir, dist})
try {
return require(pagePath)
} catch (err) {
throw pageNotFoundError(page)
}
} }

View file

@ -1,96 +0,0 @@
import { join, sep, parse } from 'path'
import fs from 'mz/fs'
import glob from 'glob-promise'
export default async function resolve (id) {
const paths = getPaths(id)
for (const p of paths) {
if (await isFile(p)) {
return p
}
}
const err = new Error(`Cannot find module ${id}`)
err.code = 'ENOENT'
throw err
}
export function resolveFromList (id, files) {
const paths = getPaths(id)
const set = new Set(files)
for (const p of paths) {
if (set.has(p)) return p
}
}
function getPaths (id) {
const i = sep === '/' ? id : id.replace(/\//g, sep)
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'
]
}
return [
i + '.js',
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')
]
}
async function isFile (p) {
let stat
try {
stat = await fs.stat(p)
} catch (err) {
if (err.code === 'ENOENT') return false
throw err
}
// We need the path to be case sensitive
const realpath = await getTrueFilePath(p)
if (p !== realpath) return false
return stat.isFile() || stat.isFIFO()
}
// This is based on the stackoverflow answer: http://stackoverflow.com/a/33139702/457224
// We assume we'll get properly normalized path names as p
async function getTrueFilePath (p) {
let fsPathNormalized = p
// OSX: HFS+ stores filenames in NFD (decomposed normal form) Unicode format,
// so we must ensure that the input path is in that format first.
if (process.platform === 'darwin') fsPathNormalized = fsPathNormalized.normalize('NFD')
// !! Windows: Curiously, the drive component mustn't be part of a glob,
// !! otherwise glob.sync() will invariably match nothing.
// !! Thus, we remove the drive component and instead pass it in as the 'cwd'
// !! (working dir.) property below.
var pathRoot = parse(fsPathNormalized).root
var noDrivePath = fsPathNormalized.slice(Math.max(pathRoot.length - 1, 0))
// Perform case-insensitive globbing (on Windows, relative to the drive /
// network share) and return the 1st match, if any.
// Fortunately, glob() with nocase case-corrects the input even if it is
// a *literal* path.
const result = await glob(noDrivePath, { nocase: true, cwd: pathRoot })
return result[0]
}

View file

@ -370,16 +370,8 @@ export default (context, render) => {
browser.close() browser.close()
}) })
it('should work with dir/index page ', async () => {
const browser = await webdriver(context.appPort, '/nested-cdm/index')
const text = await browser.elementByCss('p').text()
expect(text).toBe('ComponentDidMount executed on client.')
browser.close()
})
it('should work with dir/ page ', async () => { it('should work with dir/ page ', async () => {
const browser = await webdriver(context.appPort, '/nested-cdm/') const browser = await webdriver(context.appPort, '/nested-cdm')
const text = await browser.elementByCss('p').text() const text = await browser.elementByCss('p').text()
expect(text).toBe('ComponentDidMount executed on client.') expect(text).toBe('ComponentDidMount executed on client.')
@ -426,7 +418,7 @@ export default (context, render) => {
describe('with asPath', () => { describe('with asPath', () => {
describe('inside getInitialProps', () => { describe('inside getInitialProps', () => {
it('should show the correct asPath with a Link with as prop', async () => { it('should show the correct asPath with a Link with as prop', async () => {
const browser = await webdriver(context.appPort, '/nav/') const browser = await webdriver(context.appPort, '/nav')
const asPath = await browser const asPath = await browser
.elementByCss('#as-path-link').click() .elementByCss('#as-path-link').click()
.waitForElementByCss('.as-path-content') .waitForElementByCss('.as-path-content')
@ -437,7 +429,7 @@ export default (context, render) => {
}) })
it('should show the correct asPath with a Link without the as prop', async () => { it('should show the correct asPath with a Link without the as prop', async () => {
const browser = await webdriver(context.appPort, '/nav/') const browser = await webdriver(context.appPort, '/nav')
const asPath = await browser const asPath = await browser
.elementByCss('#as-path-link-no-as').click() .elementByCss('#as-path-link-no-as').click()
.waitForElementByCss('.as-path-content') .waitForElementByCss('.as-path-content')
@ -450,7 +442,7 @@ export default (context, render) => {
describe('with next/router', () => { describe('with next/router', () => {
it('should show the correct asPath', async () => { it('should show the correct asPath', async () => {
const browser = await webdriver(context.appPort, '/nav/') const browser = await webdriver(context.appPort, '/nav')
const asPath = await browser const asPath = await browser
.elementByCss('#as-path-using-router-link').click() .elementByCss('#as-path-using-router-link').click()
.waitForElementByCss('.as-path-content') .waitForElementByCss('.as-path-content')
@ -461,5 +453,31 @@ export default (context, render) => {
}) })
}) })
}) })
describe('with 404 pages', () => {
it('should 404 on not existent page', async () => {
const browser = await webdriver(context.appPort, '/non-existent')
expect(await browser.elementByCss('h1').text()).toBe('404')
expect(await browser.elementByCss('h2').text()).toBe('This page could not be found.')
browser.close()
})
it('should 404 for <page>/', async () => {
const browser = await webdriver(context.appPort, '/nav/about/')
expect(await browser.elementByCss('h1').text()).toBe('404')
expect(await browser.elementByCss('h2').text()).toBe('This page could not be found.')
browser.close()
})
it('should should not contain a page script in a 404 page', async () => {
const browser = await webdriver(context.appPort, '/non-existent')
const scripts = await browser.elementsByCss('script[src]')
for (const script of scripts) {
const src = await script.getAttribute('src')
expect(src.includes('/non-existent')).toBeFalsy()
}
browser.close()
})
})
}) })
} }

View file

@ -106,10 +106,28 @@ export default function ({ app }, suiteName, render, fetch) {
expect($('.as-path-content').text()).toBe('/nav/as-path?aa=10') expect($('.as-path-content').text()).toBe('/nav/as-path?aa=10')
}) })
test('error 404', async () => { describe('404', () => {
const $ = await get$('/non-existent') it('should 404 on not existent page', async () => {
expect($('h1').text()).toBe('404') const $ = await get$('/non-existent')
expect($('h2').text()).toBe('This page could not be found.') expect($('h1').text()).toBe('404')
expect($('h2').text()).toBe('This page could not be found.')
})
it('should 404 for <page>/', async () => {
const $ = await get$('/nav/about/')
expect($('h1').text()).toBe('404')
expect($('h2').text()).toBe('This page could not be found.')
})
it('should should not contain a page script in a 404 page', async () => {
const $ = await get$('/non-existent')
$('script[src]').each((index, element) => {
const src = $(element).attr('src')
if (src.includes('/non-existent')) {
throw new Error('Page includes page script')
}
})
})
}) })
describe('with the HOC based router', () => { describe('with the HOC based router', () => {

View file

@ -0,0 +1,33 @@
import Link from 'next/link'
import { Component } from 'react'
import Router from 'next/router'
let counter = 0
export default class extends Component {
increase () {
counter++
this.forceUpdate()
}
visitQueryStringPage () {
const href = { pathname: '/nav/querystring', query: { id: 10 } }
const as = { pathname: '/nav/querystring/10', hash: '10' }
Router.push(href, as)
}
render () {
return (
<div id='counter-page'>
<Link href='/no-such-page'><a id='no-such-page'>No Such Page</a></Link>
<br />
<Link href='/no-such-page' prefetch><a id='no-such-page-prefetch'>No Such Page (with prefetch)</a></Link>
<p>This is the home.</p>
<div id='counter'>
Counter: {counter}
</div>
<button id='increase' onClick={() => this.increase()}>Increase</button>
</div>
)
}
}

View file

@ -7,7 +7,8 @@ import {
nextBuild, nextBuild,
startApp, startApp,
stopApp, stopApp,
renderViaHTTP renderViaHTTP,
waitFor
} from 'next-test-utils' } from 'next-test-utils'
import webdriver from 'next-webdriver' import webdriver from 'next-webdriver'
import fetch from 'node-fetch' import fetch from 'node-fetch'
@ -93,6 +94,55 @@ describe('Production Usage', () => {
const data = await renderViaHTTP(appPort, '/static/data/item.txt') const data = await renderViaHTTP(appPort, '/static/data/item.txt')
expect(data).toBe('item') expect(data).toBe('item')
}) })
it('should reload the page on page script error', async () => {
const browser = await webdriver(appPort, '/counter')
const counter = await browser
.elementByCss('#increase').click().click()
.elementByCss('#counter').text()
expect(counter).toBe('Counter: 2')
// When we go to the 404 page, it'll do a hard reload.
// So, it's possible for the front proxy to load a page from another zone.
// Since the page is reloaded, when we go back to the counter page again,
// previous counter value should be gone.
const counterAfter404Page = await browser
.elementByCss('#no-such-page').click()
.waitForElementByCss('h1')
.back()
.waitForElementByCss('#counter-page')
.elementByCss('#counter').text()
expect(counterAfter404Page).toBe('Counter: 0')
browser.close()
})
it('should reload the page on page script error with prefetch', async () => {
const browser = await webdriver(appPort, '/counter')
const counter = await browser
.elementByCss('#increase').click().click()
.elementByCss('#counter').text()
expect(counter).toBe('Counter: 2')
// Let the browser to prefetch the page and error it on the console.
await waitFor(3000)
const browserLogs = await browser.log('browser')
expect(browserLogs[0].message).toMatch(/Page does not exist: \/no-such-page/)
// When we go to the 404 page, it'll do a hard reload.
// So, it's possible for the front proxy to load a page from another zone.
// Since the page is reloaded, when we go back to the counter page again,
// previous counter value should be gone.
const counterAfter404Page = await browser
.elementByCss('#no-such-page-prefetch').click()
.waitForElementByCss('h1')
.back()
.waitForElementByCss('#counter-page')
.elementByCss('#counter').text()
expect(counterAfter404Page).toBe('Counter: 0')
browser.close()
})
}) })
describe('X-Powered-By header', () => { describe('X-Powered-By header', () => {
@ -112,6 +162,26 @@ 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 === 'XPoweredBy') {
throw new Error('Should not set the XPoweredBy 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/isolated/_resolvedata/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
!dist

View file

@ -0,0 +1,3 @@
module.exports = {
test: 'hello'
}

View file

@ -0,0 +1,3 @@
module.exports = {
test: 'world'
}

View file

@ -0,0 +1,82 @@
/* global describe, it, expect */
import { join, sep } from 'path'
import requirePage, {getPagePath, normalizePagePath, pageNotFoundError} from '../../dist/server/require'
const dir = '/path/to/some/project'
const dist = '.next'
const pathToBundles = join(dir, dist, 'dist', 'bundles', 'pages')
describe('pageNotFoundError', () => {
it('Should throw error with ENOENT code', () => {
try {
pageNotFoundError('test')
} catch (err) {
expect(err.code).toBe('ENOENT')
}
})
})
describe('normalizePagePath', () => {
it('Should turn / into /index', () => {
expect(normalizePagePath('/')).toBe(`${sep}index`)
})
it('Should turn _error into /_error', () => {
expect(normalizePagePath('_error')).toBe(`${sep}_error`)
})
it('Should turn /abc into /abc', () => {
expect(normalizePagePath('/abc')).toBe(`${sep}abc`)
})
it('Should turn /abc/def into /abc/def', () => {
expect(normalizePagePath('/abc/def')).toBe(`${sep}abc${sep}def`)
})
it('Should throw on /../../test.js', () => {
expect(() => normalizePagePath('/../../test.js')).toThrow()
})
})
describe('getPagePath', () => {
it('Should append /index to the / page', () => {
const pagePath = getPagePath('/', {dir, dist})
expect(pagePath).toBe(join(pathToBundles, `${sep}index`))
})
it('Should prepend / when a page does not have it', () => {
const pagePath = getPagePath('_error', {dir, dist})
expect(pagePath).toBe(join(pathToBundles, `${sep}_error`))
})
it('Should throw with paths containing ../', () => {
expect(() => getPagePath('/../../package.json', {dir, dist})).toThrow()
})
})
describe('requirePage', () => {
it('Should require /index.js when using /', () => {
const page = requirePage('/', {dir: __dirname, dist: '_resolvedata'})
expect(page.test).toBe('hello')
})
it('Should require /index.js when using /index', () => {
const page = requirePage('/index', {dir: __dirname, dist: '_resolvedata'})
expect(page.test).toBe('hello')
})
it('Should require /world.js when using /world', () => {
const page = requirePage('/world', {dir: __dirname, dist: '_resolvedata'})
expect(page.test).toBe('world')
})
it('Should throw when using /../../test.js', () => {
expect(() => requirePage('/../../test.js', {dir: __dirname, dist: '_resolvedata'})).toThrow()
})
it('Should throw when using non existent pages like /non-existent.js', () => {
expect(() => requirePage('/non-existent.js', {dir: __dirname, dist: '_resolvedata'})).toThrow()
})
})

View file

@ -1,52 +0,0 @@
/* global describe, it, expect */
import { join } from 'path'
import resolve from '../../dist/server/resolve'
const dataPath = join(__dirname, '_resolvedata')
describe('Resolve', () => {
it('should resolve a .js path', async () => {
const p = await resolve(join(dataPath, 'one.js'))
expect(p).toBe(join(dataPath, 'one.js'))
})
it('should resolve a .json path', async () => {
const p = await resolve(join(dataPath, 'two.json'))
expect(p).toBe(join(dataPath, 'two.json'))
})
it('should resolve a module without an extension', async () => {
const p = await resolve(join(dataPath, 'one'))
expect(p).toBe(join(dataPath, 'one.js'))
})
it('should resolve a .js module in a directory without /', async () => {
const p = await resolve(join(dataPath, 'aa'))
expect(p).toBe(join(dataPath, 'aa', 'index.js'))
})
it('should resolve a .js module in a directory with /', async () => {
const p = await resolve(join(dataPath, 'aa/'))
expect(p).toBe(join(dataPath, 'aa', 'index.js'))
})
it('should resolve a .json module in a directory', async () => {
const p = await resolve(join(dataPath, 'bb'))
expect(p).toBe(join(dataPath, 'bb', 'index.json'))
})
it('should resolve give priority to index.js over index.json', async () => {
const p = await resolve(join(dataPath, 'cc'))
expect(p).toBe(join(dataPath, 'cc', 'index.js'))
})
it('should throw an error for non existing paths', async () => {
try {
await resolve(join(dataPath, 'aaa.js'))
throw new Error('Should not run this line.')
} catch (ex) {
expect(ex.message).toMatch(/Cannot find module/)
}
})
})

View file

@ -0,0 +1,80 @@
/* global describe, it, expect */
import {normalize} from 'path'
import {getPageEntries, createEntry} from '../../dist/server/build/webpack/utils'
describe('createEntry', () => {
it('Should turn a path into a page entry', () => {
const entry = createEntry('pages/index.js')
expect(entry.name).toBe(normalize('bundles/pages/index.js'))
expect(entry.files[0]).toBe('./pages/index.js')
})
it('Should have a custom name', () => {
const entry = createEntry('pages/index.js', {name: 'something-else.js'})
expect(entry.name).toBe(normalize('bundles/something-else.js'))
expect(entry.files[0]).toBe('./pages/index.js')
})
it('Should allow custom extension like .ts to be turned into .js', () => {
const entry = createEntry('pages/index.ts', {pageExtensions: ['js', 'ts'].join('|')})
expect(entry.name).toBe(normalize('bundles/pages/index.js'))
expect(entry.files[0]).toBe('./pages/index.ts')
})
it('Should allow custom extension like .jsx to be turned into .js', () => {
const entry = createEntry('pages/index.jsx', {pageExtensions: ['jsx', 'js'].join('|')})
expect(entry.name).toBe(normalize('bundles/pages/index.js'))
expect(entry.files[0]).toBe('./pages/index.jsx')
})
it('Should allow custom extension like .tsx to be turned into .js', () => {
const entry = createEntry('pages/index.tsx', {pageExtensions: ['tsx', 'ts'].join('|')})
expect(entry.name).toBe(normalize('bundles/pages/index.js'))
expect(entry.files[0]).toBe('./pages/index.tsx')
})
it('Should allow custom extension like .tsx to be turned into .js with another order', () => {
const entry = createEntry('pages/index.tsx', {pageExtensions: ['ts', 'tsx'].join('|')})
expect(entry.name).toBe(normalize('bundles/pages/index.js'))
expect(entry.files[0]).toBe('./pages/index.tsx')
})
it('Should turn pages/blog/index.js into pages/blog.js', () => {
const entry = createEntry('pages/blog/index.js')
expect(entry.name).toBe(normalize('bundles/pages/blog.js'))
expect(entry.files[0]).toBe('./pages/blog/index.js')
})
})
describe('getPageEntries', () => {
it('Should return paths', () => {
const pagePaths = ['pages/index.js']
const pageEntries = getPageEntries(pagePaths)
expect(pageEntries[normalize('bundles/pages/index.js')][0]).toBe('./pages/index.js')
})
it('Should include default _error', () => {
const pagePaths = ['pages/index.js']
const pageEntries = getPageEntries(pagePaths)
expect(pageEntries[normalize('bundles/pages/_error.js')][0]).toMatch(/dist[/\\]pages[/\\]_error\.js/)
})
it('Should not include default _error when _error.js is inside the pages directory', () => {
const pagePaths = ['pages/index.js', 'pages/_error.js']
const pageEntries = getPageEntries(pagePaths)
expect(pageEntries[normalize('bundles/pages/_error.js')][0]).toBe('./pages/_error.js')
})
it('Should include default _document when isServer is true', () => {
const pagePaths = ['pages/index.js']
const pageEntries = getPageEntries(pagePaths, {isServer: true})
expect(pageEntries[normalize('bundles/pages/_document.js')][0]).toMatch(/dist[/\\]pages[/\\]_document\.js/)
})
it('Should not include default _document when _document.js is inside the pages directory', () => {
const pagePaths = ['pages/index.js', 'pages/_document.js']
const pageEntries = getPageEntries(pagePaths, {isServer: true})
expect(pageEntries[normalize('bundles/pages/_document.js')][0]).toBe('./pages/_document.js')
})
})

View file

@ -3158,10 +3158,14 @@ hoek@4.x.x:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
hoist-non-react-statics@2.3.1, hoist-non-react-statics@^2.3.1: hoist-non-react-statics@2.3.1:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0"
hoist-non-react-statics@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40"
home-or-tmp@^2.0.0: home-or-tmp@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
@ -5563,19 +5567,14 @@ react-dom@16.2.0:
object-assign "^4.1.1" object-assign "^4.1.1"
prop-types "^15.6.0" prop-types "^15.6.0"
react-hot-loader@4.0.0-beta.18: react-hot-loader@4.0.0-beta.23:
version "4.0.0-beta.18" version "4.0.0-beta.23"
resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.0.0-beta.18.tgz#5a3d1b5bd813633380b88c0c660019dbf638975d" resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.0.0-beta.23.tgz#dced27e8e2400adb1690eb5b3a0a39b4e0b90615"
dependencies: dependencies:
fast-levenshtein "^2.0.6" fast-levenshtein "^2.0.6"
global "^4.3.0" global "^4.3.0"
hoist-non-react-statics "^2.3.1" hoist-non-react-statics "^2.5.0"
react-stand-in "^4.0.0-beta.18" prop-types "^15.6.0"
react-stand-in@^4.0.0-beta.18:
version "4.0.0-beta.21"
resolved "https://registry.yarnpkg.com/react-stand-in/-/react-stand-in-4.0.0-beta.21.tgz#fb694e465cb20fab7f36d3284f82b68bbd7a657e"
dependencies:
shallowequal "^1.0.2" shallowequal "^1.0.2"
react@16.2.0: react@16.2.0:
@ -6361,25 +6360,25 @@ style-loader@^0.19.1:
loader-utils "^1.0.2" loader-utils "^1.0.2"
schema-utils "^0.3.0" schema-utils "^0.3.0"
styled-jsx@2.2.3: styled-jsx@2.2.5:
version "2.2.3" version "2.2.5"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-2.2.3.tgz#9fe3df55c852388019c3bf4c026bcbf8b3e003de" resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-2.2.5.tgz#3eb98418720655421bfc1a680e2add581726a5e6"
dependencies: dependencies:
babel-plugin-syntax-jsx "6.18.0" babel-plugin-syntax-jsx "6.18.0"
babel-types "6.26.0" babel-types "6.26.0"
convert-source-map "1.5.1" convert-source-map "1.5.1"
source-map "0.6.1" source-map "0.6.1"
string-hash "1.1.3" string-hash "1.1.3"
stylis "3.4.5" stylis "3.4.10"
stylis-rule-sheet "0.0.7" stylis-rule-sheet "0.0.8"
stylis-rule-sheet@0.0.7: stylis-rule-sheet@0.0.8:
version "0.0.7" version "0.0.8"
resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.7.tgz#5c51dc879141a61821c2094ba91d2cbcf2469c6c" resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.8.tgz#b0d0a126c945b1f3047447a3aae0647013e8d166"
stylis@3.4.5: stylis@3.4.10:
version "3.4.5" version "3.4.10"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.4.5.tgz#d7b9595fc18e7b9c8775eca8270a9a1d3e59806e" resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.4.10.tgz#a135cab4b9ff208e327fbb5a6fde3fa991c638ee"
supports-color@^2.0.0: supports-color@^2.0.0:
version "2.0.0" version "2.0.0"
@ -6881,7 +6880,7 @@ webpack-hot-middleware@2.21.0:
querystring "^0.2.0" querystring "^0.2.0"
strip-ansi "^3.0.0" strip-ansi "^3.0.0"
webpack-sources@^1.0.1, webpack-sources@^1.1.0: webpack-sources@1.1.0, webpack-sources@^1.0.1, webpack-sources@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54"
dependencies: dependencies: