Skip to content

Commit

Permalink
feat: add hmr
Browse files Browse the repository at this point in the history
  • Loading branch information
marionebl committed Jul 31, 2016
1 parent 2f21199 commit 825c4e3
Show file tree
Hide file tree
Showing 11 changed files with 201 additions and 19 deletions.
7 changes: 5 additions & 2 deletions cli/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ function getHtmlHandler (htmlSettings, entryFile, id) {
_appendHtml: [
'<script>',
'var application = require(\'' + entryFile + '\')',
'var tree = application().start(\'[data-bankai="' + id + '"]\')',
'var app = application(window.__BANKAI_GLOBAL_STATE_HOOK__, {',
' onStateChange: function(data, state) { window.__BANKAI_GLOBAL_STATE_HOOK__ = state }',
'})',
'var tree = app.start(\'[data-bankai="' + id + '"]\')',
'if (tree) {',
' document.body.appendChild(tree)',
'}',
Expand Down Expand Up @@ -89,7 +92,7 @@ function start (options, cb) {
if (settings.html) {
const id = ['bankai'].concat(projectNameGenerator().raw).join('-')
const html = getHtmlHandler(settings.html, entryFile, id)
router.on('/', html)
router.on('/:path', html)
}

if (settings.css) {
Expand Down
70 changes: 70 additions & 0 deletions client-hmr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// var request = require('request')
var nanoajax = require('nanoajax')
// var yo = require('yo-yo')

// script injected in development mode
function updateStream (target, callback) {
var es = new window.EventSource(target)

es.addEventListener('message', function (e) {
nanoajax.ajax({url: target}, function (code, text) {
callback(null, text)
})
})
}

function toArray (list) {
return Array.prototype.slice.call(list)
}

function main () {
var scripts = toArray(document.querySelectorAll('script[data-bankai-hmr]'))
var styles = toArray(document.querySelectorAll('link[data-bankai-hmr]'))

scripts.forEach(function (script) {
updateStream(script.src, function (error, data) {
if (error) {
console.error(error)
}
var el = document.createElement('script')
var prev = document.querySelector('[data-bankai-hmr-copy="' + script.src + '"]')
el.text = data
el.setAttribute('data-bankai-hmr-copy', script.src)

if (prev) {
prev.parentNode.replaceChild(el, prev)
} else {
script.parentNode.insertBefore(el, script.nextSibling)
}

var id = script.dataset.bankaiHmr
var application = require(id)
var app = application(window.__BANKAI_GLOBAL_STATE_HOOK__, {
onStateChange: function (data, state) { window.__BANKAI_GLOBAL_STATE_HOOK__ = state }
})
var tree = app.start()
document.body.replaceChild(tree, document.body.firstChild)
// yo.update(old, tree)
})
})

styles.forEach(function (style) {
updateStream(style.href, function (error, data) {
if (error) {
console.error(error)
}
var el = document.createElement('style')
var prev = document.querySelector('[data-bankai-hmr-copy="' + style.href + '"]')
el.setAttribute('data-bankai-hmr-copy', style.href)
el.appendChild(document.createTextNode(data))

if (prev) {
prev.parentNode.replaceChild(el, prev)
} else {
style.parentNode.insertBefore(el, style.nextSibling)
}
})
})
}

main()
2 changes: 1 addition & 1 deletion example/basic/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
h1 {
color: blue;
color: red;
}
12 changes: 9 additions & 3 deletions example/basic/index.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
const choo = require('choo')
const html = require('choo/html')
const sheetify = require('sheetify')
const xtend = require('xtend')

sheetify('normalize.css')
const sheet = sheetify('./index.css')

function createApplication () {
function createApplication (state, use) {
const initial = state || {}
const middleware = use || {}
const application = choo()

application.use(middleware)

application.model({
state: { title: 'Not quite set yet!' },
state: xtend({}, {title: 'Not quite set yet!'}, initial),
reducers: {
update: (data, state) => ({ title: data })
}
Expand All @@ -20,6 +25,7 @@ function createApplication () {
<h1>Title: ${state.title}</h1>
<input
type="text"
value=${state.title}
oninput=${(e) => send('update', e.target.value)}/>
<button>Hello!</button>
</main>
Expand All @@ -30,8 +36,8 @@ function createApplication () {
<h1>Test: ${state.title}</h1>
<input
type="text"
value="${state.title}"
oninput=${(e) => send('update', e.target.value)}/>
<button>Hello!</button>
</main>
`

Expand Down
4 changes: 3 additions & 1 deletion example/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
"license": "MIT",
"dependencies": {
"choo": "^3.2.0",
"nanoajax": "^0.4.3",
"normalize.css": "^4.2.0",
"resolve": "^1.1.7",
"sheetify": "^5.0.5",
"sheetify-cssnext": "^1.0.7"
"sheetify-cssnext": "^1.0.7",
"yo": "^1.8.4"
}
}
17 changes: 16 additions & 1 deletion handler-css.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const stream = require('readable-stream')
const assert = require('assert')
const sse = require('sse-stream')
const stream = require('readable-stream')

module.exports = css

Expand All @@ -18,9 +19,23 @@ function css (state) {

return function (req, res) {
res.setHeader('Content-Type', 'text/css')

if (!state.cssBuf) {
throw new Error('no css found, did you register bankai.js?')
}

if (!state.cssSse) {
state.cssSse = sse('/' + state.htmlOpts.css)
state.cssSse.install(req.connection.server)
const eventStream = new stream.PassThrough()
state.on('css:ready', function () {
eventStream.push(JSON.stringify({update: ['css']}))
})
state.cssSse.on('connection', function (client) {
eventStream.pipe(client)
})
}

// either css hasn't been updated, and is ready to serve
// or attach a listener to when css will be updated
// and send when ready
Expand Down
62 changes: 59 additions & 3 deletions handler-html.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
const lrScript = require('inject-lr-script-stream')
const path = require('path')
const bl = require('bl')
const browserify = require('browserify')
const htmlIndex = require('simple-html-index')
const hyperstream = require('hyperstream')
const resolve = require('resolve')
const stream = require('stream')
const xtend = require('xtend')
const bl = require('bl')

const cwd = process.cwd()

// resolve a path according to require.resolve algorithm
// string -> string
function resolveEntryFile (relativePath) {
const entry = relativePath[0] === '.' || relativePath[0] === '/'
? relativePath
: './' + relativePath
return resolve.sync(entry, {basedir: cwd})
}

module.exports = html

Expand All @@ -12,15 +26,23 @@ function html (state) {
return function (opts) {
opts = opts || {}
const defaultOpts = {
src: '.',
entry: 'bundle.js',
css: 'bundle.css',
favicon: true
}
const htmlOpts = xtend(defaultOpts, opts)
state.htmlOpts = htmlOpts
const html = htmlIndex(htmlOpts).pipe(createMetaTag())
const scriptSelector = 'script[src="' + htmlOpts.entry + '"]'
const styleSelector = 'link[href="' + htmlOpts.css + '"]'

const htmlBuf = state.env === 'development'
? html.pipe(lrScript()).pipe(bl())
? html
.pipe(markHotReplaceable(scriptSelector, htmlOpts.src))
.pipe(markHotReplaceable(styleSelector, true))
.pipe(hmrScript())
.pipe(bl())
: html.pipe(bl())

return function (req, res) {
Expand All @@ -38,3 +60,37 @@ function createMetaTag () {
head: { _appendHtml: metaTag }
})
}

function markHotReplaceable (selector, src) {
const query = {}
const value = typeof src === 'string'
? resolveEntryFile(src)
: src

query[selector] = {
'data-bankai-hmr': value
}

return hyperstream(query)
}

function hmrScript () {
const b = browserify(path.resolve(__dirname, 'client-hmr.js'))
const script$ = new stream.PassThrough()
script$.push('<script>')

b.on('bundle', function (bundle$) {
bundle$.pipe(script$)
})

b.bundle(function (error) {
if (error) {
console.error(error)
}
script$.end('</script>')
})

return hyperstream({
body: { _appendHtml: script$ }
})
}
32 changes: 26 additions & 6 deletions handler-js.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
const sheetify = require('sheetify/transform')
const assert = require('assert')
const bl = require('bl')
const cssExtract = require('css-extract')
const stream = require('readable-stream')
const Emitter = require('events')
const errorify = require('errorify')
const sheetify = require('sheetify/transform')
const stream = require('readable-stream')
const sse = require('sse-stream')
const watchify = require('watchify')
const Emitter = require('events')
const assert = require('assert')
const xtend = require('xtend')
const bl = require('bl')

module.exports = js

Expand All @@ -22,14 +23,20 @@ function js (state) {

// signal to CSS that browserify is registered
state.jsRegistered = true
state.jsOpts = {
src: src,
opts: opts
}

const baseBrowserifyOpts = {
cache: {},
require: [],
packageCache: {},
entries: [ require.resolve(src) ],
fullPaths: true
}
var b = browserify(xtend(baseBrowserifyOpts, opts))
const browserifyOpts = xtend(baseBrowserifyOpts, opts)
var b = browserify(browserifyOpts)

// enable css if registered
if (state.cssOpts) {
Expand Down Expand Up @@ -63,6 +70,19 @@ function js (state) {
b.close()
})
}
if (state.env === 'development' && !b.sse) {
b.sse = sse('/' + state.htmlOpts.entry)
b.sse.install(req.connection.server)

const eventStream = new stream.PassThrough()
b.on('update', function (ids) {
eventStream.push(JSON.stringify({update: ids}))
})

b.sse.on('connection', function (client) {
eventStream.pipe(client)
})
}
handler(req, res, function (err, js) {
if (err) return ts.emit('error', err)
state.cssBuf.end()
Expand Down
3 changes: 3 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ const state = new Emitter()
state.env = process.env.NODE_ENV === 'production' ? 'production' : 'development'
state.cssStream = new stream.PassThrough()
state.jsRegistered = false
state.htmlOpts = null
state.jsOpts = null
state.cssReady = false
state.cssOpts = null
state.cssBuf = null
state.cssSse = null

exports.html = require('./handler-html')(state)
exports.css = require('./handler-css')(state)
Expand Down
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,26 @@
"bl": "^1.1.2",
"css-extract": "^1.1.1",
"errorify": "^0.3.1",
"es6-promise": "^3.2.1",
"hyperstream": "^1.2.2",
"inject-lr-script-stream": "^1.1.1",
"meow": "^3.7.0",
"nanoajax": "^0.4.3",
"opn": "^4.0.2",
"project-name-generator": "^2.1.2",
"readable-stream": "^2.1.4",
"request": "^2.74.0",
"resolve": "^1.1.7",
"server-router": "^2.1.0",
"sheetify": "^5.0.5",
"simple-html-index": "^1.3.0",
"sse-stream": "0.0.4",
"string-to-stream": "^1.1.0",
"watchify": "^3.7.0",
"xtend": "^4.0.1"
"whatwg-fetch": "^1.0.0",
"xtend": "^4.0.1",
"yo": "^1.8.4",
"yo-yo": "^1.2.2"
},
"devDependencies": {
"browserify": "^13.1.0",
Expand Down
2 changes: 1 addition & 1 deletion test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ test('js', function (t) {
// Needed because watchify does not reliably clean up all handles behind itself
// * https://github.com/substack/watchify/issues/22#issuecomment-88115610
// * https://github.com/substack/watchify/blob/master/test/zzz.js
test('zzz', function (t) {
test('zzz', function (t) {
t.on('end', function () {
setTimeout(function () {
process.exit()
Expand Down

0 comments on commit 825c4e3

Please sign in to comment.