closure compiler externs solves an issue but i didn't understand why? - javascript

I compile my source with closure compiler and when i call a function that got an event object from network the application throws an error in console.
The function called is:
/**
* #param {goog.events.Event} event Socket.io-Wrapper Event.
*/
de.my.app.admin.prototype.onSaved = function(event){
var category = event.data[0].category; //<-- here it throws the error because category get compiled.
var id = event.data[0].id;
var oldid = event.data[0].oldid;
[...]
}
the event object looks like this
{ data:{
0: {
category: 'someString',
id: 5,
oldid: -5
} }
[...someMoreValuesAddedBySocketIO...]
}
that is the behavior i expected.
now i add an externs declaration like this to my externs file but i didn't alter the type declaration of the #param at the function and the error disappears:
var xterns;
/**
* #typedef {{
* category : string,
* oldid : number,
* id : number
* }}
*/
xterns.NOTUSEDNAME;
/**
* #type {string}
*/
xterns.NOTUSEDNAME.prototype.category;
/**
* #type {number}
*/
xterns.NOTUSEDNAME.prototype.oldid;
/**
* #type {number}
*/
xterns.NOTUSEDNAME.prototype.id;
In short: I have a #param {goog.events.Event} event declaration and an extern for xterns.NOTUSEDNAME solves the compiler problems...
Can anyone explain why this happens?

This is a common misconception. Closure-compiler will not rename a property if any extern object contains a property of the same name. See the FAQ. If the type based optimizations are enabled, then this is no longer true and I would expect your code to break again.
To make this code type safe and compile without warnings, you would need to either:
Reference the data properties using quoted syntax event.data[0]['category']. Your properties will never be renamed by the compiler using this method (which is often used by JSON Data).
Extend the goog.events.Event type with a custom object that defines the data object as a strongly typed array.
Example:
/**
* #constructor
* #extends {goog.events.Event}
*/
de.my.app.AdminEvent = function() {};
goog.inherits(de.my.app.AdminEvent, goog.events.Event);
/** #type {Array.<{category:string, id:number, oldid:number}>} */
de.my.app.AdminEvent.prototype.data = [];
Depending on your exact situation, an interface might be a better option.

Related

strange object property behavior with closure compiler ADVANCED_OPTIMIZATIONS

When running the following code compiled with ADVANCED_OPTIMIZATIONS, I get an unexpected output (for me at least).
/**
* #typedef {{
* version: string,
* api_host: string
* }}
*/
var AppProps;
/**
*
* #param {AppProps} props
*/
window['app'] = function(props) {
props = props || {};
console.log("props.api_host = ", props.api_host);
console.log("props['api_host'] = ", props['api_host']);
};
Result:
props.api_host = undefined
props['api_host'] = http://localhost:8080
Is this expected behavior for compiled object literals? Should I always be using string accessors for Object properties (something I almost never do)?
Any insight to this behavior will be greatly appreciated.
Cheers and thank you!
PS: closure-compiler version is v20191111, also the compiler has 0 knowledge of the object literal being passed in, with the exception of the #typedef. Function invocation happens outside of the compiled code.
PPS: Here is the rewritten code, which explains the output, but not the reasoning.
console.log("props.api_host \x3d ",a.B);
console.log("props['api_host'] \x3d ",a.api_host)
I would not expect closure-compiler to rewrite the first statement as it has, given the provided #typedef. What am I doing wrong here?
After going through the docs and carefully considering my setup, I have found the errors of my ways, and a satisfactory explanation for the results.
Since everything within the compiled code, including my #typedef, is subject to rewriting, what I actually needed was an extern to describe the shape of this externally provided object.
externs.js
/**
* #interface
*/
function AppProps() {}
/**
* #type {string}
*/
AppProps.prototype.version;
/**
* #type {string}
*/
AppProps.prototype.api_host;
app.js
/**
*
* #param {AppProps} props
*/
window['app'] = function(props) {
props = props || {};
console.log("props.api_host = ", props.api_host);
console.log("props['api_host'] = ", props['api_host']);
};
The resulting output now works as expected.

Javascript Dot-Syntax function disappears in listen callback

I'm trying to update Annotorious (https://annotorious.github.io/#) to the latest version of Closure / Javascript.
When I compile it with "SIMPLE" optimizations, dot-syntax functions seem to disappear when goog.events.listen callbacks are invoked. Here's an example:
Here's the "main":
goog.provide('annotorious.Annotorious');
...
/**
* The main entrypoint to the application. The Annotorious class is instantiated exactly once,
* and added to the global window object as 'window.anno'. It exposes the external JavaScript API
* and internally manages the 'modules'. (Each module is responsible for one particular media
* type - image, OpenLayers, etc.)
* #constructor
*/
annotorious.Annotorious = function() {
/** #private **/
this._isInitialized = false;
/** #private **/
this._modules = [ new annotorious.mediatypes.image.ImageModule() ];
...
In another file (they're all compiled together), we have this:
goog.provide('annotorious.Annotation');
goog.require('annotorious.shape');
/**
* A 'domain class' implementation of the external annotation interface.
* #param {string} src the source URL of the annotated object
* #param {string} text the annotation text
* #param {annotorious.shape.Shape} shape the annotated fragment shape
* #constructor
*/
annotorious.Annotation = function(src, text, shape) {
this.src = src;
this.text = text;
this.shapes = [ shape ];
this['context'] = document.URL; // Prevents dead code removal
}
So we start up the code, and at some point we end in the editor for the annotation, listening for a "Save" button:
goog.provide('annotorious.Editor');
....
/**
* Annotation edit form.
* #param {Object} annotator reference to the annotator
* #constructor
*/
annotorious.Editor = function(annotator) {
this.element = goog.soy.renderAsElement(annotorious.templates.editform);
....
/** #private **/
//this._btnSave = goog.dom.query('.annotorious-editor-button-save', this.element)[0];
this._btnSave = this.element.querySelector('.annotorious-editor-button-save');
...
goog.events.listen(this._btnSave, goog.events.EventType.CLICK, function(event) {
event.preventDefault();
var annotation = self.getAnnotation();
annotator.addAnnotation(annotation);
annotator.stopSelection();
if (self._original_annotation)
annotator.fireEvent(annotorious.events.EventType.ANNOTATION_UPDATED, annotation, annotator.getItem());
else
annotator.fireEvent(annotorious.events.EventType.ANNOTATION_CREATED, annotation, annotator.getItem());
self.close();
});
If I put a breakpoint at "goog.events.listen(this._btnSave...", and type "annotorious.Annotation", I get what I expect:
In fact, it has all kinds of methods:
Then I let the code go, and break in the listener (event.preventDefault();, etc, above):
Now, all the dot-syntax methods are gone.
This of course causes crashes later on.
All of these files are compiled together.
This happens for all callbacks - 'load' events, other ux callbacks, etc.
This must have worked in prior versions of Closure / JS.
What could be causing this?
Thanks!
Ok, I was able to resolve the problem by adding "--assume_function_wrapper" to the closure compiler command line.
Previously, I was compiling the package using Plovr. However, since Plovr hasn't been updated in a long time, I switched to using the closure compiler natively.
Apparently either Plovr provided '--assume_function_wrapper' internally or the version of the compiler Plovr used didn't break the functions as described.
As #john suggests above I'll bet '--assume_function_wrapper' keeps goog.provide from "destroying" namespaces by re-declaring them.

How to annotate #attributes in google-closure interfaces

How can I annotate the interface of a JSON-response in google closure?
I have to use a JSONP interface which actually transforms some XML into JSON and provides it as a parameter to my callback. But unfortunately the origin XML contains some attributes so I get a JSON-Object with #attributes-fields e.g.
{
"output": {
"foo": "bar"
},
"#attributes": {
"baz": "attr"
}
}
I created an google-closure interface so the compiler will not minify my fields by accident and I get the neat auto-complition in my IDE.
/**
* #interface
*/
var JsonResult = function() {};
/**
* #type {JsonResultOutput}
*/
JsonResult.prototype.output;
/**
* #type {JsonResultAttributes}
*/
JsonResult.prototype['#attributes'];
/**
* #interface
*/
var JsonResultOutput = function() {};
/**
* #type {string}
*/
JsonResultOutput.prototype.foo;
/**
* #interface
*/
var JsonResultAttributes = function() {};
/**
* #type {string}
*/
JsonResultAttributes.prototype.baz;
Unfortunetly the compiler fires a warning if I try to annotate fields in brackets and strings. So my question is now: How should I annotate this to remove the warning? Maybe I can just remove this field in the interface, due to the problem that I have to write this field in the same way in the code as well, so the compiler will never minify this field. But I also want to document the object in it's full structure.
This looks like a bug to me. Feel free to file an issue at https://github.com/google/closure-compiler
For a workaround, you'll have to use object literal keys:
JsonResult.prototype = {
/**
* #type {JsonResultAttributes}
*/
'#attributes': {}
};

JsDoc, ES6 and #param {Constructor}

I am trying to use JsDoc to document es6 classes. Can't believe that you can't pass a class as a parameter (a class type, not an instance type).
I've been trying things but can't get this simple code to work so that JsDoc doesn't throw me some warnings.
I can't get it to work unless I create a #typedef for each of my classes, then add manually all own and inherited members to it. Can't even do a mixin!
Has anyone succeeded in passing a constructor/class parameter? So that JsDoc be in the static context, not the instance context?
/**
* #class A
*/
class A {
/**
* #static
*/
static helloFromClassA(){
}
}
/**
* #class B
* #extends A
*/
class B extends A{
/**
* #static
*/
static helloFromClassB(){
}
}
/**
* Class as object
* #param {A} ClassArgument
*/
function fn1(ClassArgument){
ClassArgument.helloFromClassA(); // Unresolved function or method helloFromClassA
// Does not work because ClassArgument is interpreted as an
// instance of A, not A's constructor
}
/**
* // Class as function
* #param {Function} ClassArgument
*/
function fn2(ClassArgument){
ClassArgument.helloFromClassA(); // Unresolved function or method helloFromClassA
// Does not work because ClassArgument is interpreted as an
// empty function, not A's constructor
}
/**
* // Type definition
* #typedef {Object} AClass
* #property {Function} helloFromClassA
* #property {Function} super
*/
/**
* // Trying to mixin the AClass
* #typedef {Object} BClass
* #property {Function} helloFromClassB
* #mixes {AClass}
* #mixes {A}
*/
/**
* // Adding manually all members
* #typedef {Object} BClass2
* #property {Function} helloFromClassB
* #property {Function} helloFromClassA
*/
/**
* #param {BClass} ClassArgument
*/
function fn3(ClassArgument){
ClassArgument.helloFromClassA(); // Unresolved function or method helloFromClassA
// Does not work because the BClass typedef does not take
// into account the mixin from AClass, nor from A
ClassArgument.helloFromClassB(); // No warming
}
/**
* #param {BClass2} ClassArgument
*/
function fn4(ClassArgument){
ClassArgument.helloFromClassA(); // No Warning
ClassArgument.helloFromClassB(); // No warming
// Works because we manually defined the typedef with all own
// and inherited properties. It's a drag.
}
fn1(B);
fn2(B);
fn3(B);
fn4(B);
jsDoc issue : https://github.com/jsdoc3/jsdoc/issues/1088
I've been running into same issue with auto-completion in WebStorm multiple times. While it looks like currently there is no straight-forward way in jsdoc to say that parameter is a reference to a constructor (not to an instance), there is a suggestion from JetBrains team to implement something like #param {typeof Constructor} (where typeof is from typescript) or #param {Constructor.} that was suggested by closure compiler team. You can vote for following issue to get your main concern with WebStorm autocompletion resolved - https://youtrack.jetbrains.com/issue/WEB-17325
In Visual Studio Code, you can specify
#param {function(new:MyClass, SomeArgType, SecondArgType, etc...)}
I'm not sure what spec that syntax comes from or who else supports it, but it Works For Me.
typescript and google closure both support {typeof SomeClass} for referencing a class as a type (not a class instance). Unfortunately jsdoc doesn't support that syntax and will fail to compile it (https://github.com/jsdoc3/jsdoc/issues/1349).
Because I want to type-checking my JavaScript with typescript and also want to generate documentation I made this jsdoc plugin to transform expressions like {typeof SomeClass} into {Class<SomeClass>} so I can have both things: https://github.com/cancerberoSgx/jsdoc-typeof-plugin

typedef of this variable doesn't produce warning

I want to produce a warning for a number if a string is assigned to it. So, I thought typedef of Closure might do this for me. I tried the following -
var Widget = function()
{
/** #typedef {number} */
this.size = null;
};
new Widget().size = "kaboom!"
When I compile it using http://closure-compiler.appspot.com/home it doesn't throw a warning or error. What am I doing wrong? And/or what other tool should I be using?
Turn the optimization up to Advanced in the closure compiler service to catch these warnings. You still won't see any for your example (well, you will see some, but not what you are expecting), because typedefs are used to define custom types. Also, you need to annotate your constructor. Run the following example in advanced mode and you will see your warnings. Instead of making a typedef for a simple thing like number, I would just use #type, but this example is to show you the proper use of typedef.
/** #typedef {number} */
var customType;
/** #constructor */
var Widget = function()
{
/** #type {customType} */
this.size = null;
};
new Widget().size = "kaboom!"

Categories