{"id":4690,"date":"2012-04-16T20:35:02","date_gmt":"2012-04-17T03:35:02","guid":{"rendered":"http:\/\/blog.mozilla.org\/addons\/?p=4690"},"modified":"2012-04-16T20:35:02","modified_gmt":"2012-04-17T03:35:02","slug":"code-review-browser-hacking-with-dotjs","status":"publish","type":"post","link":"https:\/\/blog.mozilla.org\/addons\/2012\/04\/16\/code-review-browser-hacking-with-dotjs\/","title":{"rendered":"Code review: browser hacking with dotjs"},"content":{"rendered":"<h3>about:dotjs<\/h3>\n<p><a href=\"https:\/\/github.com\/defunkt\/dotjs\">Dotjs<\/a> was originally created by <a href=\"http:\/\/defunkt.io\/\">Chris Wanstrath<\/a> 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 <a href=\"https:\/\/twitter.com\/#!\/r1cky\">Ricky Rosario<\/a>, and is available on AMO <a href=\"https:\/\/addons.mozilla.org\/en-US\/firefox\/addon\/dotjs\/\">here<\/a> and of course on github <a href=\"https:\/\/github.com\/rlr\/dotjs-addon\">here<\/a>.<\/p>\n<p>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 &#8216;github.com.js&#8217;.<\/p>\n<p>dotjs&#8217; code executes in a nice, simple sequence of events:<\/p>\n<ol>\n<li>a page loads in the browser<\/li>\n<li>dotjs compares the url of the page to a list of files on the filesystem<\/li>\n<li>if we have a match, the contents of the file are loaded and injected into the page<\/li>\n<\/ol>\n<p><!--more--><\/p>\n<h3>SDK API Usage<\/h3>\n<p>The main functionality in dotjs is implemented using <a href=\"https:\/\/github.com\/rlr\/dotjs-addon\/blob\/master\/lib\/main.js#L5:L25-25\">page-mod<\/a>:<\/p>\n<pre lang=\"javascript\">\r\nrequire(\"page-mod\").PageMod({\r\n    include: \"*\",\r\n    contentScriptFile: data.url('dotjs.js'),\r\n    onAttach: function (worker) {\r\n        worker.port.on('init', function(domain) {\r\n            let host = url.URL(domain).host;\r\n            if (!host)\r\n                return;\r\n\r\n            if (host.indexOf('www.') === 0) {\r\n                host = host.substring(4, host.length);\r\n            }\r\n            \r\n            let files = matchFile(host);\r\n\r\n            \/\/ how to tell from here if we actually matched something?\r\n            if (files.match)\r\n                worker.port.emit('load-scripts', files);\r\n        });\r\n    }\r\n});\r\n<\/pre>\n<p>The page-mod implementation is fairly straightforward &#8211; it attaches the &#8216;dotjs.js&#8217; content script to every page loaded by Firefox. In the <code>onAttach<\/code> 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 <code>matchFile<\/code> function and passes the results back to the content script.<\/p>\n<h3>Content Script<\/h3>\n<p>The content script for dotjs is also pretty straightforward:<\/p>\n<pre lang=\"javascript\">\r\n\/*\r\n * catch the 'load-scripts' event and inject the results into the current scope.\r\n *\/\r\n(function() {\r\n    self.port.on(\"load-scripts\", function(msg) {\r\n        \/\/ bail out if we're in an iframe\r\n        if (window.frameElement) return;\r\n        \r\n        if (msg.jquery) {\r\n            eval(msg.jquery);\r\n        }\r\n\r\n        if (msg.js) {\r\n            eval(msg.js);\r\n        }\r\n\r\n        if (msg.coffee) {\r\n            (function() {\r\n                eval(msg.transpiler);\r\n            }).call(window); \/\/ coffee-script.js assumes this === window\r\n            eval(CoffeeScript.compile(msg.coffee));\r\n        }\r\n\r\n        if (msg.css) {\r\n            var headNode = document.querySelector('head');\r\n            var cssNode = document.createElement('style');\r\n            cssNode.innerHTML = msg.css;\r\n            headNode.appendChild(cssNode);\r\n        }\r\n    });\r\n\r\n    if (document.URL.indexOf('http') === 0) {\r\n        self.port.emit('init', document.URL);    \r\n    }\r\n})();\r\n<\/pre>\n<p>The content script does three things:<\/p>\n<ol>\n<li>If we&#8217;re not the top-level document, bail out.<\/li>\n<li>If we get a <em>load-scripts<\/em> event, eval or inject whatever JS, Coffeescript or CSS passed back from the main add-on code.<\/li>\n<li>when the script is loaded, emit the <em>init<\/em> event to the main add-on code.<\/li>\n<\/ol>\n<h3>require(&#8220;chrome&#8221;)<\/h3>\n<p>This add-on does need chrome privileges in order to reliably find the user&#8217;s home directory and resolve the .js and .css directories in a cross-platform compatible way. All of this functionality is contained in the <a href=\"https:\/\/github.com\/rlr\/dotjs-addon\/blob\/master\/lib\/dotjs.js\">dotjs.js module<\/a> in the add-on&#8217;s lib directory. In particular, the dotjs library uses the <code>directory_service<\/code> xpcom service to get the home directory resolve the js\/css directories, as well as the SDK&#8217;s own file module to test if files exist, join paths and read file contents:<\/p>\n<pre lang=\"javascript\">\r\nconst {Cc, Ci} = require('chrome'); \r\nconst data = require(\"self\").data;\r\nconst file = require('file');\r\nconst dirSvc = Cc['@mozilla.org\/file\/directory_service;1'].getService(Ci.nsIProperties),\r\nhomeDir = dirSvc.get('Home', Ci.nsIFile).path,\r\njsDir = homeDir.indexOf('\/') === 0 ? '.js' : 'js';\r\ncssDir = homeDir.indexOf('\/') === 0 ? '.css' : 'css';\r\n<\/pre>\n<p>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&#8217;s url:<\/p>\n<pre lang=\"javascript\">\r\nlet filename = file.join(homeDir, jsDir, files[i]);\r\n\r\nif (file.exists(filename + '.js')) {\r\n    ret.js = file.read(filename + '.js');\r\n    jsmatch = true;\r\n    ret.match = true;\r\n}\r\n<\/pre>\n<p>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:<\/p>\n<pre lang=\"javascript\">\r\nif (file.exists(filename + '.coffee')) {\r\n\tret.transpiler = data.load('coffee-script.js');\r\n    ret.coffee = file.read(filename + '.coffee');\r\n    jsmatch = true;\r\n    ret.match = true;\r\n}\r\n\r\nlet cssname = file.join(homeDir, cssDir, files[i]);\r\nif (file.exists(cssname + '.css')) {\r\n    ret.css = file.read(cssname + '.css');\r\n    ret.match = true;\r\n}\r\n<\/pre>\n<h3>Performance considerations<\/h3>\n<p>When I read Ricky&#8217;s code I really liked it &#8211; it is simple, compact, makes sensible use of the SDK&#8217;s built-in features and design patterns, and also uses Firefox&#8217;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 <a href=\"https:\/\/github.com\/rlr\/dotjs-addon\/pull\/19\">pull request 19<\/a>:<\/p>\n<p><strong>Optimization 1:<\/strong> Ricky&#8217;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. [ <a href=\"https:\/\/github.com\/canuckistani\/dotjs-addon\/commit\/1bf59a254ed3cf8ac4da006deeaf2a4bd0f00a24\">commit<\/a> ]<\/p>\n<p><strong>Optimization 2:<\/strong> 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 &#8216;social plugins&#8217; which can really bog down the browser. [ <a href=\"https:\/\/github.com\/canuckistani\/dotjs-addon\/commit\/7e4007ea95749387a8eb78c4432dfcd50669545d\">commit<\/a> ]<\/p>\n<p><strong>Modularity:<\/strong> 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&#8217;t much of a concern, but isolating chrome-privileged code into modules greatly simplifies the add-on reviewer&#8217;s job, especially for large, complex add-ons. In theory, we could even write tests for dotjs.js!<\/p>\n<p>This is as far as I got. I have considered some ideas for future, mainly along these themes:<\/p>\n<ol>\n<li>We could change the logic to detect url matches without needing to inject an initial content script at all, using tabs.activeTab.url.<\/li>\n<li>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.<\/li>\n<li>when a script is loaded, we could cache the contents in a hash to avoid loading from the filesystem unnecessarily.<\/li>\n<\/ol>\n<p>Admittedly these last optimizations are unlikely to make much difference in real-world performance unless you happen to have a <em>really<\/em> slow hard drive! I think in the meantime I&#8217;d rather spend more time hacking web pages using dotjs!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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 &hellip; <a class=\"go\" href=\"https:\/\/blog.mozilla.org\/addons\/2012\/04\/16\/code-review-browser-hacking-with-dotjs\/\">Read more<\/a><\/p>\n","protected":false},"author":316,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[44,121,588,742],"tags":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v22.5 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Code review: browser hacking with dotjs - Mozilla Add-ons Community Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/blog.mozilla.org\/addons\/2012\/04\/16\/code-review-browser-hacking-with-dotjs\/\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Jeff Griffiths\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"5 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/blog.mozilla.org\/addons\/2012\/04\/16\/code-review-browser-hacking-with-dotjs\/\",\"url\":\"https:\/\/blog.mozilla.org\/addons\/2012\/04\/16\/code-review-browser-hacking-with-dotjs\/\",\"name\":\"Code review: browser hacking with dotjs - Mozilla Add-ons Community Blog\",\"isPartOf\":{\"@id\":\"https:\/\/blog.mozilla.org\/addons\/#website\"},\"datePublished\":\"2012-04-17T03:35:02+00:00\",\"dateModified\":\"2012-04-17T03:35:02+00:00\",\"author\":{\"@id\":\"https:\/\/blog.mozilla.org\/addons\/#\/schema\/person\/e2f4c71eb45392ea29162432c3f1d433\"},\"breadcrumb\":{\"@id\":\"https:\/\/blog.mozilla.org\/addons\/2012\/04\/16\/code-review-browser-hacking-with-dotjs\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/blog.mozilla.org\/addons\/2012\/04\/16\/code-review-browser-hacking-with-dotjs\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/blog.mozilla.org\/addons\/2012\/04\/16\/code-review-browser-hacking-with-dotjs\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/blog.mozilla.org\/addons\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Code review: browser hacking with dotjs\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/blog.mozilla.org\/addons\/#website\",\"url\":\"https:\/\/blog.mozilla.org\/addons\/\",\"name\":\"Mozilla Add-ons Community Blog\",\"description\":\"\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/blog.mozilla.org\/addons\/?s={search_term_string}\"},\"query-input\":\"required name=search_term_string\"}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/blog.mozilla.org\/addons\/#\/schema\/person\/e2f4c71eb45392ea29162432c3f1d433\",\"name\":\"Jeff Griffiths\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/blog.mozilla.org\/addons\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/b07ae75dd1a5414bf30d7f773ccfc894?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/b07ae75dd1a5414bf30d7f773ccfc894?s=96&d=mm&r=g\",\"caption\":\"Jeff Griffiths\"},\"description\":\"Jeff is Product Manager for the Firefox Developer Tools and occasional Open Web hacker, based in Vancouver, BC.\",\"sameAs\":[\"http:\/\/canuckistani.ca\/\",\"https:\/\/x.com\/canuckistani\"],\"url\":\"https:\/\/blog.mozilla.org\/addons\/author\/jgriffithsmozilla-com\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Code review: browser hacking with dotjs - Mozilla Add-ons Community Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/blog.mozilla.org\/addons\/2012\/04\/16\/code-review-browser-hacking-with-dotjs\/","twitter_misc":{"Written by":"Jeff Griffiths","Est. reading time":"5 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/blog.mozilla.org\/addons\/2012\/04\/16\/code-review-browser-hacking-with-dotjs\/","url":"https:\/\/blog.mozilla.org\/addons\/2012\/04\/16\/code-review-browser-hacking-with-dotjs\/","name":"Code review: browser hacking with dotjs - Mozilla Add-ons Community Blog","isPartOf":{"@id":"https:\/\/blog.mozilla.org\/addons\/#website"},"datePublished":"2012-04-17T03:35:02+00:00","dateModified":"2012-04-17T03:35:02+00:00","author":{"@id":"https:\/\/blog.mozilla.org\/addons\/#\/schema\/person\/e2f4c71eb45392ea29162432c3f1d433"},"breadcrumb":{"@id":"https:\/\/blog.mozilla.org\/addons\/2012\/04\/16\/code-review-browser-hacking-with-dotjs\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/blog.mozilla.org\/addons\/2012\/04\/16\/code-review-browser-hacking-with-dotjs\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/blog.mozilla.org\/addons\/2012\/04\/16\/code-review-browser-hacking-with-dotjs\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/blog.mozilla.org\/addons\/"},{"@type":"ListItem","position":2,"name":"Code review: browser hacking with dotjs"}]},{"@type":"WebSite","@id":"https:\/\/blog.mozilla.org\/addons\/#website","url":"https:\/\/blog.mozilla.org\/addons\/","name":"Mozilla Add-ons Community Blog","description":"","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/blog.mozilla.org\/addons\/?s={search_term_string}"},"query-input":"required name=search_term_string"}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/blog.mozilla.org\/addons\/#\/schema\/person\/e2f4c71eb45392ea29162432c3f1d433","name":"Jeff Griffiths","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/blog.mozilla.org\/addons\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/b07ae75dd1a5414bf30d7f773ccfc894?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/b07ae75dd1a5414bf30d7f773ccfc894?s=96&d=mm&r=g","caption":"Jeff Griffiths"},"description":"Jeff is Product Manager for the Firefox Developer Tools and occasional Open Web hacker, based in Vancouver, BC.","sameAs":["http:\/\/canuckistani.ca\/","https:\/\/x.com\/canuckistani"],"url":"https:\/\/blog.mozilla.org\/addons\/author\/jgriffithsmozilla-com\/"}]}},"_links":{"self":[{"href":"https:\/\/blog.mozilla.org\/addons\/wp-json\/wp\/v2\/posts\/4690"}],"collection":[{"href":"https:\/\/blog.mozilla.org\/addons\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.mozilla.org\/addons\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.mozilla.org\/addons\/wp-json\/wp\/v2\/users\/316"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.mozilla.org\/addons\/wp-json\/wp\/v2\/comments?post=4690"}],"version-history":[{"count":0,"href":"https:\/\/blog.mozilla.org\/addons\/wp-json\/wp\/v2\/posts\/4690\/revisions"}],"wp:attachment":[{"href":"https:\/\/blog.mozilla.org\/addons\/wp-json\/wp\/v2\/media?parent=4690"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.mozilla.org\/addons\/wp-json\/wp\/v2\/categories?post=4690"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.mozilla.org\/addons\/wp-json\/wp\/v2\/tags?post=4690"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}