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__: {
props,
err,
page,
pathname,
query,
buildId,
@ -76,7 +77,7 @@ export default async ({ ErrorDebugComponent: passedDebugComponent, stripAnsi: pa
ErrorComponent = await pageLoader.loadPage('/_error')
try {
Component = await pageLoader.loadPage(pathname)
Component = await pageLoader.loadPage(page)
} catch (err) {
console.error(stripAnsi(`${err.message}\n${err.stack}`))
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 child = Children.only(children)
let className = child.props.className || ''
let className = child.props.className || null
if (router.pathname === props.href && props.activeClassName) {
className = `${className} ${props.activeClassName}`.trim()
className = `${className !== null ? className : ''} ${props.activeClassName}`.trim()
}
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}`
}
function renderAndCache (req, res, pagePath, queryParams) {
async function renderAndCache (req, res, pagePath, queryParams) {
const key = getCacheKey(req)
// If we have a page in the cache, let's serve it
if (ssrCache.has(key)) {
console.log(`CACHE HIT: ${key}`)
res.setHeader('x-cache', 'HIT')
res.send(ssrCache.get(key))
return
}
// If not let's render the page into HTML
app.renderToHTML(req, res, pagePath, queryParams)
.then((html) => {
// Let's cache this page
console.log(`CACHE MISS: ${key}`)
ssrCache.set(key, html)
try {
// If not let's render the page into HTML
const html = await app.renderToHTML(req, res, pagePath, queryParams)
// Something is wrong with the request, let's skip the cache
if (res.statusCode !== 200) {
res.send(html)
})
.catch((err) => {
app.renderError(err, req, res, pagePath, queryParams)
})
return
}
// 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": {
"next": "latest",
"react": "latest,
"react": "latest",
"react-dom": "latest"
},
"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
// 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({}, '/signin')
})

View file

@ -18,7 +18,7 @@ const test = require('ava')
* Apollo that we can clear, etc
*/
const apolloFilePath = require.resolve('../lib/init-apollo')
const apolloFilePath = require.resolve('../lib/initApollo')
test.beforeEach(() => {
// 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.
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:
- create a project
- get your service account credentials and client credentials and set both in firebaseCredentials.js
- set your firebase database url in server.js
- on the firebase Authentication console, select Google as your provider
- Create a project at the [Firebase console](https://console.firebase.google.com/).
- 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.
- 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`.
- 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:

View file

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

View file

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

View file

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

View file

@ -22,8 +22,5 @@
"react": "^16.0.0",
"react-dom": "^16.0.0",
"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
// pass off to another action instead
self.update()
})
}, 1000)
}
function update () {

View file

@ -14,7 +14,7 @@ class Store {
this.timer = setInterval(() => {
this.lastUpdate = Date.now()
this.light = true
})
}, 1000)
}
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
cfg.entry = async () => {
const entries = await originalEntry()
entries['main.js'].unshift('./client/polyfills.js')
if (entries['main.js']) {
entries['main.js'].unshift('./client/polyfills.js')
}
return entries
}

View file

@ -11,15 +11,15 @@
"author": "",
"license": "MIT",
"dependencies": {
"express": "4.15.3",
"i18next": "8.4.2",
"i18next-browser-languagedetector": "2.0.0",
"i18next-express-middleware": "1.0.5",
"express": "4.16.2",
"i18next": "10.4.1",
"i18next-browser-languagedetector": "2.1.0",
"i18next-express-middleware": "1.0.10",
"i18next-node-fs-backend": "1.0.0",
"i18next-xhr-backend": "1.4.2",
"next": "latest",
"i18next-xhr-backend": "1.5.1",
"next": "5.0.0",
"react": "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 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()
@ -17,6 +16,7 @@ i18n
.use(Backend)
.use(i18nextMiddleware.LanguageDetector)
.init({
fallbackLng: 'en',
preload: ['en', 'de'], // preload all langages
ns: ['common', 'home', 'page2'], // need to preload all the namespaces
backend: {
@ -41,9 +41,9 @@ i18n
// use next.js
server.get('*', (req, res) => handle(req, res))
server.listen(port, (err) => {
server.listen(3000, (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.
However, this doesn't offer the full [colorful and very, very, veeeery nice
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!
## The idea behind the example

View file

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

View file

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

View file

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

View file

@ -13,7 +13,7 @@ function * runClockSaga () {
yield take(actionTypes.START_CLOCK)
while (true) {
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 => {
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 => {

View file

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

View file

@ -31,4 +31,3 @@ npm install
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",
"scripts": {
"dev": "concurrently \"tsc --pretty --watch\" \"next\"",
"prebuild": "tsc",
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "latest",
"react": "^16.1.0",
"react-dom": "^16.1.0"
"next": "^5.0.0",
"react": "^16.2.0",
"react-dom": "^16.2.0"
},
"devDependencies": {
"@types/next": "^2.4.5",
"@types/react": "^16.0.22",
"concurrently": "^3.5.0",
"tslint": "^5.8.0",
"typescript": "^2.6.1"
"@types/next": "^2.4.7",
"@types/react": "^16.0.36",
"@zeit/next-typescript": "0.0.8",
"typescript": "^2.7.1"
}
}

View file

@ -1,8 +1,26 @@
{
"compileOnSave": false,
"compilerOptions": {
"jsx": "react-native",
"module": "commonjs",
"strict": true,
"target": "es2017"
"target": "esnext",
"module": "esnext",
"jsx": "preserve",
"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
module.exports = {
webpack: function (config) {
webpack: function (config, { isServer }) {
if (ANALYZE) {
config.plugins.push(new BundleAnalyzerPlugin({
analyzerMode: 'server',
analyzerPort: 8888,
analyzerPort: isServer ? 8888 : 8889,
openAnalyzer: true
}))
}

View file

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

View file

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

View file

@ -1,6 +1,6 @@
{
"name": "next",
"version": "5.0.0",
"version": "5.0.1-canary.6",
"description": "Minimalistic framework for server-rendered React applications",
"main": "./dist/server/next.js",
"license": "MIT",
@ -90,12 +90,12 @@
"pkg-up": "2.0.0",
"prop-types": "15.6.0",
"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",
"resolve": "1.5.0",
"send": "0.16.1",
"strip-ansi": "3.0.1",
"styled-jsx": "2.2.3",
"styled-jsx": "2.2.5",
"touch": "3.1.0",
"uglifyjs-webpack-plugin": "1.1.6",
"unfetch": "3.0.0",
@ -105,6 +105,7 @@
"webpack": "3.10.0",
"webpack-dev-middleware": "1.12.0",
"webpack-hot-middleware": "2.21.0",
"webpack-sources": "1.1.0",
"write-file-webpack-plugin": "4.2.0",
"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.
- ![@zeit/next-css](https://github.com/zeit/next-plugins/tree/master/packages/next-css)
- ![@zeit/next-sass](https://github.com/zeit/next-plugins/tree/master/packages/next-sass)
- ![@zeit/next-less](https://github.com/zeit/next-plugins/tree/master/packages/next-less)
- [@zeit/next-css](https://github.com/zeit/next-plugins/tree/master/packages/next-css)
- [@zeit/next-sass](https://github.com/zeit/next-plugins/tree/master/packages/next-sass)
- [@zeit/next-less](https://github.com/zeit/next-plugins/tree/master/packages/next-less)
### Static file serving (e.g.: images)
@ -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.
#### 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
<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*
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
<p><details>
@ -1094,7 +1120,8 @@ Here's an example `.babelrc` file:
```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/)
- [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

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
// to work with Next.js SSR
export default class NextJsSsrImportPlugin {
constructor ({ dir, dist }) {
this.dir = dir
this.dist = dist
}
apply (compiler) {
compiler.plugin('compilation', (compilation) => {
compilation.mainTemplate.plugin('require-ensure', (code) => {
compilation.mainTemplate.plugin('require-ensure', (code, chunk) => {
// 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
// Node.js require system will convert it accordingly
const chunksDirPathNormalized = chunksDirPath.replace(/\\/g, '/')
let updatedCode = code.replace('require("./"', `require("${chunksDirPathNormalized}/"`)
const relativePathToBaseDirNormalized = relativePathToBaseDir.replace(/\\/g, '/')
let updatedCode = code.replace('require("./"', `require("${relativePathToBaseDirNormalized}/"`)
// Replace a promise equivalent which runs in the same loop
// 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
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}`)
}
@ -107,7 +115,7 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
externals: externalsConfig(dir, isServer),
context: dir,
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
const mainJS = require.resolve(`../../client/next${dev ? '-dev' : ''}`)
const clientConfig = !isServer ? {
@ -141,10 +149,15 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
],
alias: {
next: nextDir,
// This bypasses React's check for production mode. Since we know it is in production this way.
// This allows us to exclude React from being uglified. Saving multiple seconds per build.
react: dev ? 'react/cjs/react.development.js' : 'react/cjs/react.production.min.js',
'react-dom': dev ? 'react-dom/cjs/react-dom.development.js' : 'react-dom/cjs/react-dom.production.min.js'
// React already does something similar to this.
// But if the user has react-devtools, it'll throw an error showing that
// we haven't done dead code elimination (via uglifyjs).
// 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: {
@ -242,7 +255,7 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
!dev && new webpack.optimize.ModuleConcatenationPlugin(),
!isServer && new PagesPlugin(),
!isServer && new DynamicChunksPlugin(),
isServer && new NextJsSsrImportPlugin({ dir, dist: config.distDir }),
isServer && new NextJsSsrImportPlugin(),
!isServer && new webpack.optimize.CommonsChunkPlugin({
name: `commons`,
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.
// Otherwise, if some other page or module uses it, it might
// included in that bundle too.
if (dev && module.context && module.context.indexOf(`${sep}react${sep}`) >= 0) {
if (module.context && module.context.indexOf(`${sep}react${sep}`) >= 0) {
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
}
@ -301,7 +314,7 @@ export default async function getBaseWebpackConfig (dir, {dev = false, isServer
}
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

View file

@ -3,26 +3,28 @@ import glob from 'glob-promise'
const nextPagesDir = path.join(__dirname, '..', '..', '..', 'pages')
export async function getPages (dir, {dev, isServer}) {
const pageFiles = await getPagePaths(dir, {dev, isServer})
export async function getPages (dir, {dev, isServer, pageExtensions}) {
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
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 {
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
}
// Convert page path into single entry
export function createEntry (filePath, name) {
export function createEntry (filePath, {name, pageExtensions} = {}) {
const parsedPath = path.parse(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`
entryName = entryName.replace(/\.+(jsx|tsx|ts)/, '.js')
if (pageExtensions) {
entryName = entryName.replace(new RegExp(`\\.+(${pageExtensions})$`), '.js')
}
return {
name: path.join('bundles', entryName),
@ -42,23 +46,23 @@ export function createEntry (filePath, name) {
}
// Convert page paths into entries
export function getPageEntries (pagePaths, {isServer}) {
export function getPageEntries (pagePaths, {isServer = false, pageExtensions} = {}) {
const entries = {}
for (const filePath of pagePaths) {
const entry = createEntry(filePath)
const entry = createEntry(filePath, {pageExtensions})
entries[entry.name] = entry.files
}
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]) {
entries[errorPageEntry.name] = errorPageEntry.files
}
if (isServer) {
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]) {
entries[documentPageEntry.name] = documentPageEntry.files
}

View file

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

View file

@ -84,12 +84,12 @@ export class Head extends Component {
render () {
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)
return <head {...this.props}>
{(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' />
{this.getPreloadDynamicChunks()}
{this.getPreloadMainLinks()}
@ -173,7 +173,7 @@ export class NextScript extends Component {
render () {
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)
__NEXT_DATA__.chunks = chunks.names
@ -193,9 +193,18 @@ export class NextScript extends Component {
__NEXT_REGISTER_CHUNK = function (chunkName, 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`} />
{staticMarkup ? null : this.getDynamicChunks()}
{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
if (existsSync(join(nextDir, 'chunks'))) {
log(' copying dynamic import chunks')
@ -76,6 +85,7 @@ export default async function (dir, options, configuration) {
// Start the rendering process
const renderOpts = {
dir,
dist: config.distDir,
buildStats,
buildId,
nextExport: true,

View file

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

View file

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

View file

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

View file

@ -4,8 +4,7 @@ import { renderToString, renderToStaticMarkup } from 'react-dom/server'
import send from 'send'
import generateETag from 'etag'
import fresh from 'fresh'
import requireModule from './require'
import getConfig from './config'
import requirePage from './require'
import { Router } from '../lib/router'
import { loadGetInitialProps, isResSent } from '../lib/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 = {}) {
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, {
@ -41,6 +40,7 @@ async function doRender (req, res, pathname, query, {
hotReloader,
assetPrefix,
availableChunks,
dist,
dir = process.cwd(),
dev = false,
staticMarkup = false,
@ -48,17 +48,14 @@ async function doRender (req, res, pathname, query, {
} = {}) {
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')
let [Component, Document] = await Promise.all([
requireModule(pagePath),
requireModule(documentPath)
])
let Component = requirePage(page, {dir, dist})
let Document = require(documentPath)
Component = Component.default || Component
Document = Document.default || Document
const asPath = req.url
@ -105,7 +102,8 @@ async function doRender (req, res, pathname, query, {
const doc = createElement(Document, {
__NEXT_DATA__: {
props,
pathname,
page, // the rendered page
pathname, // the requested path
query,
buildId,
buildStats,
@ -224,8 +222,7 @@ export function serveStatic (req, res, path) {
}
async function ensurePage (page, { dir, hotReloader }) {
if (!hotReloader) return
if (page === '_error' || page === '_document') return
if (page === '/_error') return
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) {
const f = await resolve(path)
return require(f)
export function pageNotFoundError (page) {
const err = new Error(`Cannot find module for page: ${page}`)
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()
})
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 () => {
const browser = await webdriver(context.appPort, '/nested-cdm/')
const browser = await webdriver(context.appPort, '/nested-cdm')
const text = await browser.elementByCss('p').text()
expect(text).toBe('ComponentDidMount executed on client.')
@ -426,7 +418,7 @@ export default (context, render) => {
describe('with asPath', () => {
describe('inside getInitialProps', () => {
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
.elementByCss('#as-path-link').click()
.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 () => {
const browser = await webdriver(context.appPort, '/nav/')
const browser = await webdriver(context.appPort, '/nav')
const asPath = await browser
.elementByCss('#as-path-link-no-as').click()
.waitForElementByCss('.as-path-content')
@ -450,7 +442,7 @@ export default (context, render) => {
describe('with next/router', () => {
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
.elementByCss('#as-path-using-router-link').click()
.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')
})
test('error 404', async () => {
const $ = await get$('/non-existent')
expect($('h1').text()).toBe('404')
expect($('h2').text()).toBe('This page could not be found.')
describe('404', () => {
it('should 404 on not existent page', async () => {
const $ = await get$('/non-existent')
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', () => {

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,
startApp,
stopApp,
renderViaHTTP
renderViaHTTP,
waitFor
} from 'next-test-utils'
import webdriver from 'next-webdriver'
import fetch from 'node-fetch'
@ -93,6 +94,55 @@ describe('Production Usage', () => {
const data = await renderViaHTTP(appPort, '/static/data/item.txt')
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', () => {
@ -112,6 +162,26 @@ describe('Production Usage', () => {
await app.render(req, res, req.url)
expect(headers['X-Powered-By']).toEqual(`Next.js ${pkg.version}`)
})
it('should not set it when poweredByHeader==false', async () => {
const req = { url: '/stateless', headers: {} }
const originalConfigValue = app.config.poweredByHeader
app.config.poweredByHeader = false
const res = {
getHeader () {
return false
},
setHeader (key, value) {
if (key === '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))

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"
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"
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:
version "2.0.0"
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"
prop-types "^15.6.0"
react-hot-loader@4.0.0-beta.18:
version "4.0.0-beta.18"
resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.0.0-beta.18.tgz#5a3d1b5bd813633380b88c0c660019dbf638975d"
react-hot-loader@4.0.0-beta.23:
version "4.0.0-beta.23"
resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.0.0-beta.23.tgz#dced27e8e2400adb1690eb5b3a0a39b4e0b90615"
dependencies:
fast-levenshtein "^2.0.6"
global "^4.3.0"
hoist-non-react-statics "^2.3.1"
react-stand-in "^4.0.0-beta.18"
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:
hoist-non-react-statics "^2.5.0"
prop-types "^15.6.0"
shallowequal "^1.0.2"
react@16.2.0:
@ -6361,25 +6360,25 @@ style-loader@^0.19.1:
loader-utils "^1.0.2"
schema-utils "^0.3.0"
styled-jsx@2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-2.2.3.tgz#9fe3df55c852388019c3bf4c026bcbf8b3e003de"
styled-jsx@2.2.5:
version "2.2.5"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-2.2.5.tgz#3eb98418720655421bfc1a680e2add581726a5e6"
dependencies:
babel-plugin-syntax-jsx "6.18.0"
babel-types "6.26.0"
convert-source-map "1.5.1"
source-map "0.6.1"
string-hash "1.1.3"
stylis "3.4.5"
stylis-rule-sheet "0.0.7"
stylis "3.4.10"
stylis-rule-sheet "0.0.8"
stylis-rule-sheet@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.7.tgz#5c51dc879141a61821c2094ba91d2cbcf2469c6c"
stylis-rule-sheet@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.8.tgz#b0d0a126c945b1f3047447a3aae0647013e8d166"
stylis@3.4.5:
version "3.4.5"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.4.5.tgz#d7b9595fc18e7b9c8775eca8270a9a1d3e59806e"
stylis@3.4.10:
version "3.4.10"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.4.10.tgz#a135cab4b9ff208e327fbb5a6fde3fa991c638ee"
supports-color@^2.0.0:
version "2.0.0"
@ -6881,7 +6880,7 @@ webpack-hot-middleware@2.21.0:
querystring "^0.2.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"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54"
dependencies: