browser-sync.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. "use strict";
  2. var hooks = require("./hooks");
  3. var asyncTasks = require("./async-tasks");
  4. var config = require("./config");
  5. var connectUtils = require("./connect-utils");
  6. var utils = require("./utils");
  7. var logger = require("./logger");
  8. var eachSeries = utils.eachSeries;
  9. var _ = require("./lodash.custom");
  10. var EE = require("easy-extender");
  11. /**
  12. * Required internal plugins.
  13. * Any of these can be overridden by deliberately
  14. * causing a name-clash.
  15. */
  16. var defaultPlugins = {
  17. logger: logger,
  18. socket: require("./sockets"),
  19. "file:watcher": require("./file-watcher"),
  20. server: require("./server"),
  21. tunnel: require("./tunnel"),
  22. "client:script": require("browser-sync-client"),
  23. UI: require("browser-sync-ui")
  24. };
  25. /**
  26. * @constructor
  27. */
  28. var BrowserSync = function (emitter) {
  29. var bs = this;
  30. bs.cwd = process.cwd();
  31. bs.active = false;
  32. bs.paused = false;
  33. bs.config = config;
  34. bs.utils = utils;
  35. bs.events = bs.emitter = emitter;
  36. bs._userPlugins = [];
  37. bs._reloadQueue = [];
  38. bs._cleanupTasks = [];
  39. bs._browserReload = false;
  40. // Plugin management
  41. bs.pluginManager = new EE(defaultPlugins, hooks);
  42. };
  43. /**
  44. * Call a user-options provided callback
  45. * @param name
  46. */
  47. BrowserSync.prototype.callback = function (name) {
  48. var bs = this;
  49. var cb = bs.options.getIn(["callbacks", name]);
  50. if (_.isFunction(cb)) {
  51. cb.apply(bs.publicInstance, _.toArray(arguments).slice(1));
  52. }
  53. };
  54. /**
  55. * @param {Map} options
  56. * @param {Function} cb
  57. * @returns {BrowserSync}
  58. */
  59. BrowserSync.prototype.init = function (options, cb) {
  60. /**
  61. * Safer access to `this`
  62. * @type {BrowserSync}
  63. */
  64. var bs = this;
  65. /**
  66. * Set user-provided callback, or assign a noop
  67. * @type {Function}
  68. */
  69. bs.cb = cb || utils.defaultCallback;
  70. /**
  71. * Verify provided config.
  72. * Some options are not compatible and will cause us to
  73. * end the process.
  74. */
  75. if (!utils.verifyConfig(options, bs.cb)) {
  76. return;
  77. }
  78. /**
  79. * Save a reference to the original options
  80. * @type {Map}
  81. * @private
  82. */
  83. bs._options = options;
  84. /**
  85. * Set additional options that depend on what the
  86. * user may of provided
  87. * @type {Map}
  88. */
  89. bs.options = options;
  90. /**
  91. * Kick off default plugins.
  92. */
  93. bs.pluginManager.init();
  94. /**
  95. * Create a base logger & debugger.
  96. */
  97. bs.logger = bs.pluginManager.get("logger")(bs.events, bs);
  98. bs.debugger = bs.logger.clone({ useLevelPrefixes: true });
  99. bs.debug = bs.debugger.debug;
  100. /**
  101. * Run each setup task in sequence
  102. */
  103. eachSeries(asyncTasks, taskRunner(bs), tasksComplete(bs));
  104. return this;
  105. };
  106. /**
  107. * Run 1 setup task.
  108. * Each task is a pure function.
  109. * They can return options or instance properties to set,
  110. * but they cannot set them directly.
  111. * @param {BrowserSync} bs
  112. * @returns {Function}
  113. */
  114. function taskRunner(bs) {
  115. return function (item, cb) {
  116. bs.debug("-> {yellow:Starting Step: " + item.step);
  117. /**
  118. * Execute the current task.
  119. */
  120. item.fn(bs, executeTask);
  121. function executeTask(err, out) {
  122. /**
  123. * Exit early if any task returned an error.
  124. */
  125. if (err) {
  126. return cb(err);
  127. }
  128. /**
  129. * Act on return values (such as options to be set,
  130. * or instance properties to be set
  131. */
  132. if (out) {
  133. handleOut(bs, out);
  134. }
  135. bs.debug("+ {green:Step Complete: " + item.step);
  136. cb();
  137. }
  138. };
  139. }
  140. /**
  141. * @param bs
  142. * @param out
  143. */
  144. function handleOut(bs, out) {
  145. /**
  146. * Set a single/many option.
  147. */
  148. if (out.options) {
  149. setOptions(bs, out.options);
  150. }
  151. /**
  152. * Any options returned that require path access?
  153. */
  154. if (out.optionsIn) {
  155. out.optionsIn.forEach(function (item) {
  156. bs.setOptionIn(item.path, item.value);
  157. });
  158. }
  159. /**
  160. * Any instance properties returned?
  161. */
  162. if (out.instance) {
  163. Object.keys(out.instance).forEach(function (key) {
  164. bs[key] = out.instance[key];
  165. });
  166. }
  167. }
  168. /**
  169. * Update the options Map
  170. * @param bs
  171. * @param options
  172. */
  173. function setOptions(bs, options) {
  174. /**
  175. * If multiple options were set, act on the immutable map
  176. * in an efficient way
  177. */
  178. if (Object.keys(options).length > 1) {
  179. bs.setMany(function (item) {
  180. Object.keys(options).forEach(function (key) {
  181. item.set(key, options[key]);
  182. return item;
  183. });
  184. });
  185. }
  186. else {
  187. Object.keys(options).forEach(function (key) {
  188. bs.setOption(key, options[key]);
  189. });
  190. }
  191. }
  192. /**
  193. * At this point, ALL async tasks have completed
  194. * @param {BrowserSync} bs
  195. * @returns {Function}
  196. */
  197. function tasksComplete(bs) {
  198. return function (err) {
  199. if (err) {
  200. bs.logger.setOnce("useLevelPrefixes", true).error(err.message);
  201. }
  202. /**
  203. * Set active flag
  204. */
  205. bs.active = true;
  206. /**
  207. * @deprecated
  208. */
  209. bs.events.emit("init", bs);
  210. /**
  211. * This is no-longer needed as the Callback now only resolves
  212. * when everything (including slow things, like the tunnel) is ready.
  213. * It's here purely for backwards compatibility.
  214. * @deprecated
  215. */
  216. bs.events.emit("service:running", {
  217. options: bs.options,
  218. baseDir: bs.options.getIn(["server", "baseDir"]),
  219. type: bs.options.get("mode"),
  220. port: bs.options.get("port"),
  221. url: bs.options.getIn(["urls", "local"]),
  222. urls: bs.options.get("urls").toJS(),
  223. tunnel: bs.options.getIn(["urls", "tunnel"])
  224. });
  225. /**
  226. * Call any option-provided callbacks
  227. */
  228. bs.callback("ready", null, bs);
  229. /**
  230. * Finally, call the user-provided callback given as last arg
  231. */
  232. bs.cb(null, bs);
  233. };
  234. }
  235. /**
  236. * @param module
  237. * @param opts
  238. * @param cb
  239. */
  240. BrowserSync.prototype.registerPlugin = function (module, opts, cb) {
  241. var bs = this;
  242. bs.pluginManager.registerPlugin(module, opts, cb);
  243. if (module["plugin:name"]) {
  244. bs._userPlugins.push(module);
  245. }
  246. };
  247. /**
  248. * Get a plugin by name
  249. * @param name
  250. */
  251. BrowserSync.prototype.getUserPlugin = function (name) {
  252. var bs = this;
  253. var items = bs.getUserPlugins(function (item) {
  254. return item["plugin:name"] === name;
  255. });
  256. if (items && items.length) {
  257. return items[0];
  258. }
  259. return false;
  260. };
  261. /**
  262. * @param {Function} [filter]
  263. */
  264. BrowserSync.prototype.getUserPlugins = function (filter) {
  265. var bs = this;
  266. filter =
  267. filter ||
  268. function () {
  269. return true;
  270. };
  271. /**
  272. * Transform Plugins option
  273. */
  274. bs.userPlugins = bs._userPlugins.filter(filter).map(function (plugin) {
  275. return {
  276. name: plugin["plugin:name"],
  277. active: plugin._enabled,
  278. opts: bs.pluginManager.pluginOptions[plugin["plugin:name"]]
  279. };
  280. });
  281. return bs.userPlugins;
  282. };
  283. /**
  284. * Get middleware
  285. * @returns {*}
  286. */
  287. BrowserSync.prototype.getMiddleware = function (type) {
  288. var types = {
  289. connector: connectUtils.socketConnector(this.options)
  290. };
  291. if (type in types) {
  292. return function (req, res) {
  293. res.setHeader("Content-Type", "text/javascript");
  294. res.end(types[type]);
  295. };
  296. }
  297. };
  298. /**
  299. * Shortcut for pushing a file-serving middleware
  300. * onto the stack
  301. * @param {String} path
  302. * @param {{type: string, content: string}} props
  303. */
  304. var _serveFileCount = 0;
  305. BrowserSync.prototype.serveFile = function (path, props) {
  306. var bs = this;
  307. var mode = bs.options.get("mode");
  308. var entry = {
  309. handle: function (req, res) {
  310. res.setHeader("Content-Type", props.type);
  311. res.end(props.content);
  312. },
  313. id: "Browsersync - " + _serveFileCount++,
  314. route: path
  315. };
  316. bs._addMiddlewareToStack(entry);
  317. };
  318. /**
  319. * Add middlewares on the fly
  320. */
  321. BrowserSync.prototype._addMiddlewareToStack = function (entry) {
  322. var bs = this;
  323. /**
  324. * additional middlewares are always appended -1,
  325. * this is to allow the proxy middlewares to remain,
  326. * and the directory index to remain in serveStatic/snippet modes
  327. */
  328. bs.app.stack.splice(bs.app.stack.length - 1, 0, entry);
  329. };
  330. var _addMiddlewareCount = 0;
  331. BrowserSync.prototype.addMiddleware = function (route, handle, opts) {
  332. var bs = this;
  333. if (!bs.app) {
  334. return;
  335. }
  336. opts = opts || {};
  337. if (!opts.id) {
  338. opts.id = "bs-mw-" + _addMiddlewareCount++;
  339. }
  340. if (route === "*") {
  341. route = "";
  342. }
  343. var entry = {
  344. id: opts.id,
  345. route: route,
  346. handle: handle
  347. };
  348. if (opts.override) {
  349. entry.override = true;
  350. }
  351. bs.options = bs.options.update("middleware", function (mw) {
  352. if (bs.options.get("mode") === "proxy") {
  353. return mw.insert(mw.size - 1, entry);
  354. }
  355. return mw.concat(entry);
  356. });
  357. bs.resetMiddlewareStack();
  358. };
  359. /**
  360. * Remove middlewares on the fly
  361. * @param {String} id
  362. * @returns {Server}
  363. */
  364. BrowserSync.prototype.removeMiddleware = function (id) {
  365. var bs = this;
  366. if (!bs.app) {
  367. return;
  368. }
  369. bs.options = bs.options.update("middleware", function (mw) {
  370. return mw.filter(function (mw) {
  371. return mw.id !== id;
  372. });
  373. });
  374. bs.resetMiddlewareStack();
  375. };
  376. /**
  377. * Middleware for socket connection (external usage)
  378. * @param opts
  379. * @returns {*}
  380. */
  381. BrowserSync.prototype.getSocketConnector = function (opts) {
  382. var bs = this;
  383. return function (req, res) {
  384. res.setHeader("Content-Type", "text/javascript");
  385. res.end(bs.getExternalSocketConnector(opts));
  386. };
  387. };
  388. /**
  389. * Socket connector as a string
  390. * @param {Object} opts
  391. * @returns {*}
  392. */
  393. BrowserSync.prototype.getExternalSocketConnector = function (opts) {
  394. var bs = this;
  395. return connectUtils.socketConnector(bs.options.withMutations(function (item) {
  396. item.set("socket", item.get("socket").merge(opts));
  397. if (!bs.options.getIn(["proxy", "ws"])) {
  398. item.set("mode", "snippet");
  399. }
  400. }));
  401. };
  402. /**
  403. * Callback helper
  404. * @param name
  405. */
  406. BrowserSync.prototype.getOption = function (name) {
  407. this.debug("Getting option: {magenta:%s", name);
  408. return this.options.get(name);
  409. };
  410. /**
  411. * Callback helper
  412. * @param path
  413. */
  414. BrowserSync.prototype.getOptionIn = function (path) {
  415. this.debug("Getting option via path: {magenta:%s", path);
  416. return this.options.getIn(path);
  417. };
  418. /**
  419. * @returns {BrowserSync.options}
  420. */
  421. BrowserSync.prototype.getOptions = function () {
  422. return this.options;
  423. };
  424. /**
  425. * @returns {BrowserSync.options}
  426. */
  427. BrowserSync.prototype.getLogger = logger.getLogger;
  428. /**
  429. * @param {String} name
  430. * @param {*} value
  431. * @returns {BrowserSync.options|*}
  432. */
  433. BrowserSync.prototype.setOption = function (name, value, opts) {
  434. var bs = this;
  435. opts = opts || {};
  436. bs.debug("Setting Option: {cyan:%s} - {magenta:%s", name, value.toString());
  437. bs.options = bs.options.set(name, value);
  438. if (!opts.silent) {
  439. bs.events.emit("options:set", {
  440. path: name,
  441. value: value,
  442. options: bs.options
  443. });
  444. }
  445. return this.options;
  446. };
  447. /**
  448. * @param path
  449. * @param value
  450. * @param opts
  451. * @returns {Map|*|BrowserSync.options}
  452. */
  453. BrowserSync.prototype.setOptionIn = function (path, value, opts) {
  454. var bs = this;
  455. opts = opts || {};
  456. bs.debug("Setting Option: {cyan:%s} - {magenta:%s", path.join("."), value.toString());
  457. bs.options = bs.options.setIn(path, value);
  458. if (!opts.silent) {
  459. bs.events.emit("options:set", {
  460. path: path,
  461. value: value,
  462. options: bs.options
  463. });
  464. }
  465. return bs.options;
  466. };
  467. /**
  468. * Set multiple options with mutations
  469. * @param fn
  470. * @param opts
  471. * @returns {Map|*}
  472. */
  473. BrowserSync.prototype.setMany = function (fn, opts) {
  474. var bs = this;
  475. opts = opts || {};
  476. bs.debug("Setting multiple Options");
  477. bs.options = bs.options.withMutations(fn);
  478. if (!opts.silent) {
  479. bs.events.emit("options:set", { options: bs.options.toJS() });
  480. }
  481. return this.options;
  482. };
  483. BrowserSync.prototype.addRewriteRule = function (rule) {
  484. var bs = this;
  485. bs.options = bs.options.update("rewriteRules", function (rules) {
  486. return rules.concat(rule);
  487. });
  488. bs.resetMiddlewareStack();
  489. };
  490. BrowserSync.prototype.removeRewriteRule = function (id) {
  491. var bs = this;
  492. bs.options = bs.options.update("rewriteRules", function (rules) {
  493. return rules.filter(function (rule) {
  494. return rule.id !== id;
  495. });
  496. });
  497. bs.resetMiddlewareStack();
  498. };
  499. BrowserSync.prototype.setRewriteRules = function (rules) {
  500. var bs = this;
  501. bs.options = bs.options.update("rewriteRules", function (_) {
  502. return rules;
  503. });
  504. bs.resetMiddlewareStack();
  505. };
  506. /**
  507. * Add a new rewrite rule to the stack
  508. * @param {Object} rule
  509. */
  510. BrowserSync.prototype.resetMiddlewareStack = function () {
  511. var bs = this;
  512. var middlewares = require("./server/utils").getMiddlewares(bs, bs.options);
  513. bs.app.stack = middlewares;
  514. };
  515. /**
  516. * @param fn
  517. */
  518. BrowserSync.prototype.registerCleanupTask = function (fn) {
  519. this._cleanupTasks.push(fn);
  520. };
  521. /**
  522. * Instance Cleanup
  523. */
  524. BrowserSync.prototype.cleanup = function (cb) {
  525. var bs = this;
  526. if (!bs.active) {
  527. return;
  528. }
  529. // Remove all event listeners
  530. if (bs.events) {
  531. bs.debug("Removing event listeners...");
  532. bs.events.removeAllListeners();
  533. }
  534. // Close any core file watchers
  535. if (bs.watchers) {
  536. Object.keys(bs.watchers).forEach(function (key) {
  537. bs.watchers[key].watchers.forEach(function (watcher) {
  538. watcher.close();
  539. });
  540. });
  541. }
  542. // Run any additional clean up tasks
  543. bs._cleanupTasks.forEach(function (fn) {
  544. if (_.isFunction(fn)) {
  545. fn(bs);
  546. }
  547. });
  548. // Reset the flag
  549. bs.debug("Setting {magenta:active: false");
  550. bs.active = false;
  551. bs.paused = false;
  552. bs.pluginManager.plugins = {};
  553. bs.pluginManager.pluginOptions = {};
  554. bs.pluginManager.defaultPlugins = defaultPlugins;
  555. bs._userPlugins = [];
  556. bs.userPlugins = [];
  557. bs._reloadTimer = undefined;
  558. bs._reloadQueue = [];
  559. bs._cleanupTasks = [];
  560. if (_.isFunction(cb)) {
  561. cb(null, bs);
  562. }
  563. };
  564. module.exports = BrowserSync;
  565. //# sourceMappingURL=browser-sync.js.map