From 1cee97ca22cfff7a012ed8a7dcfcb676163c0c48 Mon Sep 17 00:00:00 2001 From: Alexander Gugel Date: Thu, 4 Jun 2015 13:21:16 +0200 Subject: [PATCH] feat: Add utilities#assert [#95469256] DOMRenderer previously had methods for asserting state. Putting those methods on the prototype makes it impossible for the minifier to do any kind of dead-code elimination. This commit adds an assert utility function used in order to check if a certain condition is met. Checking for NODE_ENV and not including the passed in error message in the thrown error should be trivial and only needs to be done once in the more generic assert function. --- dom-renderers/DOMRenderer.js | 118 +++++++++------------------------ package.json | 5 ++ scripts/optimize-assertions.js | 83 +++++++++++++++++++++++ utilities/assert.js | 40 +++++++++++ utilities/index.js | 1 + 5 files changed, 162 insertions(+), 85 deletions(-) create mode 100644 scripts/optimize-assertions.js create mode 100644 utilities/assert.js diff --git a/dom-renderers/DOMRenderer.js b/dom-renderers/DOMRenderer.js index adb22ece..6bad0e47 100644 --- a/dom-renderers/DOMRenderer.js +++ b/dom-renderers/DOMRenderer.js @@ -30,6 +30,7 @@ var PathUtils = require('../core/Path'); var vendorPrefix = require('../utilities/vendorPrefix'); var CallbackStore = require('../utilities/CallbackStore'); var eventMap = require('./events/EventMap'); +var assert = require('../utilities/assert'); var TRANSFORM = null; @@ -93,7 +94,6 @@ function DOMRenderer (element, selector, compositor) { this._lastEv = null; } - /** * Attaches an EventListener to the element associated with the passed in path. * Prevents the default browser action on all subsequent events if @@ -110,11 +110,26 @@ function DOMRenderer (element, selector, compositor) { * @return {undefined} undefined */ DOMRenderer.prototype.subscribe = function subscribe(type) { - this._assertTargetLoaded(); + assert(this._target, 'No target loaded'); this._listen(type); this._target.subscribe[type] = true; }; +/** + * Unsubscribes from all events that are of the specified type. + * + * @method + * + * @param {String} type Event type to unsubscribe from. + * @return {undefined} undefined + */ +DOMRenderer.prototype.unsubscribe = function unsubscribe(type) { + assert(this._target, 'No target loaded'); + + this._listen(type); + this._target.subscribe[type] = false; +}; + /** * Used to preventDefault if an event of the specified type is being emitted on * the currently loaded target. @@ -125,7 +140,8 @@ DOMRenderer.prototype.subscribe = function subscribe(type) { * @return {undefined} undefined */ DOMRenderer.prototype.preventDefault = function preventDefault(type) { - this._assertTargetLoaded(); + assert(this._target, 'No target loaded'); + this._listen(type); this._target.preventDefault[type] = true; }; @@ -142,7 +158,8 @@ DOMRenderer.prototype.preventDefault = function preventDefault(type) { * @return {undefined} undefined */ DOMRenderer.prototype.allowDefault = function allowDefault(type) { - this._assertTargetLoaded(); + assert(this._target, 'No target loaded'); + this._listen(type); this._target.preventDefault[type] = false; }; @@ -163,7 +180,7 @@ DOMRenderer.prototype.allowDefault = function allowDefault(type) { * @return {undefined} undefined */ DOMRenderer.prototype._listen = function _listen(type) { - this._assertTargetLoaded(); + assert(this._target, 'No target loaded'); if ( !this._target.listeners[type] && !this._root.listeners[type] @@ -175,19 +192,6 @@ DOMRenderer.prototype._listen = function _listen(type) { } }; -/** - * Unsubscribes from all events that are of the specified type. - * - * @method - * - * @param {String} type DOM event type (e.g. click, mouseover). - * @return {undefined} undefined - */ -DOMRenderer.prototype.unsubscribe = function unsubscribe(type) { - this._assertTargetLoaded(); - this._target.subscribe[type] = false; -}; - /** * Function to be added using `addEventListener` to the corresponding * DOMElement. @@ -236,7 +240,6 @@ DOMRenderer.prototype._triggerEvent = function _triggerEvent(ev) { } }; - /** * getSizeOf gets the dom size of a particular DOM element. This is * needed for render sizing in the scene graph. @@ -308,55 +311,6 @@ DOMRenderer.prototype.draw = function draw(renderState) { } }; - -/** - * Internal helper function used for ensuring that a path is currently loaded. - * - * @method - * @private - * - * @return {undefined} undefined - */ -DOMRenderer.prototype._assertPathLoaded = function _asserPathLoaded() { - if (!this._path) throw new Error('path not loaded'); -}; - -/** - * Internal helper function used for ensuring that a parent is currently loaded. - * - * @method - * @private - * - * @return {undefined} undefined - */ -DOMRenderer.prototype._assertParentLoaded = function _assertParentLoaded() { - if (!this._parent) throw new Error('parent not loaded'); -}; - -/** - * Internal helper function used for ensuring that children are currently - * loaded. - * - * @method - * @private - * - * @return {undefined} undefined - */ -DOMRenderer.prototype._assertChildrenLoaded = function _assertChildrenLoaded() { - if (!this._children) throw new Error('children not loaded'); -}; - -/** - * Internal helper function used for ensuring that a target is currently loaded. - * - * @method _assertTargetLoaded - * - * @return {undefined} undefined - */ -DOMRenderer.prototype._assertTargetLoaded = function _assertTargetLoaded() { - if (!this._target) throw new Error('No target loaded'); -}; - /** * Finds and sets the parent of the currently loaded element (path). * @@ -366,7 +320,7 @@ DOMRenderer.prototype._assertTargetLoaded = function _assertTargetLoaded() { * @return {ElementCache} Parent element. */ DOMRenderer.prototype.findParent = function findParent () { - this._assertPathLoaded(); + assert(this._path, 'No path loaded'); var path = this._path; var parent; @@ -453,7 +407,7 @@ DOMRenderer.prototype.insertEl = function insertEl (tagName) { this.findParent(); - this._assertParentLoaded(); + assert(this._parent, 'No parent loaded'); if (this._parent.void) throw new Error( @@ -475,7 +429,6 @@ DOMRenderer.prototype.insertEl = function insertEl (tagName) { }; - /** * Sets a property on the currently loaded target. * @@ -487,11 +440,10 @@ DOMRenderer.prototype.insertEl = function insertEl (tagName) { * @return {undefined} undefined */ DOMRenderer.prototype.setProperty = function setProperty (name, value) { - this._assertTargetLoaded(); + assert(this._target, 'No target loaded'); this._target.element.style[name] = value; }; - /** * Sets the size of the currently loaded target. * Removes any explicit sizing constraints when passed in `false` @@ -508,7 +460,7 @@ DOMRenderer.prototype.setProperty = function setProperty (name, value) { * @return {undefined} undefined */ DOMRenderer.prototype.setSize = function setSize (width, height) { - this._assertTargetLoaded(); + assert(this._target, 'No target loaded'); this.setWidth(width); this.setHeight(height); @@ -528,7 +480,7 @@ DOMRenderer.prototype.setSize = function setSize (width, height) { * @return {undefined} undefined */ DOMRenderer.prototype.setWidth = function setWidth(width) { - this._assertTargetLoaded(); + assert(this._target, 'No target loaded'); var contentWrapper = this._target.content; @@ -561,7 +513,7 @@ DOMRenderer.prototype.setWidth = function setWidth(width) { * @return {undefined} undefined */ DOMRenderer.prototype.setHeight = function setHeight(height) { - this._assertTargetLoaded(); + assert(this._target, 'No target loaded'); var contentWrapper = this._target.content; @@ -591,7 +543,7 @@ DOMRenderer.prototype.setHeight = function setHeight(height) { * @return {undefined} undefined */ DOMRenderer.prototype.setAttribute = function setAttribute(name, value) { - this._assertTargetLoaded(); + assert(this._target, 'No target loaded'); this._target.element.setAttribute(name, value); }; @@ -605,7 +557,7 @@ DOMRenderer.prototype.setAttribute = function setAttribute(name, value) { * @return {undefined} undefined */ DOMRenderer.prototype.setContent = function setContent(content) { - this._assertTargetLoaded(); + assert(this._target, 'No target loaded'); if (this._target.formElement) { this._target.element.value = content; @@ -629,7 +581,6 @@ DOMRenderer.prototype.setContent = function setContent(content) { ); }; - /** * Sets the passed in transform matrix (world space). Inverts the parent's world * transform. @@ -641,11 +592,10 @@ DOMRenderer.prototype.setContent = function setContent(content) { * @return {undefined} undefined */ DOMRenderer.prototype.setMatrix = function setMatrix (transform) { - this._assertTargetLoaded(); + assert(this._target, 'No target loaded'); this._target.element.style[TRANSFORM] = this._stringifyMatrix(transform); }; - /** * Adds a class to the classList associated with the currently loaded target. * @@ -656,11 +606,10 @@ DOMRenderer.prototype.setMatrix = function setMatrix (transform) { * @return {undefined} undefined */ DOMRenderer.prototype.addClass = function addClass(domClass) { - this._assertTargetLoaded(); + assert(this._target, 'No target loaded'); this._target.element.classList.add(domClass); }; - /** * Removes a class from the classList associated with the currently loaded * target. @@ -672,11 +621,10 @@ DOMRenderer.prototype.addClass = function addClass(domClass) { * @return {undefined} undefined */ DOMRenderer.prototype.removeClass = function removeClass(domClass) { - this._assertTargetLoaded(); + assert(this._target, 'No target loaded'); this._target.element.classList.remove(domClass); }; - /** * Stringifies the passed in matrix for setting the `transform` property. * diff --git a/package.json b/package.json index e2700ec7..5a735249 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,10 @@ "tap-closer": "^1.0.0", "tape": "^4.0.0", "tap-spec": "^4.0.0", + "escodegen": "^1.6.1", + "esprima": "^2.2.0", + "replace-method": "0.0.0", + "through": "^2.3.7", "uglify-js": "^2.4.17" }, "dependencies": { @@ -53,6 +57,7 @@ }, "browserify": { "transform": [ + ["./scripts/optimize-assertions", { "stripErrors": false, "preCheck": true }], "glslify" ] } diff --git a/scripts/optimize-assertions.js b/scripts/optimize-assertions.js new file mode 100644 index 00000000..ec4f1f32 --- /dev/null +++ b/scripts/optimize-assertions.js @@ -0,0 +1,83 @@ +var replaceMethod = require('replace-method'); +var codegen = require('escodegen'); +var esprima = require('esprima'); +var through = require('through'); + +/** + * Browserify transform used for transforming `assert()` function calls. + * + * @param {String} file Original filename of the current source + * file. + * @param {Object} options Configuration object. + * @param {Boolean} [stripErrors=true] Boolean value indicating if the error + * messages should be excluded from the + * bundle. + * @param {Boolean} [preCheck=true] Avoid `assert` function call. + * @return {Stream} `through` stream. + */ +var optimizeAssertionsTransform = function(file, options) { + options = options || {}; + + if (options.stripErrors == null) + options.stripErrors = true; + + if (options.preCheck == null) + options.preCheck = true; + + var stream = through(write, end); + var source = ''; + + function write(chunk) { + source += chunk; + } + + function end() { + var ast; + + try { + ast = esprima.parse(source); + } + catch (e) { + return stream.emit('error', e); + } + + var src = replaceMethod(ast); + src.replace(['assert'], function(node) { + if (options.stripErrors) { + node.arguments.length = 1; + } + + if (options.preCheck) { + node = { + 'type': 'IfStatement', + 'test': { + 'type': 'UnaryExpression', + 'operator': '!', + 'argument': node.arguments[0], + 'prefix': true + }, + 'consequent': node, + 'alternate': null + }; + } + + return node; + }); + + var code; + + try { + code = codegen.generate(ast); + } + catch (e) { + return stream.emit('error', e); + } + + stream.queue(code); + stream.queue(null); + } + + return stream; +}; + +module.exports = optimizeAssertionsTransform; diff --git a/utilities/assert.js b/utilities/assert.js new file mode 100644 index 00000000..81faae22 --- /dev/null +++ b/utilities/assert.js @@ -0,0 +1,40 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2015 Famous Industries Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +'use strict'; + +function assert(ok, errorMsg) { + if (!ok) { + if (!errorMsg) { + throw new Error( + 'Condition not met. Debug in development environment for ' + + 'further inspection and error message.' + ); + } + + throw new Error('Failed assertion: ' + errorMsg); + } +} + +module.exports = assert; diff --git a/utilities/index.js b/utilities/index.js index c0618e7a..4fbc99d6 100644 --- a/utilities/index.js +++ b/utilities/index.js @@ -25,6 +25,7 @@ 'use strict'; module.exports = { + assert: require('./assert'), CallbackStore: require('./CallbackStore'), clamp: require('./clamp'), clone: require('./clone'),