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