1 | #!/usr/bin/env node
|
2 | 'use strict'
|
3 |
|
4 | var fs = require('fs')
|
5 | var path = require('path')
|
6 | var http = require('http')
|
7 | var debounce = require('lodash.debounce')
|
8 |
|
9 | var APP_ROOT = process.cwd()
|
10 | var SRC_ROOT = path.join(APP_ROOT, 'src')
|
11 | var express = require('express')
|
12 | var st = require('st')
|
13 | var xtend = require('xtend')
|
14 | var Promise = require('bluebird')
|
15 | var through = require('through2');
|
16 |
|
17 | var _ = require('lodash');
|
18 | var socketio = require('socket.io');
|
19 | var has = require('./browserify-hmr/lib/has');
|
20 |
|
21 | var chokidar = require('chokidar')
|
22 | var watcher = chokidar.watch(SRC_ROOT, {
|
23 | cwd: SRC_ROOT,
|
24 | ignored: [
|
25 | /[\/\\]\./
|
26 | ],
|
27 | ignoreInitial: true
|
28 | })
|
29 |
|
30 | var bundle = require('./bundle')
|
31 | var args = {
|
32 | fullPaths: true,
|
33 | cache: {},
|
34 | packageCache: {}
|
35 | }
|
36 |
|
37 | var EventEmitter = require('events').EventEmitter
|
38 | var _hmr = global._hmr = {
|
39 | transformCache: {},
|
40 | }
|
41 | var hmrPlugin = require('./browserify-hmr')({basedir: SRC_ROOT})
|
42 | var cacheModuleData = {}
|
43 |
|
44 | var globalCache = {}
|
45 | watcher.on('all', (event, onPath) => {
|
46 | if (['change', 'unlink', 'unlinkDir'].indexOf(event) === -1) {
|
47 | return
|
48 | }
|
49 | update(onPath)
|
50 | })
|
51 | var reload = debounce(function () {
|
52 | if (browserSync && typeof browserSync.reload === 'function') {
|
53 | browserSync.reload()
|
54 | }
|
55 | }, 300)
|
56 | var globalRegExp = /[\\\/]node_modules[\\\/]|[\\\/]src[\\\/]global\.(?:js|libs\.json)$/
|
57 | var hmrModuleReg = /\.(jsx?|vue)$/i
|
58 | var cacheLibs
|
59 | function findAffectedModule(affectsMap, updatedFiles) {
|
60 | if (!Array.isArray(updatedFiles)) {
|
61 | updatedFiles = [updatedFiles]
|
62 | }
|
63 | if (_.every(updatedFiles, f => hmrModuleReg.test(f))) {
|
64 | return updatedFiles
|
65 | }
|
66 | return Array.prototype.concat.apply(
|
67 | updatedFiles.filter(f => hmrModuleReg.test(f)),
|
68 | updatedFiles.filter(f => !hmrModuleReg.test(f)).map(f => affectsMap[f])
|
69 | )
|
70 | }
|
71 | function update (onPath) {
|
72 | var p = path.resolve(SRC_ROOT, onPath)
|
73 | var hitCache = !!args.cache[p]
|
74 | console.log('hitCache', p, hitCache);
|
75 | var matchGlobal = p.match(globalRegExp)
|
76 | if (matchGlobal) {
|
77 | cacheLibs = void 0
|
78 | } else if (hitCache) {
|
79 | var updatedFiles = [p]
|
80 | var affects = {}
|
81 | var ignoreRegExp = /[\\\/]__hmr_manager.js$|\/brocode\/node_modules\//
|
82 | _.each(args.cache, function (row, id) {
|
83 | _.each(row.deps, function (dep) {
|
84 | var af = affects[dep] || (affects[dep] = [])
|
85 | if (!ignoreRegExp.test(dep) && !ignoreRegExp.test(id)) {
|
86 | af.push(id)
|
87 | }
|
88 | })
|
89 | })
|
90 | var affectedFiles = findAffectedModule(affects, p)
|
91 | delete args.cache[p]
|
92 | delete args.packageCache[p]
|
93 | delete args.packageCache[p + '/package.json']
|
94 | delete args.packageCache[p + '\\package.json']
|
95 | var af = affectedFiles
|
96 | .filter(p => hmrModuleReg.test(p))
|
97 | .filter(p => !(/\/js\/main\//.test(p)))
|
98 | .filter(p => fs.existsSync(p))
|
99 | if (p.indexOf('/js/main/') > -1) {
|
100 | af.push(p)
|
101 | }
|
102 | if (af.length) {
|
103 | log('(update)', af, 'starting...')
|
104 | var start = Date.now()
|
105 | bundle([], af, getOpts(false, true)).then(function () {
|
106 | log('(update)', af, `${Date.now() - start}ms`)
|
107 | })
|
108 | }
|
109 | }
|
110 | reload()
|
111 | }
|
112 |
|
113 | var app = express()
|
114 | function log () {
|
115 | var args = Array.prototype.slice.call(arguments)
|
116 | args.unshift((new Date()).toTimeString())
|
117 | console.log.apply(console, args)
|
118 | }
|
119 |
|
120 | app.use(function (req, res, next) {
|
121 | req.serverPort = req.connection.server.address().port
|
122 | next()
|
123 | })
|
124 | var globalLibsPath = path.join(SRC_ROOT, 'global.libs.json')
|
125 | function emitNewModules(socket, moduleData) {
|
126 | if (!socket.moduleData) {
|
127 | return
|
128 | }
|
129 | var currentModuleData = socket.moduleData
|
130 | var newModuleData = _.chain(moduleData)
|
131 | .toPairs()
|
132 | .filter(function(pair) {
|
133 | return pair[1].isNew && (!currentModuleData[pair[0]] || currentModuleData[pair[0]].hash !== pair[1].hash)
|
134 | })
|
135 | .map(function(pair) {
|
136 | return [pair[0], {
|
137 | index: pair[1].index,
|
138 | hash: pair[1].hash,
|
139 | source: pair[1].source,
|
140 | parents: pair[1].parents,
|
141 | deps: pair[1].deps
|
142 | }];
|
143 | })
|
144 | .fromPairs()
|
145 | .value();
|
146 | _.assign(socket.moduleData, newModuleData)
|
147 | var removedModules = _.chain(currentModuleData)
|
148 | .keys()
|
149 | .filter(function(name) {
|
150 | return !has(moduleData, name);
|
151 | })
|
152 | .value();
|
153 | if (Object.keys(newModuleData).length || removedModules.length) {
|
154 | log('[HMR]', 'Emitting updates');
|
155 | socket.emit('new modules', {newModuleData: newModuleData, removedModules: removedModules});
|
156 | }
|
157 | }
|
158 | function syncModules() {
|
159 | _.each(io.sockets.connected, function(socket) {
|
160 | emitNewModules(socket, cacheModuleData)
|
161 | })
|
162 | }
|
163 | function getOpts(isGlobal, isHmr) {
|
164 | var opts = { hmr: !!isHmr }
|
165 | var globalLibs = cacheLibs
|
166 | if (!globalLibs) {
|
167 | globalLibs = []
|
168 | try {
|
169 | globalLibs = JSON.parse(fs.readFileSync(globalLibsPath, 'utf-8'))
|
170 | } catch (e) {}
|
171 | cacheLibs = globalLibs
|
172 | }
|
173 | if (isGlobal) {
|
174 | opts.global = true
|
175 | opts.alterb = function (b) {
|
176 | globalLibs.forEach(function (x) {
|
177 | b.require(x[0], {expose: x[1] || x[0]})
|
178 | })
|
179 | }
|
180 | } else {
|
181 | opts.alterb = function (b) {
|
182 | b.on('setNewModuleData', function(moduleData) {
|
183 | _.assign(cacheModuleData, moduleData)
|
184 | syncModules()
|
185 | })
|
186 | }
|
187 | opts.externals = globalLibs.map(function (x) {
|
188 | return x[1] || x[0]
|
189 | }).filter(Boolean)
|
190 | }
|
191 | opts.args = xtend(args, {basedir: SRC_ROOT}, (!isGlobal && isHmr ? {plugin: [hmrPlugin]} : {}))
|
192 | return opts
|
193 | }
|
194 | app.get(/.*\.js$/i, function (req, res, next) {
|
195 | var isHmr = false
|
196 | if (req.serverPort > app.port + 2) {
|
197 | return next()
|
198 | } else if (req.serverPort === app.port + 2) {
|
199 | isHmr = true
|
200 | }
|
201 | if (!(req.url === '/global.js' || req.url.indexOf('/js/main/') > -1)) {
|
202 | return next()
|
203 | }
|
204 | var filePath = path.join(SRC_ROOT, req.url)
|
205 | var isGlobal = false
|
206 |
|
207 | if (req.url === '/global.js') {
|
208 | isGlobal = true
|
209 | }
|
210 | var opts = getOpts(isGlobal, isHmr)
|
211 |
|
212 | log('(bundle)', path.sep + path.relative(SRC_ROOT, filePath), `starting...`)
|
213 | var start = Date.now()
|
214 | var b = (exists) => (exists ? bundle([filePath], [], opts) : bundle([], [], opts)).then(b => {
|
215 | log('(bundle)', path.sep + path.relative(SRC_ROOT, filePath), `${Date.now() - start}ms`)
|
216 | res.type('js')
|
217 | res.send(b)
|
218 | })
|
219 |
|
220 | if (isGlobal) {
|
221 | fs.exists(filePath, b)
|
222 | } else fs.exists(filePath, function (exists) {
|
223 | if (!exists) {
|
224 | next()
|
225 | return
|
226 | }
|
227 | b(true)
|
228 | })
|
229 | })
|
230 |
|
231 | app.use(function (req, res, next) {
|
232 | var sod = req.serverPort > app.port + 2 ? '/.dist' : '/src'
|
233 | req.url = sod + req.url
|
234 | log('[static]', req.url)
|
235 | next()
|
236 | })
|
237 |
|
238 | app.use(st({
|
239 | cache: false,
|
240 | dot: true,
|
241 | path: APP_ROOT,
|
242 | index: 'index.html'
|
243 | }))
|
244 |
|
245 | var browserSync
|
246 | var io
|
247 | Object.defineProperty(exports, 'io', {
|
248 | get: function() {
|
249 | return io
|
250 | }
|
251 | })
|
252 |
|
253 | exports.app = app
|
254 | exports.start = function (port) {
|
255 | port = port || 8000
|
256 | app.port = port
|
257 | var server = http.createServer(app)
|
258 | var distServer = http.createServer(app)
|
259 | var hmrServer = http.createServer(app)
|
260 | io = socketio(hmrServer)
|
261 | io.on('connection', function(socket) {
|
262 | function log() {
|
263 | console.log.apply(console, [new Date().toTimeString(), '[HMR]'].concat(_.toArray(arguments)));
|
264 | }
|
265 | socket.on('sync', function(syncMsg) {
|
266 | log('User connected, syncing');
|
267 | var oldModuleData = _.pick(cacheModuleData, _.keys(syncMsg))
|
268 | var mainScripts = _.keys(syncMsg).filter(function(name) {
|
269 | return name.startsWith('js/main/')
|
270 | })
|
271 | log('(sync)', mainScripts, 'starting...')
|
272 | var start = Date.now()
|
273 | ;(mainScripts.length
|
274 | ? Promise.all(mainScripts.map((name) => bundle([path.join(SRC_ROOT, name)], [], getOpts(false, true))))
|
275 | : Promise.resolve([])).then(function (results) {
|
276 | log('(sync)', mainScripts, `${Date.now() - start}ms`)
|
277 | socket.moduleData = oldModuleData
|
278 | socket.emit('sync confirm', null);
|
279 | emitNewModules(socket, cacheModuleData)
|
280 | });
|
281 | });
|
282 | });
|
283 |
|
284 | server.listen(port, '0.0.0.0', function () {
|
285 | console.log('development server listening at http://0.0.0.0:%d', this.address().port)
|
286 | })
|
287 |
|
288 | browserSync = require('browser-sync').create()
|
289 |
|
290 | browserSync.init({
|
291 | proxy: 'localhost:' + port,
|
292 | port: port + 1,
|
293 | ui: false,
|
294 | open: false
|
295 | }, function() {
|
296 | console.log('development server with browsersync listening at http://0.0.0.0:%d', port + 1)
|
297 | })
|
298 |
|
299 | hmrServer.listen(port + 2, '0.0.0.0', function () {
|
300 | console.log('hot module reload server listening at http://0.0.0.0:%d, run `brocode build` to update', this.address().port)
|
301 | })
|
302 |
|
303 | distServer.listen(port + 3, '0.0.0.0', function () {
|
304 | console.log('production preview server listening at http://0.0.0.0:%d, run `brocode build` to update', this.address().port)
|
305 | })
|
306 | }
|