1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | function splitUrl (url) {
|
9 | let hash = '';
|
10 | let params = '';
|
11 | let index = url.indexOf('#');
|
12 |
|
13 | if (index >= 0) {
|
14 | hash = url.slice(index);
|
15 | url = url.slice(0, index);
|
16 | }
|
17 |
|
18 |
|
19 | const comboSign = url.indexOf('??');
|
20 |
|
21 | if (comboSign >= 0) {
|
22 | if ((comboSign + 1) !== url.lastIndexOf('?')) {
|
23 | index = url.lastIndexOf('?');
|
24 | }
|
25 | } else {
|
26 | index = url.indexOf('?');
|
27 | }
|
28 |
|
29 | if (index >= 0) {
|
30 | params = url.slice(index);
|
31 | url = url.slice(0, index);
|
32 | }
|
33 |
|
34 | return { url, params, hash };
|
35 | };
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 | function pathFromUrl (url) {
|
43 | if (!url) {
|
44 | return '';
|
45 | }
|
46 |
|
47 | let path;
|
48 |
|
49 | ({ url } = splitUrl(url));
|
50 |
|
51 | if (url.indexOf('file://') === 0) {
|
52 | path = url.replace(new RegExp('^file://(localhost)?'), '');
|
53 | } else {
|
54 |
|
55 | path = url.replace(new RegExp('^([^:]+:)?//([^:/]+)(:\\d*)?/'), '/');
|
56 | }
|
57 |
|
58 |
|
59 | return decodeURIComponent(path);
|
60 | }
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 | function numberOfMatchingSegments (left, right) {
|
69 |
|
70 | left = left.replace(/^\/+/, '').toLowerCase();
|
71 | right = right.replace(/^\/+/, '').toLowerCase();
|
72 |
|
73 | if (left === right) {
|
74 | return 10000;
|
75 | }
|
76 |
|
77 | const comps1 = left.split(/\/|\\/).reverse();
|
78 | const comps2 = right.split(/\/|\\/).reverse();
|
79 | const len = Math.min(comps1.length, comps2.length);
|
80 |
|
81 | let eqCount = 0;
|
82 |
|
83 | while ((eqCount < len) && (comps1[eqCount] === comps2[eqCount])) {
|
84 | ++eqCount;
|
85 | }
|
86 |
|
87 | return eqCount;
|
88 | }
|
89 |
|
90 |
|
91 |
|
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 | function pickBestMatch (path, objects, pathFunc = s => s) {
|
98 | let score;
|
99 | let bestMatch = { score: 0 };
|
100 |
|
101 | for (const object of objects) {
|
102 | score = numberOfMatchingSegments(path, pathFunc(object));
|
103 |
|
104 | if (score > bestMatch.score) {
|
105 | bestMatch = { object, score };
|
106 | }
|
107 | }
|
108 |
|
109 | if (bestMatch.score === 0) {
|
110 | return null;
|
111 | }
|
112 |
|
113 | return bestMatch;
|
114 | }
|
115 |
|
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 | function pathsMatch (left, right) {
|
123 | return numberOfMatchingSegments(left, right) > 0;
|
124 | }
|
125 |
|
126 | const IMAGE_STYLES = [
|
127 | { selector: 'background', styleNames: ['backgroundImage'] },
|
128 | { selector: 'border', styleNames: ['borderImage', 'webkitBorderImage', 'MozBorderImage'] }
|
129 | ];
|
130 |
|
131 | const DEFAULT_OPTIONS = {
|
132 | stylesheetReloadTimeout: 15000
|
133 | };
|
134 |
|
135 | class Reloader {
|
136 | constructor (window, console, Timer) {
|
137 | this.window = window;
|
138 | this.console = console;
|
139 | this.Timer = Timer;
|
140 | this.document = this.window.document;
|
141 | this.importCacheWaitPeriod = 200;
|
142 | this.plugins = [];
|
143 | }
|
144 |
|
145 | addPlugin (plugin) {
|
146 | return this.plugins.push(plugin);
|
147 | }
|
148 |
|
149 | analyze (callback) {
|
150 | }
|
151 |
|
152 | reload (path, options = {}) {
|
153 | this.options = {
|
154 | ...DEFAULT_OPTIONS,
|
155 | ...options
|
156 | };
|
157 |
|
158 | if (this.options.pluginOrder && this.options.pluginOrder.length) {
|
159 | this.runPluginsByOrder(path, options);
|
160 | return;
|
161 | }
|
162 |
|
163 | for (const plugin of Array.from(this.plugins)) {
|
164 | if (plugin.reload && plugin.reload(path, options)) {
|
165 | return;
|
166 | }
|
167 | }
|
168 |
|
169 | if (options.liveCSS && path.match(/\.css(?:\.map)?$/i)) {
|
170 | if (this.reloadStylesheet(path)) {
|
171 | return;
|
172 | }
|
173 | }
|
174 |
|
175 | if (options.liveImg && path.match(/\.(jpe?g|png|gif)$/i)) {
|
176 | this.reloadImages(path);
|
177 | return;
|
178 | }
|
179 |
|
180 | if (options.isChromeExtension) {
|
181 | this.reloadChromeExtension();
|
182 | return;
|
183 | }
|
184 |
|
185 | return this.reloadPage();
|
186 | }
|
187 |
|
188 | runPluginsByOrder (path, options) {
|
189 | options.pluginOrder.some(pluginId => {
|
190 | if (pluginId === 'css') {
|
191 | if (options.liveCSS && path.match(/\.css(?:\.map)?$/i)) {
|
192 | if (this.reloadStylesheet(path)) {
|
193 | return true;
|
194 | }
|
195 | }
|
196 | }
|
197 |
|
198 | if (pluginId === 'img') {
|
199 | if (options.liveImg && path.match(/\.(jpe?g|png|gif)$/i)) {
|
200 | this.reloadImages(path);
|
201 | return true;
|
202 | }
|
203 | }
|
204 |
|
205 | if (pluginId === 'extension') {
|
206 | if (options.isChromeExtension) {
|
207 | this.reloadChromeExtension();
|
208 | return true;
|
209 | }
|
210 | }
|
211 |
|
212 | if (pluginId === 'others') {
|
213 | this.reloadPage();
|
214 | return true;
|
215 | }
|
216 |
|
217 | if (pluginId === 'external') {
|
218 | return this.plugins.some(plugin => {
|
219 | if (plugin.reload && plugin.reload(path, options)) {
|
220 | return true;
|
221 | }
|
222 | });
|
223 | }
|
224 |
|
225 | return this.plugins.filter(
|
226 | plugin => plugin.constructor.identifier === pluginId
|
227 | )
|
228 | .some(plugin => {
|
229 | if (plugin.reload && plugin.reload(path, options)) {
|
230 | return true;
|
231 | }
|
232 | });
|
233 | });
|
234 | }
|
235 |
|
236 | reloadPage () {
|
237 | return this.window.document.location.reload();
|
238 | }
|
239 |
|
240 | reloadChromeExtension () {
|
241 | return this.window.chrome.runtime.reload();
|
242 | }
|
243 |
|
244 | reloadImages (path) {
|
245 | let img;
|
246 | const expando = this.generateUniqueString();
|
247 |
|
248 | for (img of Array.from(this.document.images)) {
|
249 | if (pathsMatch(path, pathFromUrl(img.src))) {
|
250 | img.src = this.generateCacheBustUrl(img.src, expando);
|
251 | }
|
252 | }
|
253 |
|
254 | if (this.document.querySelectorAll) {
|
255 | for (const { selector, styleNames } of IMAGE_STYLES) {
|
256 | for (img of Array.from(this.document.querySelectorAll(`[style*=${selector}]`))) {
|
257 | this.reloadStyleImages(img.style, styleNames, path, expando);
|
258 | }
|
259 | }
|
260 | }
|
261 |
|
262 | if (this.document.styleSheets) {
|
263 | return Array.from(this.document.styleSheets).map(styleSheet =>
|
264 | this.reloadStylesheetImages(styleSheet, path, expando)
|
265 | );
|
266 | }
|
267 | }
|
268 |
|
269 | reloadStylesheetImages (styleSheet, path, expando) {
|
270 | let rules;
|
271 |
|
272 | try {
|
273 | rules = (styleSheet || {}).cssRules;
|
274 | } catch (e) {}
|
275 |
|
276 | if (!rules) {
|
277 | return;
|
278 | }
|
279 |
|
280 | for (const rule of Array.from(rules)) {
|
281 | switch (rule.type) {
|
282 | case CSSRule.IMPORT_RULE:
|
283 | this.reloadStylesheetImages(rule.styleSheet, path, expando);
|
284 | break;
|
285 | case CSSRule.STYLE_RULE:
|
286 | for (const { styleNames } of IMAGE_STYLES) {
|
287 | this.reloadStyleImages(rule.style, styleNames, path, expando);
|
288 | }
|
289 | break;
|
290 | case CSSRule.MEDIA_RULE:
|
291 | this.reloadStylesheetImages(rule, path, expando);
|
292 | break;
|
293 | }
|
294 | }
|
295 | }
|
296 |
|
297 | reloadStyleImages (style, styleNames, path, expando) {
|
298 | for (const styleName of styleNames) {
|
299 | const value = style[styleName];
|
300 |
|
301 | if (typeof value === 'string') {
|
302 | const newValue = value.replace(new RegExp('\\burl\\s*\\(([^)]*)\\)'), (match, src) => {
|
303 | if (pathsMatch(path, pathFromUrl(src))) {
|
304 | return `url(${this.generateCacheBustUrl(src, expando)})`;
|
305 | }
|
306 |
|
307 | return match;
|
308 | });
|
309 |
|
310 | if (newValue !== value) {
|
311 | style[styleName] = newValue;
|
312 | }
|
313 | }
|
314 | }
|
315 | }
|
316 |
|
317 | reloadStylesheet (path) {
|
318 | const options = this.options || DEFAULT_OPTIONS;
|
319 |
|
320 |
|
321 | let style;
|
322 | let link;
|
323 |
|
324 | const links = ((() => {
|
325 | const result = [];
|
326 |
|
327 | for (link of Array.from(this.document.getElementsByTagName('link'))) {
|
328 | if (link.rel.match(/^stylesheet$/i) && !link.__LiveReload_pendingRemoval) {
|
329 | result.push(link);
|
330 | }
|
331 | }
|
332 |
|
333 | return result;
|
334 | })());
|
335 |
|
336 |
|
337 | const imported = [];
|
338 |
|
339 | for (style of Array.from(this.document.getElementsByTagName('style'))) {
|
340 | if (style.sheet) {
|
341 | this.collectImportedStylesheets(style, style.sheet, imported);
|
342 | }
|
343 | }
|
344 |
|
345 | for (link of Array.from(links)) {
|
346 | this.collectImportedStylesheets(link, link.sheet, imported);
|
347 | }
|
348 |
|
349 |
|
350 | if (this.window.StyleFix && this.document.querySelectorAll) {
|
351 | for (style of Array.from(this.document.querySelectorAll('style[data-href]'))) {
|
352 | links.push(style);
|
353 | }
|
354 | }
|
355 |
|
356 | this.console.log(`LiveReload found ${links.length} LINKed stylesheets, ${imported.length} @imported stylesheets`);
|
357 |
|
358 | const match = pickBestMatch(
|
359 | path,
|
360 | links.concat(imported),
|
361 | link => pathFromUrl(this.linkHref(link))
|
362 | );
|
363 |
|
364 | if (match) {
|
365 | if (match.object.rule) {
|
366 | this.console.log(`LiveReload is reloading imported stylesheet: ${match.object.href}`);
|
367 | this.reattachImportedRule(match.object);
|
368 | } else {
|
369 | this.console.log(`LiveReload is reloading stylesheet: ${this.linkHref(match.object)}`);
|
370 | this.reattachStylesheetLink(match.object);
|
371 | }
|
372 | } else {
|
373 | if (options.reloadMissingCSS) {
|
374 | this.console.log(`LiveReload will reload all stylesheets because path '${path}' did not match any specific one. \
|
375 | To disable this behavior, set 'options.reloadMissingCSS' to 'false'.`
|
376 | );
|
377 |
|
378 | for (link of Array.from(links)) {
|
379 | this.reattachStylesheetLink(link);
|
380 | }
|
381 | } else {
|
382 | this.console.log(`LiveReload will not reload path '${path}' because the stylesheet was not found on the page \
|
383 | and 'options.reloadMissingCSS' was set to 'false'.`
|
384 | );
|
385 | }
|
386 | }
|
387 |
|
388 | return true;
|
389 | }
|
390 |
|
391 | collectImportedStylesheets (link, styleSheet, result) {
|
392 |
|
393 |
|
394 | let rules;
|
395 |
|
396 | try {
|
397 | rules = (styleSheet || {}).cssRules;
|
398 | } catch (e) {}
|
399 |
|
400 | if (rules && rules.length) {
|
401 | for (let index = 0; index < rules.length; index++) {
|
402 | const rule = rules[index];
|
403 |
|
404 | switch (rule.type) {
|
405 | case CSSRule.CHARSET_RULE:
|
406 | continue;
|
407 | case CSSRule.IMPORT_RULE:
|
408 | result.push({ link, rule, index, href: rule.href });
|
409 | this.collectImportedStylesheets(link, rule.styleSheet, result);
|
410 | break;
|
411 | default:
|
412 | break;
|
413 | }
|
414 | }
|
415 | }
|
416 | }
|
417 |
|
418 | waitUntilCssLoads (clone, func) {
|
419 | const options = this.options || DEFAULT_OPTIONS;
|
420 | let callbackExecuted = false;
|
421 |
|
422 | const executeCallback = () => {
|
423 | if (callbackExecuted) {
|
424 | return;
|
425 | }
|
426 |
|
427 | callbackExecuted = true;
|
428 |
|
429 | return func();
|
430 | };
|
431 |
|
432 |
|
433 |
|
434 |
|
435 | clone.onload = () => {
|
436 | this.console.log('LiveReload: the new stylesheet has finished loading');
|
437 | this.knownToSupportCssOnLoad = true;
|
438 |
|
439 | return executeCallback();
|
440 | };
|
441 |
|
442 | if (!this.knownToSupportCssOnLoad) {
|
443 |
|
444 | let poll;
|
445 | (poll = () => {
|
446 | if (clone.sheet) {
|
447 | this.console.log('LiveReload is polling until the new CSS finishes loading...');
|
448 |
|
449 | return executeCallback();
|
450 | }
|
451 |
|
452 | return this.Timer.start(50, poll);
|
453 | })();
|
454 | }
|
455 |
|
456 |
|
457 | return this.Timer.start(options.stylesheetReloadTimeout, executeCallback);
|
458 | }
|
459 |
|
460 | linkHref (link) {
|
461 |
|
462 | return link.href || (link.getAttribute && link.getAttribute('data-href'));
|
463 | }
|
464 |
|
465 | reattachStylesheetLink (link) {
|
466 |
|
467 | let clone;
|
468 |
|
469 | if (link.__LiveReload_pendingRemoval) {
|
470 | return;
|
471 | }
|
472 |
|
473 | link.__LiveReload_pendingRemoval = true;
|
474 |
|
475 | if (link.tagName === 'STYLE') {
|
476 |
|
477 | clone = this.document.createElement('link');
|
478 | clone.rel = 'stylesheet';
|
479 | clone.media = link.media;
|
480 | clone.disabled = link.disabled;
|
481 | } else {
|
482 | clone = link.cloneNode(false);
|
483 | }
|
484 |
|
485 | clone.href = this.generateCacheBustUrl(this.linkHref(link));
|
486 |
|
487 |
|
488 | const parent = link.parentNode;
|
489 |
|
490 | if (parent.lastChild === link) {
|
491 | parent.appendChild(clone);
|
492 | } else {
|
493 | parent.insertBefore(clone, link.nextSibling);
|
494 | }
|
495 |
|
496 | return this.waitUntilCssLoads(clone, () => {
|
497 | let additionalWaitingTime;
|
498 |
|
499 | if (/AppleWebKit/.test(this.window.navigator.userAgent)) {
|
500 | additionalWaitingTime = 5;
|
501 | } else {
|
502 | additionalWaitingTime = 200;
|
503 | }
|
504 |
|
505 | return this.Timer.start(additionalWaitingTime, () => {
|
506 | if (!link.parentNode) {
|
507 | return;
|
508 | }
|
509 |
|
510 | link.parentNode.removeChild(link);
|
511 | clone.onreadystatechange = null;
|
512 |
|
513 | return (this.window.StyleFix ? this.window.StyleFix.link(clone) : undefined);
|
514 | });
|
515 | });
|
516 | }
|
517 |
|
518 | reattachImportedRule ({ rule, index, link }) {
|
519 | const parent = rule.parentStyleSheet;
|
520 | const href = this.generateCacheBustUrl(rule.href);
|
521 | const media = rule.media.length ? [].join.call(rule.media, ', ') : '';
|
522 | const newRule = `@import url("${href}") ${media};`;
|
523 |
|
524 |
|
525 | rule.__LiveReload_newHref = href;
|
526 |
|
527 |
|
528 |
|
529 |
|
530 | const tempLink = this.document.createElement('link');
|
531 | tempLink.rel = 'stylesheet';
|
532 | tempLink.href = href;
|
533 | tempLink.__LiveReload_pendingRemoval = true;
|
534 |
|
535 | if (link.parentNode) {
|
536 | link.parentNode.insertBefore(tempLink, link);
|
537 | }
|
538 |
|
539 |
|
540 | return this.Timer.start(this.importCacheWaitPeriod, () => {
|
541 | if (tempLink.parentNode) {
|
542 | tempLink.parentNode.removeChild(tempLink);
|
543 | }
|
544 |
|
545 |
|
546 | if (rule.__LiveReload_newHref !== href) {
|
547 | return;
|
548 | }
|
549 |
|
550 | parent.insertRule(newRule, index);
|
551 | parent.deleteRule(index + 1);
|
552 |
|
553 |
|
554 | rule = parent.cssRules[index];
|
555 | rule.__LiveReload_newHref = href;
|
556 |
|
557 |
|
558 | return this.Timer.start(this.importCacheWaitPeriod, () => {
|
559 |
|
560 | if (rule.__LiveReload_newHref !== href) {
|
561 | return;
|
562 | }
|
563 |
|
564 | parent.insertRule(newRule, index);
|
565 |
|
566 | return parent.deleteRule(index + 1);
|
567 | });
|
568 | });
|
569 | }
|
570 |
|
571 | generateUniqueString () {
|
572 | return `livereload=${Date.now()}`;
|
573 | }
|
574 |
|
575 | generateCacheBustUrl (url, expando) {
|
576 | const options = this.options || DEFAULT_OPTIONS;
|
577 | let hash, oldParams;
|
578 |
|
579 | if (!expando) {
|
580 | expando = this.generateUniqueString();
|
581 | }
|
582 |
|
583 | ({ url, hash, params: oldParams } = splitUrl(url));
|
584 |
|
585 | if (options.overrideURL) {
|
586 | if (url.indexOf(options.serverURL) < 0) {
|
587 | const originalUrl = url;
|
588 |
|
589 | url = options.serverURL + options.overrideURL + '?url=' + encodeURIComponent(url);
|
590 |
|
591 | this.console.log(`LiveReload is overriding source URL ${originalUrl} with ${url}`);
|
592 | }
|
593 | }
|
594 |
|
595 | let params = oldParams.replace(/(\?|&)livereload=(\d+)/, (match, sep) => `${sep}${expando}`);
|
596 |
|
597 | if (params === oldParams) {
|
598 | if (oldParams.length === 0) {
|
599 | params = `?${expando}`;
|
600 | } else {
|
601 | params = `${oldParams}&${expando}`;
|
602 | }
|
603 | }
|
604 |
|
605 | return url + params + hash;
|
606 | }
|
607 | };
|
608 |
|
609 | exports.splitUrl = splitUrl;
|
610 | exports.pathFromUrl = pathFromUrl;
|
611 | exports.numberOfMatchingSegments = numberOfMatchingSegments;
|
612 | exports.pickBestMatch = pickBestMatch;
|
613 | exports.pathsMatch = pathsMatch;
|
614 | exports.Reloader = Reloader;
|