diff --git a/lib/es5Apply.js b/lib/es5Apply.js new file mode 100644 index 0000000..bef234f --- /dev/null +++ b/lib/es5Apply.js @@ -0,0 +1,45 @@ +(function(define) { +'use strict'; +define(function() { + /** + * Carefully sets the instance's constructor property to the supplied + * constructor, using Object.defineProperty if available. If it can't + * set the constructor in a safe way, it will do nothing. + * + * @param instance {Object} component instance + * @param ctor {Function} constructor + */ + function defineConstructorIfPossible(instance, ctor) { + try { + Object.defineProperty(instance, 'constructor', { + value: ctor, + enumerable: false + }); + } catch(e) { + // If we can't define a constructor, oh well. + // This can happen if in envs where Object.defineProperty is not + // available, or when using cujojs/poly or other ES5 shims + } + } + + return function(func, thisObj, args) { + var result = null; + + if(thisObj && typeof thisObj[func] === 'function') { + func = thisObj[func]; + } + + // detect case when apply is called on constructor and fix prototype chain + if (thisObj === func) { + thisObj = Object.create(func.prototype); + defineConstructorIfPossible(thisObj, func); + func.apply(thisObj, args); + result = thisObj; + } else { + result = func.apply(thisObj, args); + } + + return result; + }; +}); +})(typeof define === 'function' && define.amd ? define : function(factory) { module.exports = factory(); }); diff --git a/lib/es6Apply.js b/lib/es6Apply.js new file mode 100644 index 0000000..41ab28f --- /dev/null +++ b/lib/es6Apply.js @@ -0,0 +1,28 @@ +/* jshint esversion: 6 */ +(function(define) { +'use strict'; +define(function() { + return function(func, thisObj, args) { + var result = null; + + if(thisObj === func || (thisObj && thisObj.constructor === func)) { + /* jshint newcap: false */ + result = new func(...(args||[])); + + // detect broken old prototypes with missing constructor + if (result.constructor !== func) { + Object.defineProperty(result, 'constructor', { + enumerable: false, + value: func + }); + } + } else if(thisObj && typeof thisObj[func] === 'function') { + result = thisObj[func](...args); + } else { + result = func.apply(thisObj, args); + } + + return result; + }; +}); +})(typeof define === 'function' && define.amd ? define : function(factory) { module.exports = factory(); }); diff --git a/lib/instantiate.js b/lib/instantiate.js index fd6f114..cd70ac8 100644 --- a/lib/instantiate.js +++ b/lib/instantiate.js @@ -8,7 +8,9 @@ (function(define){ 'use strict'; define(function() { - var undef; + var undef, universalApply; + + universalApply = require('./universalApply'); /** * Creates an object by either invoking ctor as a function and returning the result, @@ -26,42 +28,19 @@ define(function() { var begotten, ctorResult; if (forceConstructor || (forceConstructor === undef && isConstructor(ctor))) { - begotten = Object.create(ctor.prototype); - defineConstructorIfPossible(begotten, ctor); - ctorResult = ctor.apply(begotten, args); + begotten = ctor; + ctorResult = universalApply(ctor, begotten, args); + if(ctorResult !== undef) { begotten = ctorResult; } - } else { - begotten = ctor.apply(undef, args); - + begotten = universalApply(ctor, undef, args); } return begotten === undef ? null : begotten; }; - /** - * Carefully sets the instance's constructor property to the supplied - * constructor, using Object.defineProperty if available. If it can't - * set the constructor in a safe way, it will do nothing. - * - * @param instance {Object} component instance - * @param ctor {Function} constructor - */ - function defineConstructorIfPossible(instance, ctor) { - try { - Object.defineProperty(instance, 'constructor', { - value: ctor, - enumerable: false - }); - } catch(e) { - // If we can't define a constructor, oh well. - // This can happen if in envs where Object.defineProperty is not - // available, or when using cujojs/poly or other ES5 shims - } - } - /** * Determines whether the supplied function should be invoked directly or * should be invoked using new in order to create the object to be wired. @@ -72,10 +51,17 @@ define(function() { */ function isConstructor(func) { var is = false, p; - for (p in func.prototype) { - if (p !== undef) { - is = true; - break; + + // this has to work, according to spec: + // https://tc39.github.io/ecma262/#sec-function.prototype.tostring + is = is || func.toString().trim().substr(0,5) === 'class'; + + if(!is) { + for (p in func.prototype) { + if (p !== undef) { + is = true; + break; + } } } diff --git a/lib/invoker.js b/lib/invoker.js index f32c61e..aec393e 100644 --- a/lib/invoker.js +++ b/lib/invoker.js @@ -1,11 +1,13 @@ (function(define) { define(function() { + var universalApply = require('./universalApply'); + return function(methodName, args) { return function(target) { - return target[methodName].apply(target, args); + return universalApply(target[methodName], target, args); }; }; }); -})(typeof define === 'function' && define.amd ? define : function(factory) { module.exports = factory(); }); \ No newline at end of file +})(typeof define === 'function' && define.amd ? define : function(factory) { module.exports = factory(); }); diff --git a/lib/plugin/basePlugin.js b/lib/plugin/basePlugin.js index 625e77b..2764ac7 100644 --- a/lib/plugin/basePlugin.js +++ b/lib/plugin/basePlugin.js @@ -14,7 +14,7 @@ define(function(require) { var when, object, functional, pipeline, instantiate, createInvoker, - whenAll, obj, pluginInstance, undef; + whenAll, obj, pluginInstance, undef, universalApply; when = require('when'); object = require('../object'); @@ -22,6 +22,7 @@ define(function(require) { pipeline = require('../pipeline'); instantiate = require('../instantiate'); createInvoker = require('../invoker'); + universalApply = require('../universalApply'); whenAll = when.all; @@ -194,7 +195,6 @@ define(function(require) { if (!proxy.clone) { throw new Error('No clone function found for ' + componentDef.id); } - return proxy.clone(options); }); }); @@ -249,7 +249,7 @@ define(function(require) { // We'll either use the module directly, or we need // to instantiate/invoke it. return constructorName - ? module[constructorName].apply(module, args) + ? universalApply(constructorName, module, args) : typeof module == 'function' ? instantiate(module, args, isConstructor) : Object.create(module); diff --git a/lib/universalApply.js b/lib/universalApply.js new file mode 100644 index 0000000..e4dacaa --- /dev/null +++ b/lib/universalApply.js @@ -0,0 +1,45 @@ +(function(){ + 'use strict'; + + (function(define){ + + function evaluates (statement) { + try { + /* jshint evil: true */ + eval(statement); + /* jshint evil: false */ + return true; + } catch (err) { + return false; + } + } + + // we have to know it synchronously, we are unable to load this module in asynchronous way + // we cannot defer `define` and we cannot load module, that would not compile in browser + // so we can't delegate this check to another module + function isSpreadAvailable() { + return evaluates('Math.max(...[ 5, 10 ])'); + } + + var requires = []; + if (typeof('process') !== 'undefined' && 'ES_VERSION' in process.env) { + requires.push('./es'+process.env.ES_VERSION+'Apply'); + } else { + if(isSpreadAvailable()) { + requires.push('./es6Apply'); + } else { + requires.push('./es5Apply'); + } + } + + define('universalApply', requires, function(apply){ + return apply; + }); + })( + typeof define === 'function' + ? define + : function(name, requires, factory) { + module.exports = factory.apply(null, requires.map(require)); + } + ); +})(); diff --git a/test/buster.js b/test/buster.js index f27728b..5eeb20a 100644 --- a/test/buster.js +++ b/test/buster.js @@ -1,8 +1,42 @@ +'use strict'; + require('gent/test-adapter/buster'); +function evaluates (statement) { + try { + /* jshint evil: true */ + eval(statement); + /* jshint evil: false */ + return true; + } catch (err) { + return false; + } +} + +function isClassAvailable() { + return evaluates('class es6TestClass_ibyechBaloodren7 {}'); +} + +function isSpreadAvailable() { + return evaluates('parseInt(...["20", 10])'); +} + +var tests = ['node/**/*-test.js']; + +console.log('class operator %savailable', isClassAvailable() ? '' : 'not '); +console.log('spread operator %savailable', isSpreadAvailable() ? '' : 'not '); + +if( + isClassAvailable() + && isSpreadAvailable() + && !('ES_VERSION' in process.env && parseFloat(process.env.ES_VERSION) < 6) +) { + tests.push('node-es6/**/*-test.js'); +} + module.exports['node'] = { environment: 'node', - tests: ['node/**/*-test.js'] + tests: tests // TODO: Why doesn't this work? //, testHelpers:['gent/test-adapter/buster'] -}; \ No newline at end of file +}; diff --git a/test/node-es6/lib/plugin/basePlugin-test.js b/test/node-es6/lib/plugin/basePlugin-test.js new file mode 100644 index 0000000..b513266 --- /dev/null +++ b/test/node-es6/lib/plugin/basePlugin-test.js @@ -0,0 +1,95 @@ +/* jshint esversion: 6 */ +(function(buster, context) { +'use strict'; + +var assert, refute, fail, sentinel; + +assert = buster.assert; +refute = buster.refute; +fail = buster.fail; + +sentinel = {}; + +function createContext(spec) { + return context.call(null, spec, null, { require: require }); +} + +class es6Class +{ + constructor () { + this.constructorRan = true; + this.args = Array.prototype.slice.call(arguments); + } + + someMethod() { + + } +} + +buster.testCase('es6/lib/plugin/basePlugin', { + 'clone factory': { + 'should call constructor when cloning an object with an es6 constructor': function() { + class FabulousEs6 { + constructor () { + this.instanceProp = 'instanceProp'; + } + } + FabulousEs6.prototype.prototypeProp = 'prototypeProp'; + + return createContext({ + fab: { + create: FabulousEs6 + }, + copy: { + clone: { $ref: 'fab' } + } + }).then( + function(context) { + assert.defined(context.copy, 'copy is defined'); + assert.defined(context.copy.prototypeProp, 'copy.prototypeProp is defined'); + assert.defined(context.copy.instanceProp, 'copy.instanceProp is defined'); + refute.same(context.copy, context.fab); + }, + fail + ); + } + }, + + 'create factory': { + 'should call es6 constructor': function() { + return createContext({ + test: { + create: { + module: es6Class, + } + } + }).then( + function(context) { + assert(context.test.constructorRan); + }, + fail + ); + }, + + 'should call es6 constructor functions with args': function() { + return createContext({ + test: { + create: { + module: es6Class, + args: [1, 'foo', 1.7] + } + } + }).then( + function(context) { + assert(context.test instanceof es6Class); + assert.equals(context.test.args, [1, 'foo', 1.7]); + }, + fail + ); + }, + } +}); +})( + require('buster'), + require('../../../../lib/context') +);