Jetpack/Design Guidelines

From MozillaWiki
Jump to: navigation, search
Draft-template-image.png THIS PAGE IS A WORKING DRAFT Pencil-emoji U270F-gray.png
The page may be difficult to navigate, and some information on its subject might be incomplete and/or evolving rapidly.
If you have any questions or ideas, please add them as a new topic on the discussion page.

Goals

Jetpack is intended to improve the following areas of Firefox extensibility:

Compatibility

Large numbers of current addons require work at each major release to maintain compatibility. A major design goal for Jetpack is to provide a base set of functionality needed to extend the browser and create a stable set of interfaces around those features to eliminate these incompatibility problems. In a nutshell, Keep Addons Working Across Major Releases! for the benefit of end-users who like using addons and developers who don't want to chase more frequent Firefox and other product release trains.

Security

Currently, addons can "do anything" once installed. Jetpack is being designed to restrict, isolate, and sandbox the set of capabilities it provides to improve security for users, by extending to addons only the capabilities they need, restrict access to internal implementation details of those capabilities, and make their use of those capabilities more visible to code reviewers and users.

Usability

Historically, Firefox API design has been limited by the requirements of XPCOM/XPIDL and has not taken advantage of improvements in the expressiveness of the JavaScript language and achievements in API ergonomics by JavaScript libraries. Jetpack will provide a JS-only API with a strong focus on simplicity, productivity, and flexibility.

Principles

Jetpack APIs should be:

  • simple, eschewing obfuscation
  • minimal, providing functionality that meets common use cases, not every possible case
  • easy to learn and use, even without documentation
  • hard to misuse, whether intentionally or unintentionally
  • consistent, both within each API and across the entire API set provided by Jetpack
    • similar object properties and method parameters should have similar names
    • similar method parameters should be in the same position in call signatures
  • flexible, so adaptable to unanticipated use cases (although not at the expense of overgenericism)
  • reusable, for use in higher-level and composed APIs
  • completely specified, in Jetpack Enhancement Proposals (JEPs)
  • completely documented, in Jetpack documentation

Recommendations

Note: these are recommendations, not rules, so you can break them, but you should do so only with good reason (and in consultation with Jetpack leads).

Jetpack APIs are implemented by Cuddlefish modules conforming to the CommonJS specification. Modules that provide functionality via a singleton should provide it via the simplest distinct nonabbreviated name.

let storage = require("simple-storage").storage;

[Myk: for modules that only export a single symbol, it would be even simpler for the require call to return the singleton itself. We could enable this with some Cuddlefish changes, but it violates CommonJS convention and would complicate migration to a native Javascript module implementation in the future, were one to be implemented, as such an implementation would be unlikely to support this behavior.]

Modules that enable extensions to create multiple instances of an object should export a constructor function for it. Constructors should take a single parameter, options, that is a property collection (i.e. object) of name/value pairs with which to configure the properties of the instance.

let Panel = require("panel").Panel;
let panel = new Panel({ content: "...", width: 200, height: 300 });
// panel.content, panel.width, and panel.height are defined.

It should also be possible to construct instances of an object by calling its constructor without the new operator.

let panel = require("panel").Panel();

Properties that don't have to remain constant over the life of an instance should be settable and deletable after construction by directly setting and deleting the properties.

let Panel = require("panel").Panel;
let panel = new Panel();

// Set some properties.
panel.content = "...";
panel.width = 200;
panel.height = 300;

// Change the value of a property.
panel.height = 400;

// Delete a property.
delete panel.height;

Properties that can accept either a single or multiple unordered values should be settable to either a single or an array of values, and it should be possible to add and remove individual values from their set of values by calling add and remove methods on the property:

let Foo = require("foo").Foo;
let foo = new Foo();
foo.bar = ["a", "b", "c"];
foo.bar.add("d");
foo.bar.remove(["a", "b"]);
// foo.bar now has the values "c" and "d".

It should also be possible to enumerate multi-value properties via for each.. in loops:

for each (let val in foo.bar) { ... }

The add/remove methods on multi-value properties should remove primitive values (strings, numbers, booleans) by value and non-primitive values by reference:

foo.bar.add("a");
foo.bar.add(function() alert("foo!"));
let c = function() alert("bar!");
foo.bar.add(c);

foo.bar.remove("a");
// a is removed (the string "a" is a primitive value).
foo.bar.remove(function() alert("foo!"));
// function() alert("foo!") is not removed (the function being removed
// refers to a different instance of a function that was added).
foo.bar.add(c);
// c is removed (it refers to the same instance of a function that was added).


Method names should be verbs, e.g. show, post. Property names should be the simplest accurate conventional or idiomatic label for their value (e.g. persistent for the boolean property of a visual feature that specifies whether or not the feature remains visible all the time).

Instances should have a destroy method that unloads any resources and breaks any cyclical references associated with the object. The method should be called automatically when the extension that instantiated the object is unloaded. Extensions should also be able to call it to unload those resources and break those references once the object is no longer required.

let Panel = require("panel").Panel;
let panel = new Panel();

panel.destroy();

Callback interfaces should be implemented as properties to which callback functions can be set. Callback properties should be settable and deletable like other properties. Callback properties should be named "on" followed by the CamelCase present-tense (if verb) name of the event/notification triggering the callback, e.g. onClick, onLoad, onSelect.

let Panel = require("panel").Panel;

// Set a callback on construction.
let panel = new Panel({ onShow: function() { ... } });

// Set a callback after construction.
panel.onShow = function() { ... };

// Remove a callback.
delete panel.onShow;

When callbacks are invoked, the this object of the function call should be the object on which the callback is registered.

[Myk: recommendations for positioning of callback arguments is TBD.]

When capitalizing verbs in method and callback property names, only the first letter of verbs comprising a stem and affix (e.g. "upload") should be capitalized, while the first letters of all words of phrasal verbs (verbs comprising multiple words, like "sign in") should be capitalized, e.g. "onUpload" but "onSignIn".

A quick technique for differentiating these is to consider the simple imperative sentence "[verb] it": "upload it" sounds right, while "sign in it" doesn't (it should be "sign it in"), suggesting that the former is a verb comprising a stem and affix while the latter is a phrasal verb.

Event callback properties should be multi-value properties, so it should be possible to specify multiple callbacks for an event by assigning an array to the event's callback property or by calling its add and remove methods:

let foo = function() { ... };
let bar = function() { ... };
const Panel = require("panel").Panel;
let panel = new Panel({ onShow: [ foo, bar ] });
panel.show(); // foo and bar are both called.

let baz = function() { ... };
panel.onShow.add(baz);
panel.show(); // foo, bar, and baz are all called.

panel.onShow.remove(bar);
panel.show(); // Only foo and baz are called.

[Myk: the reasoning here is that the common case is for consumers to register a single callback, so that should be the simplest option, but there are use cases for consumers to register multiple callbacks, so that should also be possible. jQuery uses bind/unbind as callback addition/removal methods for certain kinds of objects, but "bind" is a built-in in the new ECMAScript 5, so we shouldn't use it here to avoid conflation with that meaning. jQuery's API also supports generic and namespaced event handlers, but we don't currently have well-defined common use cases for those features, so we shouldn't support them at this time.]

Interfaces that accept rich content for display (e.g. panel, visual-feature) should do so via a property named content. The property should accept a string of HTML, an E4X XML object, or a URL from which the content should be loaded.

Interfaces should take advantage of composition to reduce repetition and complexity. For example, interfaces that display a panel should solicit it from their consumers in the form of a property that accepts a Panel object.

The names of modules, singletons, constructors, properties, and methods should be the simplest distinct word or phrase that accurately describes the thing being named, with single full words being preferable to acronyms (e.g. Request rather than XHR), abbreviations (e.g. Separator instead of Sep), and phrases (e.g. persistent rather than alwaysDisplay and precede instead of insertBefore).

References