UNPKG

7.25 kBJavaScriptView Raw
1/* global alert */
2const { Connector } = require('./connector');
3const { Timer } = require('./timer');
4const { Options } = require('./options');
5const { Reloader } = require('./reloader');
6const { ProtocolError } = require('./protocol');
7
8class LiveReload {
9 constructor (window) {
10 this.window = window;
11 this.listeners = {};
12 this.plugins = [];
13 this.pluginIdentifiers = {};
14
15 // i can haz console?
16 this.console =
17 this.window.console && this.window.console.log && this.window.console.error
18 ? this.window.location.href.match(/LR-verbose/)
19 ? this.window.console
20 : {
21 log () {},
22 error: this.window.console.error.bind(this.window.console)
23 }
24 : {
25 log () {},
26 error () {}
27 };
28
29 // i can haz sockets?
30 if (!(this.WebSocket = this.window.WebSocket || this.window.MozWebSocket)) {
31 this.console.error('LiveReload disabled because the browser does not seem to support web sockets');
32
33 return;
34 }
35
36 // i can haz options?
37 if ('LiveReloadOptions' in window) {
38 this.options = new Options();
39
40 for (const k of Object.keys(window.LiveReloadOptions || {})) {
41 const v = window.LiveReloadOptions[k];
42
43 this.options.set(k, v);
44 }
45 } else {
46 this.options = Options.extract(this.window.document);
47
48 if (!this.options) {
49 this.console.error('LiveReload disabled because it could not find its own <SCRIPT> tag');
50
51 return;
52 }
53 }
54
55 // i can haz reloader?
56 this.reloader = new Reloader(this.window, this.console, Timer);
57
58 // i can haz connection?
59 this.connector = new Connector(this.options, this.WebSocket, Timer, {
60 connecting: () => {},
61
62 socketConnected: () => {},
63
64 connected: protocol => {
65 if (typeof this.listeners.connect === 'function') {
66 this.listeners.connect();
67 }
68
69 this.log(`LiveReload is connected to ${this.options.host}:${this.options.port} (protocol v${protocol}).`);
70
71 return this.analyze();
72 },
73
74 error: e => {
75 if (e instanceof ProtocolError) {
76 if (typeof console !== 'undefined' && console !== null) {
77 return console.log(`${e.message}.`);
78 }
79 } else {
80 if (typeof console !== 'undefined' && console !== null) {
81 return console.log(`LiveReload internal error: ${e.message}`);
82 }
83 }
84 },
85
86 disconnected: (reason, nextDelay) => {
87 if (typeof this.listeners.disconnect === 'function') {
88 this.listeners.disconnect();
89 }
90
91 switch (reason) {
92 case 'cannot-connect':
93 return this.log(`LiveReload cannot connect to ${this.options.host}:${this.options.port}, will retry in ${nextDelay} sec.`);
94 case 'broken':
95 return this.log(`LiveReload disconnected from ${this.options.host}:${this.options.port}, reconnecting in ${nextDelay} sec.`);
96 case 'handshake-timeout':
97 return this.log(`LiveReload cannot connect to ${this.options.host}:${this.options.port} (handshake timeout), will retry in ${nextDelay} sec.`);
98 case 'handshake-failed':
99 return this.log(`LiveReload cannot connect to ${this.options.host}:${this.options.port} (handshake failed), will retry in ${nextDelay} sec.`);
100 case 'manual': // nop
101 case 'error': // nop
102 default:
103 return this.log(`LiveReload disconnected from ${this.options.host}:${this.options.port} (${reason}), reconnecting in ${nextDelay} sec.`);
104 }
105 },
106
107 message: message => {
108 switch (message.command) {
109 case 'reload':
110 return this.performReload(message);
111 case 'alert':
112 return this.performAlert(message);
113 }
114 }
115 });
116
117 this.initialized = true;
118 }
119
120 on (eventName, handler) {
121 this.listeners[eventName] = handler;
122 }
123
124 log (message) {
125 return this.console.log(`${message}`);
126 }
127
128 performReload (message) {
129 this.log(`LiveReload received reload request: ${JSON.stringify(message, null, 2)}`);
130
131 return this.reloader.reload(message.path, {
132 liveCSS: message.liveCSS != null ? message.liveCSS : true,
133 liveImg: message.liveImg != null ? message.liveImg : true,
134 reloadMissingCSS: message.reloadMissingCSS != null ? message.reloadMissingCSS : true,
135 originalPath: message.originalPath || '',
136 overrideURL: message.overrideURL || '',
137 serverURL: `http://${this.options.host}:${this.options.port}`,
138 pluginOrder: this.options.pluginOrder
139 });
140 }
141
142 performAlert (message) {
143 return alert(message.message);
144 }
145
146 shutDown () {
147 if (!this.initialized) {
148 return;
149 }
150
151 this.connector.disconnect();
152 this.log('LiveReload disconnected.');
153
154 return (typeof this.listeners.shutdown === 'function' ? this.listeners.shutdown() : undefined);
155 }
156
157 hasPlugin (identifier) {
158 return !!this.pluginIdentifiers[identifier];
159 }
160
161 addPlugin (PluginClass) {
162 if (!this.initialized) {
163 return;
164 }
165
166 if (this.hasPlugin(PluginClass.identifier)) {
167 return;
168 }
169
170 this.pluginIdentifiers[PluginClass.identifier] = true;
171
172 const plugin = new PluginClass(
173 this.window,
174 {
175 // expose internal objects for those who know what they're doing
176 // (note that these are private APIs and subject to change at any time!)
177 _livereload: this,
178 _reloader: this.reloader,
179 _connector: this.connector,
180
181 // official API
182 console: this.console,
183 Timer,
184 generateCacheBustUrl: url => this.reloader.generateCacheBustUrl(url)
185 }
186 );
187
188 // API that PluginClass can/must provide:
189 //
190 // string PluginClass.identifier
191 // -- required, globally-unique name of this plugin
192 //
193 // string PluginClass.version
194 // -- required, plugin version number (format %d.%d or %d.%d.%d)
195 //
196 // plugin = new PluginClass(window, officialLiveReloadAPI)
197 // -- required, plugin constructor
198 //
199 // bool plugin.reload(string path, { bool liveCSS, bool liveImg })
200 // -- optional, attemp to reload the given path, return true if handled
201 //
202 // object plugin.analyze()
203 // -- optional, returns plugin-specific information about the current document (to send to the connected server)
204 // (LiveReload 2 server currently only defines 'disable' key in this object; return {disable:true} to disable server-side
205 // compilation of a matching plugin's files)
206
207 this.plugins.push(plugin);
208 this.reloader.addPlugin(plugin);
209 }
210
211 analyze () {
212 if (!this.initialized) {
213 return;
214 }
215
216 if (!(this.connector.protocol >= 7)) {
217 return;
218 }
219
220 const pluginsData = {};
221
222 for (const plugin of this.plugins) {
223 var pluginData = (typeof plugin.analyze === 'function' ? plugin.analyze() : undefined) || {};
224
225 pluginsData[plugin.constructor.identifier] = pluginData;
226 pluginData.version = plugin.constructor.version;
227 }
228
229 this.connector.sendCommand({
230 command: 'info',
231 plugins: pluginsData,
232 url: this.window.location.href
233 });
234 }
235};
236
237exports.LiveReload = LiveReload;