The ForgeJS project uses the JSHint and JSCS to maintain a level on consistency and compliancy across all files and the Google Closure Compiler (GCC) to minify and optimize the build version of the library. In order to be compatible with them, a few things need to be respected when writing code.
To maintain a level on consistency across all files, submissions should pass the JSHint task.
The jshint and jscs tasks in Grunt tasks uses restrictive settings. Please ensure that grunt jshint
and grunt jscs
passes (or use grunt lint
to run the two tasks).
To optimize, the compiler relies heavily on property type. A property type, parameter or even a return value, can be typed using the JSDoc. A list of all annotations supported by the compiler is available here.
The following annotations are those which matter the most when using the compiler.
@type {}
: goes right before a variable declaration - JSDoc ref@param {}
: specifies the type of function parameter - JSDoc ref@return {}
: specifies a returned value type - JSDoc ref
@typedef {{}}
: used for new type definition (not necessary for a class, only for an Object structure) - JSDoc ref@property {}
: specifies a property of a custom type - JSDoc ref
All built-in types can be used in the JSDoc, along with all declared types of ForgeJS. But there is a trap with the built-in types: a number
is not the same as a Number
(note the capitalized N). The first one is only a value, while the latter is an Object. They don't represent at all the same thing for the compiler, so be aware of capitalized type. This constraint don't exist with the ForgeJS objects.
If a type is missing, like a custom Object (for example, special JSON format), it can be declared as follows :
/**
* @typedef {{foo:string, bar:AnotherType}}
* @name {MissingType}
* @property {string} foo
* @property {AnotherType} bar
*/
var MissingType; // This is very important
A missing type can be added in the appropriate file in externs/forge
. In order to easily find each type, they are separated across multiple files, each file corresponding to a package. For example, all custom types used in core
are in core.ext.js
. If the file doesn't exist yet, feel free to create it.
Everytime the structure of the config.json
changes, it needs to be updated in the appropriate file in reference/
. It can be either adding new properties to an object, or adding a new object. This reference follows the JSON Schema, a xsd-like document to describe and annotate JSON documents.
In the field properties
of the object to edit, there are an object for each property :
"properties": {
"foo": {
"type": "string",
"title": "Foo",
"description": "The property foo, doing something.",
"example": "foo-string-content"
},
"bar": {
"type": "number",
"title": "Bar",
"description": "The property bar, defining something if foo is longer than 6 characters.",
"example": 42
}
}
Beside the key of the property, corresponding to the one that will be present in the config.json
file, a property is constitued essentially of these four properties :
type
can bestring
,number
,boolean
, or more complex valuestitle
is the name of the property appearing in the documentationdescription
is the description of the property appearing in the documentationexample
is the example of the property appearing in the documentation
There can be more properties, such as a default
value, and a min
and max
value for numbers. For more properties, see the current reference and consult examples on the JSON Schema website.
Not all properties are a string, a number or a boolean. Sometimes we need to specify complex types. Instead of simply put object
as the type, a new object will be specified.
A new object is added to its relevant folder, in reference/
. The structure is the following:
{
"id": "foo",
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "FooConfig",
"description": "An instance of <a href=\"#foo-id\">a foo.</a>",
"type": "object",
"properties": {}
}
id
is the identifier of the object$schema
is the JSON Schema referencetitle
is the name of the object, as used in the sources (with the@type
tag)description
is the description appearing in the documentiontype
is alwaysobject
Properties are then added as described above.
When describing a property being another object, add the reference as follows, with $ref
equals to the id
of the object. There is no need to specify anything more :
"bar": {
"$ref": "foo"
}
Sometimes, the compiler doesn't recognize the type of a variable, especially following conditions. To force an inline (in a function, outside of the jsdoc block) implicit (force it) cast, wrap the member with parenthesis, and put a @type
tag in front of it, like this :
var hello = /** @type {string} */ ( foo.bar() );
Let's say a method takes one optional argument, a number. In this method, this argument is used in a function call, but this function can not take an undefined
argument. To counter that, a check is done on it, the argument takes a default value if undefined.
/** @type {string} */
var default_value = "default";
/**
* @param {string=} optional_arg
*/
function method(optional_arg)
{
if (optional_arg === undefined)
{
optional_arg = default_value;
}
other_function(optional_arg);
};
/**
* @param {string} arg
*/
function other_function(arg)
{
...
}
In this case, everything seems correct, but when calling other_function
, the compiler thinks optional_arg
can still be undefined
. In many case it works perfectly, but sometimes not, so an inline cast needs to be done when calling other_function
.
other_function( /** @type {string} */ (optional_arg) )
Apart from when no other solution can be found, try to avoid inline casting.
Since no good externs for THREE, Hammer and Dash.js could be found, the ones used here are custom. Only the used methods and class are written here, so be sure to complete it if something is missing from it. Those extern files can be found in externs/lib/
, with the extension ext.js
. Below are examples how to complete them.
Never assign a value to a constant.
/** @const */
THREE.MyConstant;
/**
* @constructor
* @param {string} arg1
* @param {number=} arg2
* @return {!THREE.MyClass}
*/
THREE.MyClass = function(arg1, arg2) {};
Never assign a value to a property as well.
/** @type {number} */
THREE.MyClass.prototype.prop;
/**
* @param {(number|THREE.MyClass)} arg1
* @param {number=} arg2
* @param {number=} arg3
* @return {THREE.MyClass}
*/
THREE.MyClass.prototype.myMethod = function(arg1, arg2, arg3) {};
Array
should always be typed, as Array<string>
, never Array
alone. As for the Map
type, even if it does not exist, another type can be declared: Object<string, FORGE.Foo>
.
When creating a new class, the most important annotation is @constructor
. The other, if the class inherits from another, is @extends
. Something like this should be obtained :
/**
* @constructor FORGE.Foo
* @extends {FORGE.Bar}
*/
FORGE.Foo = function()
{
do_something();
FORGE.Bar.call(this);
}
FORGE.Foo.prototype = Object.create(FORGE.Bar.prototype);
FORGE.Foo.prototype.constructor = FORGE.Foo;
Do not comment big chunk of code, as "archive" or "backup". Either use the @deprecated
annotation, or remove it, git is here for the backup. Commented chunks like those can break the build, as used RegExp are not perfect.
When using the Object.defineProperty
on a class, in case the this
keyword is used, the getter
and setter
should be preceded by the @this
annotation like this, mentionning the class the property is being defined on :
/**
* Get and set the bar property.
* @name FORGE.Foo#bar
* @type {string}
*/
Object.defineProperty(FORGE.Foo.prototype, "bar",
{
/** @this {FORGE.Foo} */
get: function()
{
return this._bar;
},
/** @this {FORGE.Foo} */
set: function(value)
{
this._bar = value;
}
});
Singletons need to be avoided at all cost, but if there is no other way around it, it needs to follow this special syntax (even with the tmp
part):
/**
* @constructor
* @extends {FORGE.Bar}
*/
FORGE.Foo = (function(c)
{
var tmp = c();
tmp.prototype = Object.create(FORGE.Bar.prototype);
tmp.prototype.constructor = tmp;
// Prototype is then declared here
tmp.prototype.method = function(args)
{
dosomething();
};
// Return our new instance to be affected to FORGE.Foo
return new tmp();
})(function()
{
return function()
{
// Property of the singleton are declared here
this.prop = null;
FORGE.Bar.call(this);
}
});
The @suppress
annotation should be avoided at all cost: if the compiler is generating a warning, there is a good reason for it.
If an Object
is declared in the following style, it should have all of his properties name without any quotes.
The compiler can respect the Object structure sometimes (like if the Object is simple as { x: 54, y: 879 }
), but the better thing to do is to provide a definition of the Object
and type it with it.
/** @type {CustomFooType} */
var Foo =
{
bar: 2,
baz: true
};
The use of such loop on an Array
is strongly unrecommended, for a lot of reason, including the fact that the variable (i
below) used as "index" of the array is in fact a string
, and thus the compiler does not agree with that.
for (var i in array)
{
do_something(array[i]); // Warning, i is of type string while type number is required
}