css-reload.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. const { Services } = require("resource://gre/modules/Services.jsm");
  6. const { getTheme } = require("devtools/client/shared/theme");
  7. function iterStyleNodes(window, func) {
  8. for (let node of window.document.childNodes) {
  9. // Look for ProcessingInstruction nodes.
  10. if (node.nodeType === 7) {
  11. func(node);
  12. }
  13. }
  14. const links = window.document.getElementsByTagNameNS(
  15. "http://www.w3.org/1999/xhtml", "link"
  16. );
  17. for (let node of links) {
  18. func(node);
  19. }
  20. }
  21. function replaceCSS(window, fileURI) {
  22. const document = window.document;
  23. const randomKey = Math.random();
  24. Services.obs.notifyObservers(null, "startupcache-invalidate", null);
  25. // Scan every CSS tag and reload ones that match the file we are
  26. // looking for.
  27. iterStyleNodes(window, node => {
  28. if (node.nodeType === 7) {
  29. // xml-stylesheet declaration
  30. if (node.data.includes(fileURI)) {
  31. const newNode = window.document.createProcessingInstruction(
  32. "xml-stylesheet",
  33. `href="${fileURI}?s=${randomKey}" type="text/css"`
  34. );
  35. document.insertBefore(newNode, node);
  36. document.removeChild(node);
  37. }
  38. } else if (node.href.includes(fileURI)) {
  39. const parentNode = node.parentNode;
  40. const newNode = window.document.createElementNS(
  41. "http://www.w3.org/1999/xhtml",
  42. "link"
  43. );
  44. newNode.rel = "stylesheet";
  45. newNode.type = "text/css";
  46. newNode.href = fileURI + "?s=" + randomKey;
  47. parentNode.insertBefore(newNode, node);
  48. parentNode.removeChild(node);
  49. }
  50. });
  51. }
  52. function _replaceResourceInSheet(sheet, filename, randomKey) {
  53. for (let i = 0; i < sheet.cssRules.length; i++) {
  54. const rule = sheet.cssRules[i];
  55. if (rule.type === rule.IMPORT_RULE) {
  56. _replaceResourceInSheet(rule.styleSheet, filename);
  57. } else if (rule.cssText.includes(filename)) {
  58. // Strip off any existing query strings. This might lose
  59. // updates for files if there are multiple resources
  60. // referenced in the same rule, but the chances of someone hot
  61. // reloading multiple resources in the same rule is very low.
  62. const text = rule.cssText.replace(/\?s=0.\d+/g, "");
  63. const newRule = (
  64. text.replace(filename, filename + "?s=" + randomKey)
  65. );
  66. sheet.deleteRule(i);
  67. sheet.insertRule(newRule, i);
  68. }
  69. }
  70. }
  71. function replaceCSSResource(window, fileURI) {
  72. const document = window.document;
  73. const randomKey = Math.random();
  74. // Only match the filename. False positives are much better than
  75. // missing updates, as all that would happen is we reload more
  76. // resources than we need. We do this because many resources only
  77. // use relative paths.
  78. const parts = fileURI.split("/");
  79. const file = parts[parts.length - 1];
  80. // Scan every single rule in the entire page for any reference to
  81. // this resource, and re-insert the rule to force it to update.
  82. for (let sheet of document.styleSheets) {
  83. _replaceResourceInSheet(sheet, file, randomKey);
  84. }
  85. for (let node of document.querySelectorAll("img,image")) {
  86. if (node.src.startsWith(fileURI)) {
  87. node.src = fileURI + "?s=" + randomKey;
  88. }
  89. }
  90. }
  91. function watchCSS(window) {
  92. if (Services.prefs.getBoolPref("devtools.loader.hotreload")) {
  93. const watcher = require("devtools/client/shared/devtools-file-watcher");
  94. function onFileChanged(_, relativePath) {
  95. if (relativePath.match(/\.css$/)) {
  96. if (relativePath.startsWith("client/themes")) {
  97. let path = relativePath.replace(/^client\/themes\//, "");
  98. // Special-case a few files that get imported from other CSS
  99. // files. We just manually hot reload the parent CSS file.
  100. if (path === "variables.css" || path === "toolbars.css" ||
  101. path === "common.css" || path === "splitters.css") {
  102. replaceCSS(window, "chrome://devtools/skin/" + getTheme() + "-theme.css");
  103. } else {
  104. replaceCSS(window, "chrome://devtools/skin/" + path);
  105. }
  106. return;
  107. }
  108. replaceCSS(
  109. window,
  110. "chrome://devtools/content/" + relativePath.replace(/^client\//, "")
  111. );
  112. replaceCSS(window, "resource://devtools/" + relativePath);
  113. } else if (relativePath.match(/\.(svg|png)$/)) {
  114. relativePath = relativePath.replace(/^client\/themes\//, "");
  115. replaceCSSResource(window, "chrome://devtools/skin/" + relativePath);
  116. }
  117. }
  118. watcher.on("file-changed", onFileChanged);
  119. window.addEventListener("unload", () => {
  120. watcher.off("file-changed", onFileChanged);
  121. });
  122. }
  123. }
  124. module.exports = { watchCSS };