about:dotjs
Dotjs was originally created by Chris Wanstrath as a Google Chrome extension and as of this writing has also been ported to Firefox, Safari and Fluid. The SDK-based Firefox add-on was ported by Mozilla WebDev Ricky Rosario, and is available on AMO here and of course on github here.
The idea behind dotjs is nice and simple: you create a .js directory in your home directory and put your content scripts there. To load a script on github.com, you just name that script ‘github.com.js’.
dotjs’ code executes in a nice, simple sequence of events:
- a page loads in the browser
- dotjs compares the url of the page to a list of files on the filesystem
- if we have a match, the contents of the file are loaded and injected into the page
SDK API Usage
The main functionality in dotjs is implemented using page-mod:
require("page-mod").PageMod({
include: "*",
contentScriptFile: data.url('dotjs.js'),
onAttach: function (worker) {
worker.port.on('init', function(domain) {
let host = url.URL(domain).host;
if (!host)
return;
if (host.indexOf('www.') === 0) {
host = host.substring(4, host.length);
}
let files = matchFile(host);
// how to tell from here if we actually matched something?
if (files.match)
worker.port.emit('load-scripts', files);
});
}
});
The page-mod implementation is fairly straightforward – it attaches the ‘dotjs.js’ content script to every page loaded by Firefox. In the onAttach
handler the worker is set up to listen for an init event; when it recieves this event it then gets a list of files from the matchFile
function and passes the results back to the content script.
Content Script
The content script for dotjs is also pretty straightforward:
/*
* catch the 'load-scripts' event and inject the results into the current scope.
*/
(function() {
self.port.on("load-scripts", function(msg) {
// bail out if we're in an iframe
if (window.frameElement) return;
if (msg.jquery) {
eval(msg.jquery);
}
if (msg.js) {
eval(msg.js);
}
if (msg.coffee) {
(function() {
eval(msg.transpiler);
}).call(window); // coffee-script.js assumes this === window
eval(CoffeeScript.compile(msg.coffee));
}
if (msg.css) {
var headNode = document.querySelector('head');
var cssNode = document.createElement('style');
cssNode.innerHTML = msg.css;
headNode.appendChild(cssNode);
}
});
if (document.URL.indexOf('http') === 0) {
self.port.emit('init', document.URL);
}
})();
The content script does three things:
- If we’re not the top-level document, bail out.
- If we get a load-scripts event, eval or inject whatever JS, Coffeescript or CSS passed back from the main add-on code.
- when the script is loaded, emit the init event to the main add-on code.
require(“chrome”)
This add-on does need chrome privileges in order to reliably find the user’s home directory and resolve the .js and .css directories in a cross-platform compatible way. All of this functionality is contained in the dotjs.js module in the add-on’s lib directory. In particular, the dotjs library uses the directory_service
xpcom service to get the home directory resolve the js/css directories, as well as the SDK’s own file module to test if files exist, join paths and read file contents:
const {Cc, Ci} = require('chrome');
const data = require("self").data;
const file = require('file');
const dirSvc = Cc['@mozilla.org/file/directory_service;1'].getService(Ci.nsIProperties),
homeDir = dirSvc.get('Home', Ci.nsIFile).path,
jsDir = homeDir.indexOf('/') === 0 ? '.js' : 'js';
cssDir = homeDir.indexOf('/') === 0 ? '.css' : 'css';
Once we have a handle on the .js directory, we loop through the files in it to find a match based on the current web page’s url:
let filename = file.join(homeDir, jsDir, files[i]);
if (file.exists(filename + '.js')) {
ret.js = file.read(filename + '.js');
jsmatch = true;
ret.match = true;
}
Ricky also added in Coffeescript and CSS support as a nice extra feature, so that you can write Coffeescript content scripts or CSS style-sheets and have them injected into the target site by the add-on:
if (file.exists(filename + '.coffee')) {
ret.transpiler = data.load('coffee-script.js');
ret.coffee = file.read(filename + '.coffee');
jsmatch = true;
ret.match = true;
}
let cssname = file.join(homeDir, cssDir, files[i]);
if (file.exists(cssname + '.css')) {
ret.css = file.read(cssname + '.css');
ret.match = true;
}
Performance considerations
When I read Ricky’s code I really liked it – it is simple, compact, makes sensible use of the SDK’s built-in features and design patterns, and also uses Firefox’s internal apis to get filesystem access when needed. I have also been involved with lots of discussions about various add-on memory use complaints, and wondered if I would make dotjs a little more efficient. The first real attempt at this encapsulated in pull request 19:
Optimization 1: Ricky’s original code always loaded jQuery into every page loaded in the browser. Instead, I opted to remove the jQuery dependency for the initial content script. This way jQuery is only loaded if there is a file match. [ commit ]
Optimization 2: when using page-mod, always bail out of documents that are in a frame. Social networking sites in particular are guilty of embedding lots of iframes as ‘social plugins’ which can really bog down the browser. [ commit ]
Modularity: originally the file matching code was located in main.js, but I opted to move it into dotjs.js as a separate CommonJS module, mainly so I could keep the chrome-privileged code as separate as possible. For such a small add-on as dotjs this isn’t much of a concern, but isolating chrome-privileged code into modules greatly simplifies the add-on reviewer’s job, especially for large, complex add-ons. In theory, we could even write tests for dotjs.js!
This is as far as I got. I have considered some ideas for future, mainly along these themes:
- We could change the logic to detect url matches without needing to inject an initial content script at all, using tabs.activeTab.url.
- instead of looking the scripts in ~/.js via synchronous file access, it would probably be better to get an initial list asnychronously on start-up and then get OS-level filesystem change notifications.
- when a script is loaded, we could cache the contents in a hash to avoid loading from the filesystem unnecessarily.
Admittedly these last optimizations are unlikely to make much difference in real-world performance unless you happen to have a really slow hard drive! I think in the meantime I’d rather spend more time hacking web pages using dotjs!
Mardeg wrote on
Jeff Griffiths wrote on
Frz wrote on
Jeff Griffiths wrote on
ochameau wrote on
Jeff Griffiths wrote on
Nick wrote on