зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1243406 - enable CSS/SVG/PNG hot reloading for all devtools panels r=bgrins
This commit is contained in:
Родитель
078eda97bb
Коммит
32bc04cb3c
|
@ -4,7 +4,7 @@
|
|||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
|
||||
<?xml-stylesheet href="debugger.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://devtools/content/debugger/debugger.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://devtools/skin/debugger.css" type="text/css"?>
|
||||
<!DOCTYPE window [
|
||||
|
|
|
@ -18,6 +18,10 @@ const BROWSER_BASED_DIRS = [
|
|||
"resource://devtools/client/shared/redux"
|
||||
];
|
||||
|
||||
function clearCache() {
|
||||
Services.obs.notifyObservers(null, "startupcache-invalidate", null);
|
||||
}
|
||||
|
||||
/*
|
||||
* Create a loader to be used in a browser environment. This evaluates
|
||||
* modules in their own environment, but sets window (the normal
|
||||
|
@ -151,8 +155,9 @@ function BrowserLoaderBuilder(baseURI, window) {
|
|||
|
||||
if (hotReloadEnabled) {
|
||||
const watcher = devtools.require("devtools/client/shared/file-watcher");
|
||||
const onFileChanged = (_, fileURI) => {
|
||||
this.hotReloadFile(window, componentProxies, fileURI);
|
||||
const onFileChanged = (_, relativePath) => {
|
||||
this.hotReloadFile(window, componentProxies,
|
||||
"resource://devtools/" + relativePath);
|
||||
};
|
||||
watcher.on("file-changed", onFileChanged);
|
||||
|
||||
|
@ -185,13 +190,7 @@ BrowserLoaderBuilder.prototype = {
|
|||
});
|
||||
},
|
||||
|
||||
clearCache: function() {
|
||||
Services.obs.notifyObservers(null, "startupcache-invalidate", null);
|
||||
},
|
||||
|
||||
hotReloadFile: function(window, componentProxies, fileURI) {
|
||||
dump("Hot reloading: " + fileURI + "\n");
|
||||
|
||||
if (fileURI.match(/\.js$/)) {
|
||||
// Test for React proxy components
|
||||
const proxy = componentProxies.get(fileURI);
|
||||
|
@ -199,23 +198,9 @@ BrowserLoaderBuilder.prototype = {
|
|||
// Remove the old module and re-require the new one; the require
|
||||
// hook in the loader will take care of the rest
|
||||
delete this.loader.modules[fileURI];
|
||||
this.clearCache();
|
||||
clearCache();
|
||||
this.require(fileURI);
|
||||
}
|
||||
} else if (fileURI.match(/\.css$/)) {
|
||||
const links = [...window.document.getElementsByTagNameNS("http://www.w3.org/1999/xhtml", "link")];
|
||||
links.forEach(link => {
|
||||
if (link.href.indexOf(fileURI) === 0) {
|
||||
const parentNode = link.parentNode;
|
||||
const newLink = window.document.createElementNS("http://www.w3.org/1999/xhtml", "link");
|
||||
newLink.rel = "stylesheet";
|
||||
newLink.type = "text/css";
|
||||
newLink.href = fileURI + "?s=" + Math.random();
|
||||
|
||||
parentNode.insertBefore(newLink, link);
|
||||
parentNode.removeChild(link);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { Services } = require("resource://gre/modules/Services.jsm");
|
||||
const { getTheme } = require("devtools/client/shared/theme");
|
||||
|
||||
function iterStyleNodes(window, func) {
|
||||
for (let node of window.document.childNodes) {
|
||||
// Look for ProcessingInstruction nodes.
|
||||
if (node.nodeType === 7) {
|
||||
func(node);
|
||||
}
|
||||
}
|
||||
|
||||
const links = window.document.getElementsByTagNameNS(
|
||||
"http://www.w3.org/1999/xhtml", "link"
|
||||
);
|
||||
for (let node of links) {
|
||||
func(node);
|
||||
}
|
||||
}
|
||||
|
||||
function replaceCSS(window, fileURI) {
|
||||
const document = window.document;
|
||||
const randomKey = Math.random();
|
||||
Services.obs.notifyObservers(null, "startupcache-invalidate", null);
|
||||
|
||||
// Scan every CSS tag and reload ones that match the file we are
|
||||
// looking for.
|
||||
iterStyleNodes(window, node => {
|
||||
if (node.nodeType === 7) {
|
||||
// xml-stylesheet declaration
|
||||
if (node.data.includes(fileURI)) {
|
||||
const newNode = window.document.createProcessingInstruction(
|
||||
"xml-stylesheet",
|
||||
`href="${fileURI}?s=${randomKey}" type="text/css"`
|
||||
);
|
||||
document.insertBefore(newNode, node);
|
||||
document.removeChild(node);
|
||||
}
|
||||
} else if (node.href.includes(fileURI)) {
|
||||
const parentNode = node.parentNode;
|
||||
const newNode = window.document.createElementNS(
|
||||
"http://www.w3.org/1999/xhtml",
|
||||
"link"
|
||||
);
|
||||
newNode.rel = "stylesheet";
|
||||
newNode.type = "text/css";
|
||||
newNode.href = fileURI + "?s=" + randomKey;
|
||||
|
||||
parentNode.insertBefore(newNode, node);
|
||||
parentNode.removeChild(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _replaceResourceInSheet(sheet, filename, randomKey) {
|
||||
for (let i = 0; i < sheet.cssRules.length; i++) {
|
||||
const rule = sheet.cssRules[i];
|
||||
if (rule.type === rule.IMPORT_RULE) {
|
||||
_replaceResourceInSheet(rule.styleSheet, filename);
|
||||
} else if (rule.cssText.includes(filename)) {
|
||||
// Strip off any existing query strings. This might lose
|
||||
// updates for files if there are multiple resources
|
||||
// referenced in the same rule, but the chances of someone hot
|
||||
// reloading multiple resources in the same rule is very low.
|
||||
const text = rule.cssText.replace(/\?s=0.\d+/g, "");
|
||||
const newRule = (
|
||||
text.replace(filename, filename + "?s=" + randomKey)
|
||||
);
|
||||
|
||||
sheet.deleteRule(i);
|
||||
sheet.insertRule(newRule, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function replaceCSSResource(window, fileURI) {
|
||||
const document = window.document;
|
||||
const randomKey = Math.random();
|
||||
|
||||
// Only match the filename. False positives are much better than
|
||||
// missing updates, as all that would happen is we reload more
|
||||
// resources than we need. We do this because many resources only
|
||||
// use relative paths.
|
||||
const parts = fileURI.split("/");
|
||||
const file = parts[parts.length - 1];
|
||||
|
||||
// Scan every single rule in the entire page for any reference to
|
||||
// this resource, and re-insert the rule to force it to update.
|
||||
for (let sheet of document.styleSheets) {
|
||||
_replaceResourceInSheet(sheet, file, randomKey);
|
||||
}
|
||||
|
||||
for (let node of document.querySelectorAll("img,image")) {
|
||||
if (node.src.startsWith(fileURI)) {
|
||||
node.src = fileURI + "?s=" + randomKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function watchCSS(window) {
|
||||
if (Services.prefs.getBoolPref("devtools.loader.hotreload")) {
|
||||
const watcher = require("devtools/client/shared/file-watcher");
|
||||
|
||||
function onFileChanged(_, relativePath) {
|
||||
if (relativePath.match(/\.css$/)) {
|
||||
if (relativePath.startsWith("client/themes")) {
|
||||
let path = relativePath.replace(/^client\/themes\//, "");
|
||||
|
||||
// Special-case a few files that get imported from other CSS
|
||||
// files. We just manually hot reload the parent CSS file.
|
||||
if (path === "variables.css" || path === "toolbars.css" ||
|
||||
path === "common.css" || path === "splitters.css") {
|
||||
replaceCSS(window, "chrome://devtools/skin/" + getTheme() + "-theme.css");
|
||||
} else {
|
||||
replaceCSS(window, "chrome://devtools/skin/" + path);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
replaceCSS(
|
||||
window,
|
||||
"chrome://devtools/content/" + relativePath.replace(/^client\//, "")
|
||||
);
|
||||
replaceCSS(window, "resource://devtools/" + relativePath);
|
||||
} else if (relativePath.match(/\.(svg|png)$/)) {
|
||||
relativePath = relativePath.replace(/^client\/themes\//, "");
|
||||
replaceCSSResource(window, "chrome://devtools/skin/" + relativePath);
|
||||
}
|
||||
}
|
||||
watcher.on("file-changed", onFileChanged);
|
||||
|
||||
window.addEventListener("unload", () => {
|
||||
watcher.off("file-changed", onFileChanged);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { watchCSS };
|
|
@ -9,6 +9,17 @@ importScripts("resource://gre/modules/osfile.jsm");
|
|||
|
||||
const modifiedTimes = new Map();
|
||||
|
||||
function findSourceDir(path) {
|
||||
if (path === "" || path === "/") {
|
||||
return null;
|
||||
} else if (OS.File.exists(
|
||||
OS.Path.join(path, "devtools/client/shared/file-watcher.js")
|
||||
)) {
|
||||
return path;
|
||||
}
|
||||
return findSourceDir(OS.Path.dirname(path));
|
||||
}
|
||||
|
||||
function gatherFiles(path, fileRegex) {
|
||||
let files = [];
|
||||
const iterator = new OS.File.DirectoryIterator(path);
|
||||
|
@ -60,19 +71,44 @@ function scanFiles(files, onChangedFile) {
|
|||
|
||||
onmessage = function(event) {
|
||||
const { path, fileRegex } = event.data;
|
||||
let info = OS.File.stat(path);
|
||||
const devtoolsPath = event.data.devtoolsPath.replace(/\/$/, "");
|
||||
|
||||
// We need to figure out a src dir to watch. These are the actual
|
||||
// files the user is working with, not the files in the obj dir. We
|
||||
// do this by walking up the filesystem and looking for the devtools
|
||||
// directories, and falling back to the raw path. This means none of
|
||||
// this will work for users who store their obj dirs outside of the
|
||||
// src dir.
|
||||
//
|
||||
// We take care not to mess with the `devtoolsPath` if that's what
|
||||
// we end up using, because it might be intentionally mapped to a
|
||||
// specific place on the filesystem for loading devtools externally.
|
||||
//
|
||||
// `devtoolsPath` is currently the devtools directory inside of the
|
||||
// obj dir, and we search for `devtools/client`, so go up 2 levels
|
||||
// to skip that devtools dir and start searching for the src dir.
|
||||
const searchPoint = OS.Path.dirname(OS.Path.dirname(devtoolsPath));
|
||||
const srcPath = findSourceDir(searchPoint);
|
||||
const rootPath = srcPath ? OS.Path.join(srcPath, "devtools") : devtoolsPath;
|
||||
const watchPath = OS.Path.join(rootPath, path.replace(/^devtools\//, ""));
|
||||
|
||||
const info = OS.File.stat(watchPath);
|
||||
if (!info.isDir) {
|
||||
throw new Error("watcher expects a directory as root path");
|
||||
throw new Error("Watcher expects a directory as root path");
|
||||
}
|
||||
|
||||
// We get a list of all the files upfront, which means we don't
|
||||
// support adding new files. But you need to rebuild Firefox when
|
||||
// adding a new file anyway.
|
||||
const files = gatherFiles(path, fileRegex || /.*/);
|
||||
const files = gatherFiles(watchPath, fileRegex || /.*/);
|
||||
|
||||
// Every second, scan for file changes by stat-ing each of them and
|
||||
// comparing modification time.
|
||||
setInterval(() => {
|
||||
scanFiles(files, changedFile => postMessage(changedFile));
|
||||
scanFiles(files, changedFile => {
|
||||
postMessage({ fullPath: changedFile,
|
||||
relativePath: changedFile.replace(rootPath + "/", "") });
|
||||
});
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
|
|
|
@ -3,16 +3,17 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const { Ci, ChromeWorker } = require("chrome");
|
||||
const { Ci, Cu, ChromeWorker } = require("chrome");
|
||||
const { Services } = require("resource://gre/modules/Services.jsm");
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
|
||||
const HOTRELOAD_PREF = "devtools.loader.hotreload";
|
||||
|
||||
function resolveResourceURI(uri) {
|
||||
function resolveResourcePath(uri) {
|
||||
const handler = Services.io.getProtocolHandler("resource")
|
||||
.QueryInterface(Ci.nsIResProtocolHandler);
|
||||
return handler.resolveURI(Services.io.newURI(uri, null, null));
|
||||
const resolved = handler.resolveURI(Services.io.newURI(uri, null, null));
|
||||
return resolved.replace(/file:\/\//, "");
|
||||
}
|
||||
|
||||
function watchFiles(path, onFileChanged) {
|
||||
|
@ -20,16 +21,6 @@ function watchFiles(path, onFileChanged) {
|
|||
throw new Error("`watchFiles` expects a devtools path");
|
||||
}
|
||||
|
||||
// We need to figure out a local path to watch. We start with
|
||||
// whatever devtools points to.
|
||||
let resolvedRootURI = resolveResourceURI("resource://devtools");
|
||||
if (resolvedRootURI.match(/\/obj\-.*/)) {
|
||||
// Move from the built directory to the user's local files
|
||||
resolvedRootURI = resolvedRootURI.replace(/\/obj\-.*/, "") + "/devtools";
|
||||
}
|
||||
resolvedRootURI = resolvedRootURI.replace(/^file:\/\//, "");
|
||||
const localURI = resolvedRootURI + "/" + path.replace(/^devtools\//, "");
|
||||
|
||||
const watchWorker = new ChromeWorker(
|
||||
"resource://devtools/client/shared/file-watcher-worker.js"
|
||||
);
|
||||
|
@ -39,15 +30,16 @@ function watchFiles(path, onFileChanged) {
|
|||
// chrome). This means that this system will only work when built
|
||||
// files are symlinked, so that these URIs actually read from
|
||||
// local sources. There might be a better way to do this.
|
||||
const relativePath = event.data.replace(resolvedRootURI + "/", "");
|
||||
if (relativePath.startsWith("client/themes")) {
|
||||
onFileChanged(relativePath.replace("client/themes",
|
||||
"chrome://devtools/skin"));
|
||||
}
|
||||
onFileChanged("resource://devtools/" + relativePath);
|
||||
const { relativePath, fullPath } = event.data;
|
||||
onFileChanged(relativePath, fullPath);
|
||||
};
|
||||
|
||||
watchWorker.postMessage({ path: localURI, fileRegex: /\.(js|css)$/ });
|
||||
watchWorker.postMessage({
|
||||
path: path,
|
||||
// We must do this here because we can't access the needed APIs in
|
||||
// a worker.
|
||||
devtoolsPath: resolveResourcePath("resource://devtools"),
|
||||
fileRegex: /\.(js|css|svg|png)$/ });
|
||||
return watchWorker;
|
||||
}
|
||||
|
||||
|
@ -56,11 +48,10 @@ EventEmitter.decorate(module.exports);
|
|||
let watchWorker;
|
||||
function onPrefChange() {
|
||||
if (Services.prefs.getBoolPref(HOTRELOAD_PREF) && !watchWorker) {
|
||||
watchWorker = watchFiles("devtools/client", changedFile => {
|
||||
module.exports.emit("file-changed", changedFile);
|
||||
watchWorker = watchFiles("devtools/client", (relativePath, fullPath) => {
|
||||
module.exports.emit("file-changed", relativePath, fullPath);
|
||||
});
|
||||
}
|
||||
else if(watchWorker) {
|
||||
} else if (watchWorker) {
|
||||
watchWorker.terminate();
|
||||
watchWorker = null;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ DevToolsModules(
|
|||
'autocomplete-popup.js',
|
||||
'browser-loader.js',
|
||||
'css-parsing-utils.js',
|
||||
'css-reload.js',
|
||||
'Curl.jsm',
|
||||
'demangle.js',
|
||||
'developer-toolbar.js',
|
||||
|
|
|
@ -159,9 +159,10 @@
|
|||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
const {require} = Components.utils.import("resource://devtools/shared/Loader.jsm", {});
|
||||
const {gDevTools} = require("devtools/client/framework/devtools");
|
||||
const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
|
||||
const { gDevTools } = require("devtools/client/framework/devtools");
|
||||
const StylesheetUtils = require("sdk/stylesheet/utils");
|
||||
const { watchCSS } = require("devtools/client/shared/css-reload");
|
||||
|
||||
if (documentElement.hasAttribute("force-theme")) {
|
||||
switchTheme(documentElement.getAttribute("force-theme"));
|
||||
|
@ -173,4 +174,6 @@
|
|||
gDevTools.off("pref-changed", handlePrefChange);
|
||||
});
|
||||
}
|
||||
|
||||
watchCSS(window);
|
||||
})();
|
||||
|
|
Загрузка…
Ссылка в новой задаче