Skip to content

Commit

Permalink
Programmatic API (#310)
Browse files Browse the repository at this point in the history
* add 'next' api

* add render APIs

* add 'as' prop to Link

* check Accept header to serve json response

* check if response was finished on getInitialProps call

* move server/app to server/index

* load webpack-hot-middleware-client by absolute path

* server: options for testing

* add tests

* example: improve

* server: make dir optional

* fix client routing

* add parameterized routing example

* link: fix display url

* Add custom-server-express example (#352)

* Add custom-server-express example

* Remove extraneous nexts in express routes defs

* Update next config in server.js

* Handle accept headers totally inside Next.js (#385)

* Handle accept headers totally inside Next.js
Now user doesn't need to handle it anymore.

* Move json pages serving to /_next/pages base path.

* Join paths correctly.

* remove next/render
  • Loading branch information
nkzawa authored and rauchg committed Dec 16, 2016
1 parent 289feed commit 1708222
Show file tree
Hide file tree
Showing 28 changed files with 440 additions and 183 deletions.
7 changes: 2 additions & 5 deletions bin/next-dev
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { resolve, join } from 'path'
import parseArgs from 'minimist'
import { exists } from 'mz/fs'
import Server from '../server'
import clean from '../server/build/clean'

const argv = parseArgs(process.argv.slice(2), {
alias: {
Expand All @@ -19,11 +18,9 @@ const argv = parseArgs(process.argv.slice(2), {

const dir = resolve(argv._[0] || '.')

clean(dir)
const srv = new Server({ dir, dev: true })
srv.start(argv.port)
.then(async () => {
const srv = new Server({ dir, dev: true, hotReload: true })
await srv.start(argv.port)

if (!process.env.NOW) {
console.log(`> Ready on http://localhost:${argv.port}`)
}
Expand Down
12 changes: 10 additions & 2 deletions client/next.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@ import App from '../lib/app'
import evalScript, { requireModule } from '../lib/eval-script'

const {
__NEXT_DATA__: { component, errorComponent, props, ids, err }
__NEXT_DATA__: {
component,
errorComponent,
props,
ids,
err,
pathname,
query
}
} = window

domready(() => {
const Component = evalScript(component).default
const ErrorComponent = evalScript(errorComponent).default

const router = new Router(window.location.href, {
const router = new Router(pathname, query, {
Component,
ErrorComponent,
ctx: { err }
Expand Down
10 changes: 10 additions & 0 deletions examples/custom-server-express/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"scripts": {
"start": "node server.js"
},
"dependencies": {
"accepts": "1.3.3",
"express": "^4.14.0",
"next": "*"
}
}
3 changes: 3 additions & 0 deletions examples/custom-server-express/pages/a.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import React from 'react'

export default () => <div>a</div>
3 changes: 3 additions & 0 deletions examples/custom-server-express/pages/b.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import React from 'react'

export default () => <div>b</div>
9 changes: 9 additions & 0 deletions examples/custom-server-express/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'
import Link from 'next/link'

export default () => (
<ul>
<li><Link href='/b' as='/a'><a>a</a></Link></li>
<li><Link href='/a' as='/b'><a>b</a></Link></li>
</ul>
)
27 changes: 27 additions & 0 deletions examples/custom-server-express/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const express = require('express')
const next = require('next')

const app = next({ dir: '.', dev: true })
const handle = app.getRequestHandler()

app.prepare()
.then(() => {
const server = express()

server.get('/a', (req, res) => {
return app.render(req, res, '/b', req.query)
})

server.get('/b', (req, res) => {
return app.render(req, res, '/a', req.query)
})

server.get('*', (req, res) => {
return handle(req, res)
})

server.listen(3000, (err) => {
if (err) throw err
console.log('> Ready on http://localhost:3000')
})
})
9 changes: 9 additions & 0 deletions examples/custom-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"scripts": {
"start": "node server.js"
},
"dependencies": {
"accepts": "1.3.3",
"next": "*"
}
}
3 changes: 3 additions & 0 deletions examples/custom-server/pages/a.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import React from 'react'

export default () => <div>a</div>
3 changes: 3 additions & 0 deletions examples/custom-server/pages/b.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import React from 'react'

export default () => <div>b</div>
9 changes: 9 additions & 0 deletions examples/custom-server/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'
import Link from 'next/link'

export default () => (
<ul>
<li><Link href='/b' as='/a'><a>a</a></Link></li>
<li><Link href='/a' as='/b'><a>b</a></Link></li>
</ul>
)
25 changes: 25 additions & 0 deletions examples/custom-server/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')

const app = next({ dev: true })
const handle = app.getRequestHandler()

app.prepare()
.then(() => {
createServer((req, res) => {
const { pathname, query } = parse(req.url, true)

if (pathname === '/a') {
app.render(req, res, '/b', query)
} else if (pathname === '/b') {
app.render(req, res, '/a', query)
} else {
handle(req, res)
}
})
.listen(3000, (err) => {
if (err) throw err
console.log('> Ready on http://localhost:3000')
})
})
10 changes: 10 additions & 0 deletions examples/parameterized-routing/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"scripts": {
"start": "node server.js"
},
"dependencies": {
"accepts": "1.3.3",
"next": "*",
"path-match": "1.2.4"
}
}
17 changes: 17 additions & 0 deletions examples/parameterized-routing/pages/blog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React, { Component } from 'react'

export default class extends Component {
static getInitialProps ({ query: { id } }) {
return { id }
}

render () {
return <div>
<h1>My {this.props.id} blog post</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua.
</p>
</div>
}
}
10 changes: 10 additions & 0 deletions examples/parameterized-routing/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react'
import Link from 'next/link'

export default () => (
<ul>
<li><Link href='/blog?id=first' as='/blog/first'><a>My first blog post</a></Link></li>
<li><Link href='/blog?id=second' as='/blog/second'><a>My second blog post</a></Link></li>
<li><Link href='/blog?id=last' as='/blog/last'><a>My last blog post</a></Link></li>
</ul>
)
27 changes: 27 additions & 0 deletions examples/parameterized-routing/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')
const pathMatch = require('path-match')

const app = next({ dev: true })
const handle = app.getRequestHandler()
const route = pathMatch()
const match = route('/blog/:id')

app.prepare()
.then(() => {
createServer((req, res) => {
const { pathname } = parse(req.url)
const params = match(pathname)
if (params === false) {
handle(req, res)
return
}

app.render(req, res, '/blog', params)
})
.listen(3000, (err) => {
if (err) throw err
console.log('> Ready on http://localhost:3000')
})
})
9 changes: 6 additions & 3 deletions lib/link.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default class Link extends Component {
return
}

const { href, scroll } = this.props
const { href, scroll, as } = this.props

if (!isLocal(href)) {
// ignore click if it's outside our scope
Expand All @@ -26,8 +26,11 @@ export default class Link extends Component {

e.preventDefault()

const route = as ? href : null
const url = as || href

// straight up redirect
this.context.router.push(null, href)
this.context.router.push(route, url)
.then((success) => {
if (!success) return
if (scroll !== false) window.scrollTo(0, 0)
Expand All @@ -48,7 +51,7 @@ export default class Link extends Component {
// if child does not specify a href, specify it
// so that repetition is not needed by the user
if (!isAnchor || !('href' in child.props)) {
props.href = this.props.href
props.href = this.props.as || this.props.href
}

if (isAnchor) {
Expand Down
25 changes: 9 additions & 16 deletions lib/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,16 @@ import evalScript from './eval-script'
import shallowEquals from './shallow-equals'

export default class Router {
constructor (url, { Component, ErrorComponent, ctx } = {}) {
const parsed = parse(url, true)

constructor (pathname, query, { Component, ErrorComponent, ctx } = {}) {
// represents the current component key
this.route = toRoute(parsed.pathname)
this.route = toRoute(pathname)

// set up the component cache (by route keys)
this.components = { [this.route]: { Component, ctx } }

this.ErrorComponent = ErrorComponent
this.pathname = parsed.pathname
this.query = parsed.query
this.pathname = pathname
this.query = query
this.subscriptions = new Set()
this.componentLoadCancel = null
this.onPopState = this.onPopState.bind(this)
Expand Down Expand Up @@ -108,7 +106,7 @@ export default class Router {
}

async change (method, route, url) {
const { pathname, query } = parse(url, true)
const { pathname, query } = parse(route || url, true)

if (!route) route = toRoute(pathname)

Expand Down Expand Up @@ -158,20 +156,18 @@ export default class Router {
return this.pathname !== pathname || !shallowEquals(query, this.query)
}

async fetchComponent (url) {
const route = toRoute(parse(url).pathname)

async fetchComponent (route) {
let data = this.components[route]
if (!data) {
let cancel

const componentUrl = toJSONUrl(route)
data = await new Promise((resolve, reject) => {
this.componentLoadCancel = cancel = () => {
if (xhr.abort) xhr.abort()
}

const xhr = loadComponent(componentUrl, (err, data) => {
const url = `/_next/pages${route}`
const xhr = loadComponent(url, (err, data) => {
if (err) return reject(err)
resolve({
Component: data.Component,
Expand Down Expand Up @@ -234,10 +230,6 @@ function toRoute (path) {
return path.replace(/\/$/, '') || '/'
}

function toJSONUrl (route) {
return (route === '/' ? '/index' : route) + '.json'
}

function loadComponent (url, fn) {
return loadJSON(url, (err, data) => {
if (err) return fn(err)
Expand Down Expand Up @@ -277,6 +269,7 @@ function loadJSON (url, fn) {
fn(err)
}
xhr.open('GET', url)
xhr.setRequestHeader('Accept', 'application/json')
xhr.send()

return xhr
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "next",
"version": "1.2.3",
"description": "Minimalistic framework for server-rendered React applications",
"main": "./dist/lib/index.js",
"main": "./dist/server/next.js",
"license": "MIT",
"repository": "zeit/next.js",
"files": [
Expand Down Expand Up @@ -31,6 +31,7 @@
"parser": "babel-eslint"
},
"dependencies": {
"accepts": "1.3.3",
"ansi-html": "0.0.6",
"babel-core": "6.20.0",
"babel-generator": "6.20.0",
Expand Down
8 changes: 5 additions & 3 deletions server/build/plugins/watch-pages-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export default class WatchPagesPlugin {
return join('bundles', relative(compiler.options.context, f))
}
const errorPageName = join('bundles', 'pages', '_error.js')
const errorPagePath = join(__dirname, '..', '..', '..', 'pages', '_error.js')
const hotMiddlewareClientPath = join(__dirname, '..', '..', '..', 'client/webpack-hot-middleware-client')

compiler.plugin('watch-run', (watching, callback) => {
Object.keys(compiler.fileTimestamps)
Expand All @@ -35,7 +37,7 @@ export default class WatchPagesPlugin {

if (compiler.hasEntry(name)) return

const entries = ['next/dist/client/webpack-hot-middleware-client', f]
const entries = [hotMiddlewareClientPath, f]
compiler.addEntry(entries, name)
})

Expand All @@ -47,8 +49,8 @@ export default class WatchPagesPlugin {

if (name === errorPageName) {
compiler.addEntry([
'next/dist/client/webpack-hot-middleware-client',
join(__dirname, '..', '..', '..', 'pages', '_error.js')
hotMiddlewareClientPath,
errorPagePath
], name)
}
})
Expand Down
Loading

2 comments on commit 1708222

@bcoe
Copy link
Contributor

@bcoe bcoe commented on 1708222 Dec 17, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nkzawa @rauchg love this new API, it shoudl make writing tests for next apps way easier. I'd love to start playing with this at npm without installing from git, don't suppose I could convince you to release the update to a beta or next tag on npm?

@rauchg
Copy link
Member

@rauchg rauchg commented on 1708222 Dec 17, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bcoe we'll do that very shortly

Please sign in to comment.