Bug 1243406 - enable CSS/SVG/PNG hot reloading for all devtools panels r=bgrins

This commit is contained in:
James Long 2016-02-26 14:40:38 -05:00
parent ab0e93e9c7
commit 592d3fdfeb
7 changed files with 212 additions and 54 deletions

View File

@ -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 [

View File

@ -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);
}
});
}
}
};

View File

@ -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 };

View File

@ -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);
};

View File

@ -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;
}

View File

@ -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',

View File

@ -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);
})();