From 9b06a22f31655ca3ff70954ebacef0fc351e7097 Mon Sep 17 00:00:00 2001
From: nkzawa
Date: Thu, 6 Oct 2016 08:52:50 +0900
Subject: [PATCH] initial source
---
.gitignore | 3 +
bin/next | 31 +++++
bin/next-build | 44 +++++++
bin/next-start | 27 ++++
client/eval-script.js | 31 +++++
client/next.js | 18 +++
client/router.js | 259 +++++++++++++++++++++++++++++++++++++++
client/shallow-equals.js | 11 ++
gulpfile.js | 156 +++++++++++++++++++++++
lib/app.js | 71 +++++++++++
lib/document.js | 59 +++++++++
package.json | 52 ++++++++
server/build.js | 93 ++++++++++++++
server/index.js | 109 ++++++++++++++++
server/render.js | 37 ++++++
server/router.js | 35 ++++++
16 files changed, 1036 insertions(+)
create mode 100644 .gitignore
create mode 100755 bin/next
create mode 100755 bin/next-build
create mode 100755 bin/next-start
create mode 100644 client/eval-script.js
create mode 100644 client/next.js
create mode 100644 client/router.js
create mode 100644 client/shallow-equals.js
create mode 100644 gulpfile.js
create mode 100644 lib/app.js
create mode 100644 lib/document.js
create mode 100644 package.json
create mode 100644 server/build.js
create mode 100644 server/index.js
create mode 100644 server/render.js
create mode 100644 server/router.js
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..f2e955cc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+*.log
+node_modules
+dist
diff --git a/bin/next b/bin/next
new file mode 100755
index 00000000..81f7d96c
--- /dev/null
+++ b/bin/next
@@ -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)
+})
diff --git a/bin/next-build b/bin/next-build
new file mode 100755
index 00000000..12c96328
--- /dev/null
+++ b/bin/next-build
@@ -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+' })
+}
+
diff --git a/bin/next-start b/bin/next-start
new file mode 100755
index 00000000..e46dcab4
--- /dev/null
+++ b/bin/next-start
@@ -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)
+})
diff --git a/client/eval-script.js b/client/eval-script.js
new file mode 100644
index 00000000..c32901ef
--- /dev/null
+++ b/client/eval-script.js
@@ -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
+}
diff --git a/client/next.js b/client/next.js
new file mode 100644
index 00000000..0beca747
--- /dev/null
+++ b/client/next.js
@@ -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)
diff --git a/client/router.js b/client/router.js
new file mode 100644
index 00000000..cad9184b
--- /dev/null
+++ b/client/router.js
@@ -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)
+ }
+}
diff --git a/client/shallow-equals.js b/client/shallow-equals.js
new file mode 100644
index 00000000..a4335c50
--- /dev/null
+++ b/client/shallow-equals.js
@@ -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;
+}
diff --git a/gulpfile.js b/gulpfile.js
new file mode 100644
index 00000000..bd022d54
--- /dev/null
+++ b/gulpfile.js
@@ -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
+ })
+}
diff --git a/lib/app.js b/lib/app.js
new file mode 100644
index 00000000..cd38bd55
--- /dev/null
+++ b/lib/app.js
@@ -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 }
+ }
+}
diff --git a/lib/document.js b/lib/document.js
new file mode 100644
index 00000000..3e0789c0
--- /dev/null
+++ b/lib/document.js
@@ -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
+
+
+
+
+
+
+
+ }
+}
+
+export function Head (props, context) {
+ const { head } = context._documentProps
+ const h = (head || [])
+ .map((h, i) => React.cloneElement(h, { key: '_next' + i }))
+ return {h}
+}
+
+Head.contextTypes = { _documentProps: PropTypes.any }
+
+export function Main (props, context) {
+ const { html, data } = context._documentProps;
+ return
+}
+
+Main.contextTypes = { _documentProps: PropTypes.any }
+
+export function DevTools (props, context) {
+ const { hotReload } = context._documentProps
+ return hotReload ? : 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