UNPKG

8.11 kBJavaScriptView Raw
1/* jshint node:true */
2const crypto = require('crypto');
3const lr = require('tiny-lr');
4const portfinder = require('portfinder');
5const anymatch = require('anymatch');
6const servers = {};
7const schema = require('./schema.json');
8let {validate} = require('schema-utils');
9
10const PLUGIN_NAME = 'LiveReloadPlugin';
11
12class LiveReloadPlugin {
13 constructor(options = {}) {
14 // Fallback to schema-utils v1 for webpack v4
15 if (!validate)
16 validate = require('schema-utils');
17
18 validate(schema, options, {name: 'Livereload Plugin'});
19
20 this.defaultPort = 35729;
21 this.options = Object.assign({
22 protocol: '',
23 port: this.defaultPort,
24 hostname: '" + location.hostname + "',
25 ignore: null,
26 quiet: false,
27 useSourceHash: false,
28 useSourceSize: false,
29 appendScriptTag: false,
30 delay: 0,
31 }, options);
32
33 // Random alphanumeric string appended to id to allow multiple instances of live reload
34 this.instanceId = crypto.randomBytes(8).toString('hex');
35 this.lastHash = null;
36 this.lastChildHashes = [];
37 this.server = null;
38 this.sourceHashs = {};
39 this.sourceSizes = {};
40 this.webpack = null;
41 this.infrastructureLogger = null;
42 this.isWebpack4 = false;
43 }
44
45 apply(compiler) {
46 this.webpack = compiler.webpack ? compiler.webpack : require('webpack');
47 this.infrastructureLogger = compiler.getInfrastructureLogger ? compiler.getInfrastructureLogger(PLUGIN_NAME) : null;
48 this.isWebpack4 = compiler.webpack ? false : typeof compiler.resolvers !== 'undefined';
49
50 compiler.hooks.compilation.tap(PLUGIN_NAME, this._applyCompilation.bind(this));
51 compiler.hooks.watchRun.tapAsync(PLUGIN_NAME, this._start.bind(this));
52 compiler.hooks.afterEmit.tap(PLUGIN_NAME, this._afterEmit.bind(this));
53 compiler.hooks.emit.tap(PLUGIN_NAME, this._emit.bind(this));
54 compiler.hooks.failed.tap(PLUGIN_NAME, this._failed.bind(this));
55 }
56
57 /**
58 * @param a1
59 * @param a2
60 * @returns {boolean|*}
61 */
62 static arraysEqual(a1, a2) {
63 return a1.length === a2.length && a1.every((v,i) => v === a2[i])
64 }
65
66 /**
67 * @param str
68 * @returns {string}
69 */
70 static generateHashCode(str) {
71 const hash = crypto.createHash('sha256');
72 hash.update(str);
73 return hash.digest('hex');
74 }
75
76 /**
77 *
78 * @param compilation
79 * @returns {*}
80 * @private
81 */
82 _applyCompilation(compilation) {
83 if (this.isWebpack4) {
84 return compilation.mainTemplate.hooks.startup.tap(PLUGIN_NAME, this._scriptTag.bind(this));
85 }
86
87 this.webpack.javascript.JavascriptModulesPlugin.getCompilationHooks(compilation).renderRequire.tap(PLUGIN_NAME, this._scriptTag.bind(this));
88 }
89
90 /**
91 * @param watching
92 * @param cb
93 * @private
94 */
95 _start(watching, cb) {
96 if (servers[this.options.port]) {
97 this.server = servers[this.options.port];
98 return cb();
99 }
100
101 const listen = (err = null, port = null) => {
102 if (err) return cb(err);
103
104 this.options.port = port || this.options.port;
105
106 this.server = servers[this.options.port] = lr({
107 ...this.options,
108 errorListener: (err) => {
109 this.logger.error(`Live Reload disabled: ${err.message}`);
110 if (err.code !== 'EADDRINUSE') {
111 this.logger.error(err.stack);
112 }
113 cb();
114 },
115 });
116
117 this.server.listen(this.options.port, (err) => {
118 if (!err && !this.options.quiet) {
119 this.logger.info(`Live Reload listening on port ${this.options.port}`);
120 }
121 cb();
122 });
123 };
124
125 if(this.options.port === 0) {
126 portfinder.basePort = this.defaultPort;
127 portfinder.getPort(listen);
128 } else {
129 listen();
130 }
131 }
132
133 /**
134 * @returns {boolean}
135 * @private
136 */
137 _isRunning() {
138 return !!this.server;
139 }
140
141 /**
142 * @private
143 * @param compilation
144 */
145 _afterEmit(compilation) {
146 const hash = compilation.hash;
147 const childHashes = (compilation.children || []).map(child => child.hash);
148
149 const include = Object.entries(compilation.assets)
150 .filter(this._fileIgnoredOrNotEmitted.bind(this))
151 .filter(this._fileSizeDoesntMatch.bind(this))
152 .filter(this._fileHashDoesntMatch.bind(this))
153 .map((data) => data[0])
154 ;
155
156 if (
157 this._isRunning()
158 && include.length > 0
159 && (hash !== this.lastHash || !LiveReloadPlugin.arraysEqual(childHashes, this.lastChildHashes))
160 ) {
161 this.lastHash = hash;
162 this.lastChildHashes = childHashes;
163 setTimeout(() => {
164 this.server.notifyClients(include);
165 }, this.options.delay);
166 }
167 }
168
169 /**
170 * @private
171 * @param compilation
172 */
173 _emit(compilation) {
174 Object.entries(compilation.assets).forEach(this._calculateSourceHash.bind(this));
175 }
176
177 /**
178 * @private
179 */
180 _failed() {
181 this.lastHash = null;
182 this.lastChildHashes = [];
183 this.sourceHashs = {};
184 this.sourceSizes = {};
185 }
186
187 /**
188 * @returns {string}
189 * @private
190 */
191 _autoloadJs() {
192 const protocol = this.options.protocol;
193 const fullProtocol = `${protocol}${protocol ? ':' : ''}`
194 return (
195 `
196 // webpack-livereload-plugin
197 (function() {
198 if (typeof window === "undefined") { return };
199 var id = "webpack-livereload-plugin-script-${this.instanceId}";
200 if (document.getElementById(id)) { return; }
201 var el = document.createElement("script");
202 el.id = id;
203 el.async = true;
204 el.src = "${fullProtocol}//${this.options.hostname}:${this.options.port}/livereload.js";
205 document.getElementsByTagName("head")[0].appendChild(el);
206 console.log("[Live Reload] enabled");
207 }());
208 `
209 );
210 }
211
212 /**
213 * @param source
214 * @returns {*}
215 * @private
216 */
217 _scriptTag(source) {
218 if (this.options.appendScriptTag && this._isRunning()) {
219 return this._autoloadJs() + source;
220 }
221 else {
222 return source;
223 }
224 }
225
226 /**
227 * @param data
228 * @returns {boolean|*}
229 * @private
230 */
231 _fileIgnoredOrNotEmitted(data) {
232 const size = this.isWebpack4 ? data[1].emitted : data[1].size();
233
234 if (Array.isArray(this.options.ignore)) {
235 return !anymatch(this.options.ignore, data[0]) && size;
236 }
237
238 return !data[0].match(this.options.ignore) && size;
239 }
240
241 /**
242 * Check compiled source size
243 *
244 * @param data
245 * @returns {boolean}
246 * @private
247 */
248 _fileSizeDoesntMatch(data) {
249 if (!this.options.useSourceSize)
250 return true;
251
252 if (this.sourceSizes[data[0]] === data[1].size()) {
253 return false;
254 }
255
256 this.sourceSizes[data[0]] = data[1].size();
257 return true;
258 }
259
260 /**
261 * Check compiled source hash
262 *
263 * @param data
264 * @returns {boolean}
265 * @private
266 */
267 _fileHashDoesntMatch(data) {
268 if (!this.options.useSourceHash)
269 return true;
270
271 if (
272 this.sourceHashs[data[0]] !== undefined
273 && this.sourceHashs[data[0]].hash === this.sourceHashs[data[0]].calculated
274 ) {
275 return false;
276 }
277
278 // Update source hash
279 this.sourceHashs[data[0]].hash = this.sourceHashs[data[0]].calculated;
280 return true;
281 }
282
283 /**
284 * Calculate compiled source hash
285 *
286 * @param data
287 * @returns {void}
288 * @private
289 */
290 _calculateSourceHash(data) {
291 if (!this.options.useSourceHash) return;
292
293 // Calculate source hash
294 this.sourceHashs[data[0]] = {
295 hash: this.sourceHashs[data[0]] ? this.sourceHashs[data[0]].hash : null,
296 calculated: LiveReloadPlugin.generateHashCode(data[1].source())
297 };
298 }
299
300 /**
301 * @private
302 */
303 get logger() {
304 if (this.infrastructureLogger) {
305 return this.infrastructureLogger;
306 }
307
308 // Fallback logger webpack v3
309 return {
310 error: console.error,
311 warn: console.log,
312 info: console.log,
313 log: console.log,
314 debug: console.log,
315 trace: console.log,
316 group: console.log,
317 groupEnd: console.log,
318 groupCollapsed: console.log,
319 status: console.log,
320 clear: console.log,
321 profile: console.log,
322 }
323 }
324}
325
326module.exports = LiveReloadPlugin;