mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
initial source
This commit is contained in:
parent
404bee1215
commit
9b06a22f31
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
*.log
|
||||
node_modules
|
||||
dist
|
31
bin/next
Executable file
31
bin/next
Executable file
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { resolve } from 'path'
|
||||
import parseArgs from 'minimist'
|
||||
import { spawn } from 'cross-spawn';
|
||||
|
||||
const defaultCommand = 'dev'
|
||||
const commands = new Set([
|
||||
defaultCommand,
|
||||
'build',
|
||||
'start'
|
||||
])
|
||||
|
||||
let cmd = process.argv[2]
|
||||
let args
|
||||
|
||||
if (commands.has(cmd)) {
|
||||
args = process.argv.slice(3)
|
||||
} else {
|
||||
cmd = defaultCommand
|
||||
args = process.argv.slice(2)
|
||||
}
|
||||
|
||||
const bin = resolve(__dirname, 'next-' + cmd)
|
||||
|
||||
const proc = spawn(bin, args, { stdio: 'inherit', customFds: [0, 1, 2] })
|
||||
proc.on('close', (code) => process.exit(code))
|
||||
proc.on('error', (err) => {
|
||||
console.log(err)
|
||||
process.exit(1)
|
||||
})
|
44
bin/next-build
Executable file
44
bin/next-build
Executable file
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { resolve, dirname } from 'path'
|
||||
import parseArgs from 'minimist'
|
||||
import fs from 'mz/fs'
|
||||
import mkdirp from 'mkdirp-then';
|
||||
import glob from 'glob-promise'
|
||||
import { transpile, bundle } from '../server/build'
|
||||
|
||||
const argv = parseArgs(process.argv.slice(2), {
|
||||
alias: {
|
||||
h: 'help',
|
||||
},
|
||||
boolean: ['h']
|
||||
})
|
||||
|
||||
const dir = resolve(argv._[0] || '.')
|
||||
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
const paths = await glob('**/*.js', { cwd: dir, ignore: 'node_modules/**' })
|
||||
await Promise.all(paths.map(async (p) => {
|
||||
const code = await transpile(resolve(dir, p))
|
||||
const outpath = resolve(dir, '.next', p)
|
||||
await writeFile(outpath, code)
|
||||
}))
|
||||
|
||||
const pagePaths = await glob('.next/pages/**/*.js', { cwd: dir })
|
||||
await Promise.all(pagePaths.map(async (p) => {
|
||||
const code = await bundle(resolve(dir, p))
|
||||
const outpath = resolve(dir, '.next', p)
|
||||
await writeFile(outpath, code)
|
||||
}))
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
exit(1)
|
||||
})
|
||||
|
||||
async function writeFile (path, data) {
|
||||
await mkdirp(dirname(path))
|
||||
await fs.writeFile(path, data, { encoding: 'utf8', flag: 'w+' })
|
||||
}
|
||||
|
27
bin/next-start
Executable file
27
bin/next-start
Executable file
|
@ -0,0 +1,27 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import parseArgs from 'minimist'
|
||||
import Server from '../server'
|
||||
|
||||
const argv = parseArgs(process.argv.slice(2), {
|
||||
alias: {
|
||||
h: 'help',
|
||||
p: 'port'
|
||||
},
|
||||
boolean: ['h'],
|
||||
default: {
|
||||
p: 3000
|
||||
}
|
||||
})
|
||||
|
||||
const dir = argv._[0] || '.'
|
||||
|
||||
const srv = new Server(dir)
|
||||
srv.start(argv.port)
|
||||
.then(() => {
|
||||
console.log('> Ready on http://localhost:%d', argv.port);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
exit(1)
|
||||
})
|
31
client/eval-script.js
Normal file
31
client/eval-script.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import App from '../lib/app'
|
||||
|
||||
const modules = new Map([
|
||||
['react', React],
|
||||
['react-dom', ReactDOM],
|
||||
['next/app', App]
|
||||
])
|
||||
|
||||
/**
|
||||
* IMPORTANT: This module is compiled *without* `use strict`
|
||||
* so that when we `eval` a dependency below, we don't enforce
|
||||
* `use strict` implicitly.
|
||||
*
|
||||
* Otherwise, modules like `d3` get `eval`d and forced into
|
||||
* `use strict` where they don't work (at least in current versions)
|
||||
*
|
||||
* To see the compilation details, look at `gulpfile.js` and the
|
||||
* usage of `babel-plugin-transform-remove-strict-mode`.
|
||||
*/
|
||||
|
||||
export default function evalScript (script) {
|
||||
const module = { exports: {} }
|
||||
const require = function (path) { // eslint-disable-line no-unused-vars
|
||||
return modules.get(path)
|
||||
}
|
||||
// don't use Function() here since it changes source locations
|
||||
eval(script) // eslint-disable-line no-eval
|
||||
return module.exports
|
||||
}
|
18
client/next.js
Normal file
18
client/next.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { createElement } from 'react'
|
||||
import { render } from 'react-dom'
|
||||
import evalScript from './eval-script'
|
||||
import Router from './router'
|
||||
import DefaultApp from '../lib/app'
|
||||
|
||||
const {
|
||||
__NEXT_DATA__: { app, component, props }
|
||||
} = window
|
||||
|
||||
const App = app ? evalScript(app).default : DefaultApp
|
||||
const Component = evalScript(component).default
|
||||
|
||||
const router = new Router({ Component, props })
|
||||
const container = document.getElementById('__next')
|
||||
const appProps = { Component, props, router: {} }
|
||||
|
||||
render(createElement(App, { ...appProps }), container)
|
259
client/router.js
Normal file
259
client/router.js
Normal file
|
@ -0,0 +1,259 @@
|
|||
import { parse } from 'url'
|
||||
import evalScript from './eval-script'
|
||||
import shallowEquals from './shallow-equals'
|
||||
|
||||
export default class Router {
|
||||
constructor (initialData) {
|
||||
this.subscriptions = []
|
||||
|
||||
const { Component } = initialData
|
||||
const { pathname } = location
|
||||
const route = toRoute(pathname)
|
||||
|
||||
this.currentRoute = route
|
||||
this.currentComponent = Component.displayName
|
||||
this.currentComponentData = initialData
|
||||
|
||||
// set up the component cache (by route keys)
|
||||
this.components = { [route]: initialData }
|
||||
|
||||
// in order for `e.state` to work on the `onpopstate` event
|
||||
// we have to register the initial route upon initialization
|
||||
const url = pathname + (location.search || '') + (location.hash || '')
|
||||
this.replace(Component, url)
|
||||
|
||||
this.onPopState = this.onPopState.bind(this)
|
||||
window.addEventListener('unload', () => {})
|
||||
window.addEventListener('popstate', this.onPopState)
|
||||
}
|
||||
|
||||
onPopState (e) {
|
||||
this.abortComponentLoad()
|
||||
const cur = this.currentComponent
|
||||
const pathname = location.pathname
|
||||
const url = pathname + (location.search || '') + (location.hash || '')
|
||||
const { fromComponent, route } = e.state || {}
|
||||
if (fromComponent && cur && fromComponent === cur) {
|
||||
// if the component has not changed due
|
||||
// to the url change, it means we only
|
||||
// need to notify the subscriber about
|
||||
// the URL change
|
||||
this.set(url)
|
||||
} else {
|
||||
this.fetchComponent(route || url, (err, data) => {
|
||||
if (err) {
|
||||
// the only way we can appropriately handle
|
||||
// this failure is deferring to the browser
|
||||
// since the URL has already changed
|
||||
location.reload()
|
||||
} else {
|
||||
this.currentRoute = route || toRoute(pathname)
|
||||
this.currentComponent = data.Component.displayName
|
||||
this.currentComponentData = data
|
||||
this.set(url)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
update (route, data) {
|
||||
data.Component = evalScript(data.component).default
|
||||
delete data.component
|
||||
this.components[route] = data
|
||||
if (route === this.currentRoute) {
|
||||
let cancelled = false
|
||||
const cancel = () => { cancelled = true }
|
||||
this.componentLoadCancel = cancel
|
||||
getInitialProps(data, (err, dataWithProps) => {
|
||||
if (cancel === this.componentLoadCancel) {
|
||||
this.componentLoadCancel = false
|
||||
}
|
||||
if (cancelled) return
|
||||
if (err) throw err
|
||||
this.currentComponentData = dataWithProps
|
||||
this.notify()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
goTo (url, fn) {
|
||||
this.change('pushState', null, url, fn)
|
||||
}
|
||||
|
||||
back () {
|
||||
history.back()
|
||||
}
|
||||
|
||||
push (fromComponent, url, fn) {
|
||||
this.change('pushState', fromComponent, url, fn)
|
||||
}
|
||||
|
||||
replace (fromComponent, url, fn) {
|
||||
this.change('replaceState', fromComponent, url, fn)
|
||||
}
|
||||
|
||||
change (method, component, url, fn) {
|
||||
this.abortComponentLoad()
|
||||
|
||||
const set = (name) => {
|
||||
this.currentComponent = name
|
||||
const state = name
|
||||
? { fromComponent: name, route: this.currentRoute }
|
||||
: {}
|
||||
history[method](state, null, url)
|
||||
this.set(url)
|
||||
if (fn) fn(null)
|
||||
}
|
||||
|
||||
const componentName = component && component.displayName
|
||||
if (component && !componentName) {
|
||||
throw new Error('Initial component must have a unique `displayName`')
|
||||
}
|
||||
|
||||
if (this.currentComponent &&
|
||||
componentName !== this.currentComponent) {
|
||||
this.fetchComponent(url, (err, data) => {
|
||||
if (!err) {
|
||||
this.currentRoute = toRoute(url)
|
||||
this.currentComponentData = data
|
||||
set(data.Component.displayName)
|
||||
}
|
||||
if (fn) fn(err, data)
|
||||
})
|
||||
} else {
|
||||
set(componentName)
|
||||
}
|
||||
}
|
||||
|
||||
set (url) {
|
||||
const parsed = parse(url, true)
|
||||
if (this.urlIsNew(parsed)) {
|
||||
this.pathname = parsed.pathname
|
||||
this.query = parsed.query
|
||||
this.notify()
|
||||
}
|
||||
}
|
||||
|
||||
urlIsNew ({ pathname, query }) {
|
||||
return this.pathname !== pathname || !shallowEquals(query, this.query)
|
||||
}
|
||||
|
||||
fetchComponent (url, fn) {
|
||||
const pathname = parse(url, true)
|
||||
const route = toRoute(pathname)
|
||||
|
||||
let cancelled = false
|
||||
let componentXHR = null
|
||||
const cancel = () => {
|
||||
cancelled = true
|
||||
|
||||
if (componentXHR && componentXHR.abort) {
|
||||
componentXHR.abort()
|
||||
}
|
||||
}
|
||||
|
||||
if (this.components[route]) {
|
||||
const data = this.components[route]
|
||||
getInitialProps(data, (err, dataWithProps) => {
|
||||
if (cancel === this.componentLoadCancel) {
|
||||
this.componentLoadCancel = false
|
||||
}
|
||||
if (cancelled) return
|
||||
fn(err, dataWithProps)
|
||||
})
|
||||
this.componentLoadCancel = cancel
|
||||
return
|
||||
}
|
||||
|
||||
const componentUrl = toJSONUrl(route)
|
||||
|
||||
componentXHR = loadComponent(componentUrl, (err, data) => {
|
||||
if (cancel === this.componentLoadCancel) {
|
||||
this.componentLoadCancel = false
|
||||
}
|
||||
if (err) {
|
||||
if (!cancelled) fn(err)
|
||||
} else {
|
||||
// we update the cache even if cancelled
|
||||
if (!this.components[route]) {
|
||||
this.components[route] = data
|
||||
}
|
||||
if (!cancelled) fn(null, data)
|
||||
}
|
||||
})
|
||||
|
||||
this.componentLoadCancel = cancel
|
||||
}
|
||||
|
||||
abortComponentLoad () {
|
||||
if (this.componentLoadCancel) {
|
||||
this.componentLoadCancel()
|
||||
this.componentLoadCancel = null
|
||||
}
|
||||
}
|
||||
|
||||
notify () {
|
||||
this.subscriptions.forEach(fn => fn())
|
||||
}
|
||||
|
||||
subscribe (fn) {
|
||||
this.subscriptions.push(fn)
|
||||
return () => {
|
||||
const i = this.subscriptions.indexOf(fn)
|
||||
if (~i) this.subscriptions.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// every route finishing in `/test/` becomes `/test`
|
||||
|
||||
export function toRoute (path) {
|
||||
return path.replace(/\/$/, '') || '/'
|
||||
}
|
||||
|
||||
export function toJSONUrl (route) {
|
||||
return ('/' === route ? '/index' : route) + '.json'
|
||||
}
|
||||
|
||||
export function loadComponent (url, fn) {
|
||||
return loadJSON(url, (err, data) => {
|
||||
if (err && fn) fn(err)
|
||||
const { component, props } = data
|
||||
const Component = evalScript(component).default
|
||||
getInitialProps({ Component, props }, fn)
|
||||
})
|
||||
}
|
||||
|
||||
function loadJSON (url, fn) {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.onload = () => {
|
||||
let data
|
||||
|
||||
try {
|
||||
data = JSON.parse(xhr.responseText)
|
||||
} catch (err) {
|
||||
fn(new Error('Failed to load JSON for ' + url))
|
||||
return
|
||||
}
|
||||
|
||||
fn(null, data)
|
||||
}
|
||||
xhr.onerror = () => {
|
||||
if (fn) fn(new Error('XHR failed. Status: ' + xhr.status))
|
||||
}
|
||||
xhr.open('GET', url)
|
||||
xhr.send()
|
||||
|
||||
return xhr
|
||||
}
|
||||
|
||||
function getInitialProps (data, fn) {
|
||||
const { Component: { getInitialProps } } = data
|
||||
if (getInitialProps) {
|
||||
Promise.resolve(getInitialProps({}))
|
||||
.then((props) => fn(null, { ...data, props }))
|
||||
.catch(fn)
|
||||
} else {
|
||||
fn(null, data)
|
||||
}
|
||||
}
|
11
client/shallow-equals.js
Normal file
11
client/shallow-equals.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
export default function shallowEquals (a, b) {
|
||||
for (const i in a) {
|
||||
if (b[i] !== a[i]) return false;
|
||||
}
|
||||
|
||||
for (const i in b) {
|
||||
if (b[i] !== a[i]) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
156
gulpfile.js
Normal file
156
gulpfile.js
Normal file
|
@ -0,0 +1,156 @@
|
|||
const gulp = require('gulp')
|
||||
const babel = require('gulp-babel')
|
||||
const cache = require('gulp-cached')
|
||||
const notify_ = require('gulp-notify')
|
||||
const uglify = require('gulp-uglify')
|
||||
const webpack = require('webpack-stream')
|
||||
const del = require('del')
|
||||
|
||||
const babelOptions = {
|
||||
presets: ['es2015', 'react'],
|
||||
plugins: [
|
||||
'transform-async-to-generator',
|
||||
'transform-object-rest-spread',
|
||||
'transform-class-properties',
|
||||
'transform-runtime'
|
||||
]
|
||||
}
|
||||
|
||||
gulp.task('compile', [
|
||||
'compile-bin',
|
||||
'compile-lib',
|
||||
'compile-server',
|
||||
'compile-client'
|
||||
])
|
||||
|
||||
gulp.task('compile-bin', () => {
|
||||
return gulp.src('bin/*')
|
||||
.pipe(cache('bin'))
|
||||
.pipe(babel(babelOptions))
|
||||
.pipe(gulp.dest('dist/bin'))
|
||||
.pipe(notify('Compiled binaries'))
|
||||
})
|
||||
|
||||
gulp.task('compile-lib', () => {
|
||||
return gulp.src('lib/**/*.js')
|
||||
.pipe(cache('lib'))
|
||||
.pipe(babel(babelOptions))
|
||||
.pipe(gulp.dest('dist/lib'))
|
||||
.pipe(notify('Compiled lib files'))
|
||||
})
|
||||
|
||||
gulp.task('compile-server', () => {
|
||||
return gulp.src('server/**/*.js')
|
||||
.pipe(cache('server'))
|
||||
.pipe(babel(babelOptions))
|
||||
.pipe(gulp.dest('dist/server'))
|
||||
.pipe(notify('Compiled server files'))
|
||||
})
|
||||
|
||||
gulp.task('compile-client', () => {
|
||||
return gulp.src('client/**/*.js')
|
||||
.pipe(cache('client'))
|
||||
.pipe(babel(babelOptions))
|
||||
.pipe(gulp.dest('dist/client'))
|
||||
.pipe(notify('Compiled client files'))
|
||||
})
|
||||
|
||||
gulp.task('build', [
|
||||
'build-dev-client',
|
||||
])
|
||||
|
||||
gulp.task('build-release', [
|
||||
'build-release-client'
|
||||
])
|
||||
|
||||
gulp.task('build-dev-client', ['compile-lib', 'compile-client'], () => {
|
||||
return gulp
|
||||
.src('dist/client/next-dev.js')
|
||||
.pipe(webpack({
|
||||
quiet: true,
|
||||
output: { filename: 'next-dev.bundle.js' }
|
||||
}))
|
||||
.pipe(gulp.dest('dist/client'))
|
||||
.pipe(notify('Built dev client'))
|
||||
})
|
||||
|
||||
gulp.task('build-release-client', ['compile-lib', 'compile-client'], () => {
|
||||
return gulp
|
||||
.src('dist/client/next.js')
|
||||
.pipe(webpack({
|
||||
quiet: true,
|
||||
output: { filename: 'next.bundle.js' },
|
||||
plugins: [
|
||||
new webpack.webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: JSON.stringify('production')
|
||||
}
|
||||
})
|
||||
]
|
||||
}))
|
||||
.pipe(uglify({ preserveComments: 'license' }))
|
||||
.pipe(gulp.dest('dist/client'))
|
||||
.pipe(notify('Built release client'))
|
||||
})
|
||||
|
||||
gulp.task('watch', [
|
||||
'watch-bin',
|
||||
'watch-lib',
|
||||
'watch-server',
|
||||
'watch-client'
|
||||
])
|
||||
|
||||
gulp.task('watch-bin', function () {
|
||||
return gulp.watch('bin/*', [
|
||||
'compile-bin'
|
||||
])
|
||||
})
|
||||
|
||||
gulp.task('watch-lib', function () {
|
||||
return gulp.watch('lib/**/*.js', [
|
||||
'compile-lib',
|
||||
'build-dev-client'
|
||||
])
|
||||
})
|
||||
|
||||
gulp.task('watch-server', function () {
|
||||
return gulp.watch('server/**/*.js', [
|
||||
'compile-server'
|
||||
])
|
||||
})
|
||||
|
||||
gulp.task('watch-client', function () {
|
||||
return gulp.watch('client/**/*.js', [
|
||||
'compile-client',
|
||||
'build-dev-client'
|
||||
])
|
||||
})
|
||||
|
||||
gulp.task('clean', () => {
|
||||
return del(['dist'])
|
||||
})
|
||||
|
||||
gulp.task('default', [
|
||||
'compile',
|
||||
'build',
|
||||
'watch'
|
||||
])
|
||||
|
||||
gulp.task('release', [
|
||||
'compile',
|
||||
'build-release'
|
||||
])
|
||||
|
||||
// avoid logging to the console
|
||||
// that we created a notification
|
||||
notify_.logLevel(0)
|
||||
|
||||
// notification helper
|
||||
function notify (msg) {
|
||||
return notify_({
|
||||
title: '▲ Next',
|
||||
message: msg,
|
||||
icon: false,
|
||||
onLast: true
|
||||
})
|
||||
}
|
71
lib/app.js
Normal file
71
lib/app.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
import React, { Component, PropTypes } from 'react'
|
||||
|
||||
export default class App extends Component {
|
||||
static childContextTypes = {
|
||||
router: PropTypes.object,
|
||||
headManager: PropTypes.object
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = propsToState(props)
|
||||
this.close = null
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
const state = propsToState(nextProps)
|
||||
try {
|
||||
this.setState(state)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { router } = this.props
|
||||
|
||||
this.close = router.subscribe(() => {
|
||||
const state = propsToState({
|
||||
router,
|
||||
data: router.currentComponentData
|
||||
})
|
||||
|
||||
try {
|
||||
this.setState(state)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.close) this.close()
|
||||
}
|
||||
|
||||
getChildContext () {
|
||||
const { router, headManager } = this.props
|
||||
return { router, headManager }
|
||||
}
|
||||
|
||||
render () {
|
||||
const { Component, props } = this.state
|
||||
return React.createElement(Component, { ...props })
|
||||
}
|
||||
}
|
||||
|
||||
function propsToState (props) {
|
||||
const { Component, router } = props
|
||||
const url = {
|
||||
query: router.query,
|
||||
pathname: router.pathname,
|
||||
back: () => router.back(),
|
||||
goTo: (url, fn) => router.goTo(url, fn),
|
||||
push: (url, fn) => router.push(Component, url, fn),
|
||||
replace: (url, fn) => router.replace(Component, url, fn)
|
||||
}
|
||||
|
||||
return {
|
||||
Component,
|
||||
props: { ...props.props, url }
|
||||
}
|
||||
}
|
59
lib/document.js
Normal file
59
lib/document.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
import React, { Component, PropTypes } from 'react'
|
||||
import htmlescape from 'htmlescape'
|
||||
|
||||
export default class Document extends Component {
|
||||
static childContextTypes = {
|
||||
_documentProps: PropTypes.any
|
||||
}
|
||||
|
||||
getChildContext () {
|
||||
return {
|
||||
_documentProps: this.props
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return <html>
|
||||
<Head/>
|
||||
<body>
|
||||
<Main/>
|
||||
<DevTools/>
|
||||
<NextScript/>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}
|
||||
|
||||
export function Head (props, context) {
|
||||
const { head } = context._documentProps
|
||||
const h = (head || [])
|
||||
.map((h, i) => React.cloneElement(h, { key: '_next' + i }))
|
||||
return <head>{h}</head>
|
||||
}
|
||||
|
||||
Head.contextTypes = { _documentProps: PropTypes.any }
|
||||
|
||||
export function Main (props, context) {
|
||||
const { html, data } = context._documentProps;
|
||||
return <div>
|
||||
<div id='__next' dangerouslySetInnerHTML={{ __html: html }} />
|
||||
<script dangerouslySetInnerHTML={{ __html: '__NEXT_DATA__ = ' + htmlescape(data) }}></script>
|
||||
</div>
|
||||
}
|
||||
|
||||
Main.contextTypes = { _documentProps: PropTypes.any }
|
||||
|
||||
export function DevTools (props, context) {
|
||||
const { hotReload } = context._documentProps
|
||||
return hotReload ? <div id='__next-hot-code-reloading-indicator'/> : null
|
||||
}
|
||||
|
||||
DevTools.contextTypes = { _documentProps: PropTypes.any }
|
||||
|
||||
export function NextScript (props, context) {
|
||||
const { hotReload } = context._documentProps;
|
||||
const src = !hotReload ? '/_next/next.bundle.js' : '/_next/next-dev.bundle.js'
|
||||
return <script type='text/javascript' src={src}/>
|
||||
}
|
||||
|
||||
NextScript.contextTypes = { _documentProps: PropTypes.any }
|
52
package.json
Normal file
52
package.json
Normal file
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"name": "@zeit/next",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"main": "./dist/lib/index.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"bin": {
|
||||
"next": "./dist/bin/next"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp",
|
||||
"test": "ava"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-core": "6.17.0",
|
||||
"babel-generator": "6.17.0",
|
||||
"babel-loader": "6.2.5",
|
||||
"babel-plugin-module-alias": "1.6.0",
|
||||
"babel-plugin-transform-async-to-generator": "6.16.0",
|
||||
"babel-plugin-transform-class-properties": "6.16.0",
|
||||
"babel-plugin-transform-object-rest-spread": "6.16.0",
|
||||
"babel-plugin-transform-runtime": "6.15.0",
|
||||
"babel-preset-es2015": "6.16.0",
|
||||
"babel-preset-react": "6.16.0",
|
||||
"babel-runtime": "6.11.6",
|
||||
"cross-spawn": "4.0.2",
|
||||
"glob-promise": "1.0.6",
|
||||
"htmlescape": "1.1.1",
|
||||
"memory-fs": "0.3.0",
|
||||
"minimist": "1.2.0",
|
||||
"mkdirp-then": "1.2.0",
|
||||
"mz": "2.4.0",
|
||||
"path-match": "1.2.4",
|
||||
"react": "15.3.2",
|
||||
"react-dom": "15.3.2",
|
||||
"send": "0.14.1",
|
||||
"url": "0.11.0",
|
||||
"webpack": "1.13.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ava": "0.16.0",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-babel": "6.1.2",
|
||||
"gulp-cached": "1.1.0",
|
||||
"gulp-notify": "2.2.0",
|
||||
"gulp-uglify": "2.0.0",
|
||||
"webpack-stream": "3.2.0"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
93
server/build.js
Normal file
93
server/build.js
Normal file
|
@ -0,0 +1,93 @@
|
|||
import { resolve, dirname, basename } from 'path'
|
||||
import webpack from 'webpack'
|
||||
import { transformFile } from 'babel-core'
|
||||
import MemoryFS from 'memory-fs'
|
||||
import preset2015 from 'babel-preset-es2015'
|
||||
import presetReact from 'babel-preset-react'
|
||||
import transformAsyncToGenerator from 'babel-plugin-transform-async-to-generator'
|
||||
import transformClassProperties from 'babel-plugin-transform-class-properties'
|
||||
import transformObjectRestSpread from 'babel-plugin-transform-object-rest-spread'
|
||||
import transformRuntime from 'babel-plugin-transform-runtime'
|
||||
import moduleAlias from 'babel-plugin-module-alias'
|
||||
|
||||
const babelRuntimePath = require.resolve('babel-runtime/package')
|
||||
.replace(/[\\\/]package\.json$/, '');
|
||||
|
||||
export function transpile (path, { root = process.cwd() } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
transformFile(path, {
|
||||
presets: [preset2015, presetReact],
|
||||
plugins: [
|
||||
transformAsyncToGenerator,
|
||||
transformClassProperties,
|
||||
transformObjectRestSpread,
|
||||
transformRuntime,
|
||||
[
|
||||
moduleAlias,
|
||||
[
|
||||
{ src: `npm:${babelRuntimePath}`, expose: 'babel-runtime' },
|
||||
{ src: `npm:${require.resolve('react')}`, expose: 'react' }
|
||||
]
|
||||
]
|
||||
],
|
||||
ast: false
|
||||
}, (err, result) => {
|
||||
if (err) return reject(err)
|
||||
resolve(result.code)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function bundle (path, { root = process.cwd() } = {}) {
|
||||
const fs = new MemoryFS()
|
||||
|
||||
const compiler = webpack({
|
||||
entry: path,
|
||||
output: {
|
||||
path: dirname(path),
|
||||
filename: basename(path),
|
||||
libraryTarget: 'commonjs2'
|
||||
},
|
||||
externals: [
|
||||
'react',
|
||||
'react-dom',
|
||||
'next',
|
||||
'next/head',
|
||||
'next/link',
|
||||
'next/component',
|
||||
'next/app'
|
||||
],
|
||||
resolveLoader: {
|
||||
root: resolve(__dirname, '..', '..', 'node_modules')
|
||||
},
|
||||
plugins: [
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: { warnings: false },
|
||||
sourceMap: false
|
||||
})
|
||||
],
|
||||
module: {
|
||||
preLoaders: [
|
||||
{ test: /\.json$/, loader: 'json-loader' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
compiler.outputFileSystem = fs
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
compiler.run((err, stats) => {
|
||||
if (err) return reject(err)
|
||||
|
||||
const jsonStats = stats.toJson()
|
||||
if (jsonStats.errors.length > 0) {
|
||||
const error = new Error(jsonStats.errors[0])
|
||||
error.errors = jsonStats.errors
|
||||
error.warnings = jsonStats.warnings
|
||||
return reject(error)
|
||||
}
|
||||
|
||||
resolve(fs.readFileSync(path))
|
||||
})
|
||||
})
|
||||
}
|
109
server/index.js
Normal file
109
server/index.js
Normal file
|
@ -0,0 +1,109 @@
|
|||
import http from 'http'
|
||||
import { resolve } from 'path'
|
||||
import send from 'send'
|
||||
import Router from './router'
|
||||
import { render, renderJSON } from './render'
|
||||
|
||||
export default class Server {
|
||||
constructor (root) {
|
||||
this.root = resolve(root)
|
||||
this.router = new Router()
|
||||
|
||||
this.http = http.createServer((req, res) => {
|
||||
this.run(req, res).catch((err) => {
|
||||
this.renderError(req, res, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async start (port) {
|
||||
this.router.get('/_next/:path+', async (req, res, params) => {
|
||||
const path = (params.path || []).join('/')
|
||||
await this.serveStatic(req, res, path)
|
||||
})
|
||||
|
||||
this.router.get('/:path+.json', async (req, res, params) => {
|
||||
const path = (params.path || []).join('/')
|
||||
await this.renderJSON(req, res, path)
|
||||
})
|
||||
|
||||
this.router.get('/:path*', async (req, res, params) => {
|
||||
const path = (params.path || []).join('/')
|
||||
await this.render(req, res, path)
|
||||
})
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
this.http.listen(port, (err) => {
|
||||
if (err) return reject(err)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async run (req, res) {
|
||||
const fn = this.router.match(req, res)
|
||||
if (fn) {
|
||||
await fn()
|
||||
} else {
|
||||
await this.render404(req, res)
|
||||
}
|
||||
}
|
||||
|
||||
async render (req, res, path) {
|
||||
const { root } = this
|
||||
let html
|
||||
try {
|
||||
html = await render(path, req, res, { root })
|
||||
} catch (err) {
|
||||
if ('MODULE_NOT_FOUND' === err.code) {
|
||||
return this.render404(req, res)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
res.setHeader('Content-Type', 'text/html')
|
||||
res.setHeader('Content-Length', Buffer.byteLength(html))
|
||||
res.end(html)
|
||||
}
|
||||
|
||||
async renderJSON (req, res, path) {
|
||||
let json
|
||||
try {
|
||||
json = await renderJSON(path, { root })
|
||||
} catch (err) {
|
||||
if ('MODULE_NOT_FOUND' === err.code) {
|
||||
return this.render404(req, res)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.setHeader('Content-Length', Buffer.byteLength(json))
|
||||
res.end(json)
|
||||
}
|
||||
|
||||
async render404 (req, res) {
|
||||
res.writeHead(404)
|
||||
res.end('Not Found')
|
||||
}
|
||||
|
||||
async renderError (req, res, err) {
|
||||
console.error(err)
|
||||
res.writeHead(500)
|
||||
res.end('Error')
|
||||
}
|
||||
|
||||
serveStatic (req, res, path) {
|
||||
const p = resolve(__dirname, '../client', path)
|
||||
return new Promise((resolve, reject) => {
|
||||
send(req, p)
|
||||
.on('error', (err) => {
|
||||
if ('ENOENT' === err.code) {
|
||||
this.render404(req, res).then(resolve, reject)
|
||||
} else {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
.pipe(res)
|
||||
.on('finish', resolve)
|
||||
})
|
||||
}
|
||||
}
|
37
server/render.js
Normal file
37
server/render.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { relative, resolve } from 'path'
|
||||
import { createElement } from 'react'
|
||||
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
|
||||
import fs from 'mz/fs'
|
||||
import Document from '../lib/document'
|
||||
import App from '../lib/app'
|
||||
|
||||
export async function render (path, req, res, { root = process.cwd() } = {}) {
|
||||
const mod = require(resolve(root, '.next', 'pages', path)) || {}
|
||||
const Component = mod.default
|
||||
|
||||
let props = {}
|
||||
if (Component.getInitialProps) {
|
||||
props = await Component.getInitialProps({ req, res })
|
||||
}
|
||||
|
||||
const bundlePath = resolve(root, '.next', '.next', 'pages', path || 'index.js')
|
||||
const component = await fs.readFile(bundlePath, 'utf8')
|
||||
|
||||
const app = createElement(App, {
|
||||
Component,
|
||||
props,
|
||||
router: {}
|
||||
})
|
||||
|
||||
const doc = createElement(Document, {
|
||||
head: [],
|
||||
html: renderToString(app),
|
||||
data: { component },
|
||||
hotReload: false
|
||||
})
|
||||
|
||||
return '<!DOCTYPE html>' + renderToStaticMarkup(doc)
|
||||
}
|
||||
|
||||
export async function renderJSON (path) {
|
||||
}
|
35
server/router.js
Normal file
35
server/router.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { parse } from 'url'
|
||||
import pathMatch from 'path-match'
|
||||
|
||||
const route = pathMatch()
|
||||
|
||||
export default class Router {
|
||||
constructor () {
|
||||
this.routes = new Map()
|
||||
}
|
||||
|
||||
get (path, fn) {
|
||||
this.add('GET', path, fn)
|
||||
}
|
||||
|
||||
add (method, path, fn) {
|
||||
const routes = this.routes.get(method) || new Set()
|
||||
routes.add({ match: route(path), fn })
|
||||
this.routes.set(method, routes)
|
||||
}
|
||||
|
||||
match (req, res) {
|
||||
const routes = this.routes.get(req.method)
|
||||
if (!routes) return
|
||||
|
||||
const { pathname } = parse(req.url)
|
||||
for (const r of routes) {
|
||||
const params = r.match(pathname)
|
||||
if (params) {
|
||||
return async () => {
|
||||
return r.fn(req, res, params)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue