├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── es6-class ├── bower.json ├── lib └── index.js ├── package.json └── test ├── examples ├── anonymous-class.js ├── call-super-function.js ├── class-expressions.js ├── class-extend.js ├── class-with-constructor.js ├── class-with-method-declaration.js ├── empty-named-class.js ├── enumerable.js ├── explicit-super-in-constructor.js ├── extends-null.js ├── getter-setter-super.js ├── getter-setter.js ├── implicit-superclass.js ├── method-declaration-with-arguments.js ├── methods-are-writable.js ├── methods-with-rest-params.js ├── static-getter.js ├── static-method.js ├── static-setter.js ├── strict-mode.js └── super-change-proto.js └── runner.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test/results 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.8.2 2 | 3 | * Do not bother calling defineProperties without any descriptors. 4 | 5 | ## v0.8.1 6 | 7 | * Generate the correct `expression` property for function expressions (escodegen support). 8 | 9 | ## v0.8.0 10 | 11 | * Update dependencies. 12 | 13 | ## v0.7.0 14 | 15 | * Use Facebook's fork of Esprima. 16 | 17 | ## v0.6.0 18 | 19 | * Use recast's `visit` method instead of `traverse`. 20 | 21 | ## v0.5.2 22 | 23 | * Ensure that static getters and setters work correctly. 24 | 25 | ## v0.5.1 26 | 27 | * Ensure that getters and setters are enumerable. 28 | 29 | ## v0.5.0 30 | 31 | * Ensure that classes run in strict mode. 32 | 33 | ## v0.4.3 34 | 35 | * Use [ast-util](https://github.com/eventualbuddha/ast-util) for a variety of 36 | ast-generation tasks. 37 | 38 | ## v0.4.2 39 | 40 | * Adhere to the spec by making class methods writable. 41 | 42 | ## v0.4.1 43 | 44 | * Ensure extending `null` works. 45 | * Ensure super calls in static methods of anonymous classes work. 46 | 47 | ## v0.4.0 48 | 49 | * Add support for class expressions. 50 | * Add support for anonymous classes. 51 | * Ensure that super classes are captured at time of class definition. 52 | 53 | ## v0.3.3 54 | 55 | * Fix a typo that caused default params in class method not to work. 56 | 57 | ## v0.3.2 58 | 59 | * Ensure that rest and default params work in constructors. 60 | 61 | ## v0.3.1 62 | 63 | * Ensure rest params work in class methods. 64 | * Fix that there could not be a static and non-static property of the same name. 65 | 66 | ## v0.3.0 67 | 68 | * Add support for static methods (thanks, @thomasAboyt). 69 | 70 | ## v0.2.0 71 | 72 | * Change the API to be in line with es6-arrow-function and regenerator. 73 | 74 | ## v0.1.0 75 | 76 | * Ensure that prototype properties are, by default, non-enumerable. 77 | 78 | ## v0.0.2 79 | 80 | * README updates. 81 | 82 | ## v0.0.1 83 | 84 | * Initial version of project. 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Square Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # es6-class 2 | 3 | Compiles JavaScript written using ES6 classes to use ES5-compatible function 4 | syntax. For example, this: 5 | 6 | ```js 7 | class Person { 8 | constructor(firstName, lastName) { 9 | this.firstName = firstName; 10 | this.lastName = lastName; 11 | } 12 | 13 | get name() { 14 | return this.firstName + ' ' + this.lastName; 15 | } 16 | 17 | toString() { 18 | return this.name; 19 | } 20 | } 21 | ``` 22 | 23 | compiles to the equivalent with `Person` as a function. See the [esnext demo 24 | page](https://esnext.github.io/esnext) for more on the behavior and generated 25 | JavaScript. 26 | 27 | For more information about the proposed syntax, see the [wiki page on 28 | classes](http://wiki.ecmascript.org/doku.php?id=strawman:maximally_minimal_classes). 29 | 30 | ## Install 31 | 32 | ``` 33 | $ npm install es6-class 34 | ``` 35 | 36 | ## Usage 37 | 38 | ```js 39 | $ node 40 | > var compile = require('es6-class').compile; 41 | ``` 42 | 43 | Without arguments: 44 | 45 | ```js 46 | > compile('class Foo {}'); 47 | 'var Foo = (function() {\n function Foo() {}\n return Foo;\n})();' 48 | ``` 49 | 50 | ## Browserify 51 | 52 | Browserify support is built in. 53 | 54 | ``` 55 | $ npm install es6-class # install local dependency 56 | $ browserify -t es6-class $file 57 | ``` 58 | 59 | ## Contributing 60 | 61 | [![Build Status](https://travis-ci.org/esnext/es6-class.png?branch=master)](https://travis-ci.org/esnext/es6-class) 62 | 63 | ### Setup 64 | 65 | First, install the development dependencies: 66 | 67 | ``` 68 | $ npm install 69 | ``` 70 | 71 | Then, try running the tests: 72 | 73 | ``` 74 | $ npm test 75 | ``` 76 | 77 | To run specific example files: 78 | 79 | ``` 80 | $ node test/runner test/examples/my-example.js test/examples/other-example.js 81 | ``` 82 | 83 | ### Pull Requests 84 | 85 | 1. Fork it 86 | 2. Create your feature branch (`git checkout -b my-new-feature`) 87 | 3. Commit your changes (`git commit -am 'Add some feature'`) 88 | 4. Push to the branch (`git push origin my-new-feature`) 89 | 5. Create new Pull Request 90 | 91 | Any contributors to the master es6-class repository must sign the [Individual 92 | Contributor License Agreement (CLA)][cla]. It's a short form that covers our 93 | bases and makes sure you're eligible to contribute. 94 | 95 | [cla]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 96 | 97 | When you have a change you'd like to see in the master repository, [send a pull 98 | request](https://github.com/esnext/es6-class/pulls). Before we merge your 99 | request, we'll make sure you're in the list of people who have signed a CLA. 100 | -------------------------------------------------------------------------------- /bin/es6-class: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | function showUsage(code) { 4 | var name = require('path').basename(process.argv[1]); 5 | console.log('Usage:', name, '[options] [--] [FILE]'); 6 | console.log(); 7 | console.log('Options:'); 8 | console.log(); 9 | console.log(' -h, --help prints this usage info'); 10 | console.log(); 11 | console.log('Examples:'); 12 | console.log(); 13 | console.log(' # process a file and output to stdout'); 14 | console.log(' ' + name + ' app.js'); 15 | console.log(); 16 | console.log(' # process from stdin to stdout'); 17 | console.log(' echo "class Animal {}" | ' + name); 18 | process.exit(+code); 19 | } 20 | 21 | module.exports = require('..'); 22 | 23 | var run = module.exports.run = function run() { 24 | var inRawArgs = false; 25 | var args = []; 26 | 27 | [].forEach.call(arguments, function(arg) { 28 | if (!inRawArgs) { 29 | switch(arg) { 30 | case '--': 31 | inRawArgs = true; 32 | break; 33 | 34 | case '-h': 35 | case '--help': 36 | showUsage(); 37 | break; 38 | 39 | default: 40 | args.push(arg); 41 | } 42 | } else { 43 | args.push(arg); 44 | } 45 | }); 46 | 47 | if (args.length > 1) { 48 | showUsage(1); 49 | } 50 | 51 | var stream; 52 | if (process.stdin.isTTY) { 53 | if (args.length === 0) { 54 | showUsage(1); 55 | } 56 | stream = require('fs').createReadStream(args[0]); 57 | } else { 58 | if (args.length !== 0) { 59 | showUsage(1); 60 | } 61 | stream = process.stdin; 62 | } 63 | stream.pipe(module.exports()).pipe(process.stdout); 64 | }; 65 | 66 | if (require.main === module) { 67 | run.apply(null, process.argv.slice(2)); 68 | } 69 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "es6-class", 3 | "version": "0.9.5", 4 | "homepage": "https://github.com/square/es6-class", 5 | "authors": [ 6 | "Brian Donovan " 7 | ], 8 | "description": "ES6 classes compiled to ES5.", 9 | "main": "lib/index.js", 10 | "keywords": [ 11 | "es6", 12 | "class" 13 | ], 14 | "license": "Apache 2", 15 | "ignore": [ 16 | "**/.*", 17 | "node_modules", 18 | "test" 19 | ] 20 | } -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, undef:true, unused:true */ 2 | 3 | var assert = require('assert'); 4 | var through = require('through'); 5 | var recast = require('recast'); 6 | var types = recast.types; 7 | var n = types.namedTypes; 8 | var b = types.builders; 9 | 10 | var util = require('ast-util'); 11 | 12 | var Class = types.Type.or(n.ClassDeclaration, n.ClassExpression); 13 | 14 | /** 15 | * Builds a function expression from an existing function, copying over the 16 | * properties that are not part of ast-types's build fields. 17 | * 18 | * @private 19 | * @param {Node} fn 20 | * @return {Node} 21 | */ 22 | function copyFunctionExpression(fn) { 23 | var result = b.functionExpression( 24 | fn.id, 25 | fn.params, 26 | fn.body, 27 | fn.generator, 28 | fn.expression 29 | ); 30 | if ('async' in fn) { 31 | result.async = fn.async; 32 | } 33 | if ('defaults' in fn) { 34 | result.defaults = fn.defaults; 35 | } 36 | if ('rest' in fn) { 37 | result.rest = fn.rest; 38 | } 39 | return result; 40 | } 41 | 42 | /** 43 | * Visits a node of an AST looking class declarations or expressions. This is 44 | * intended to be used with the ast-types `visit()` function. 45 | * 46 | * @private 47 | */ 48 | var visitor = types.PathVisitor.fromMethodsObject({ 49 | /** 50 | * Visits a `ClassDeclaration` node and replaces it with an equivalent 51 | * node with the class definition or expression turned into a function 52 | * with prototype properties for method definitions. 53 | * 54 | * @private 55 | * @param {NodePath} path 56 | * @this {PathVisitor} 57 | */ 58 | visitClassDeclaration: function(path) { 59 | // We need to know the class ID (either path.value.id or some 60 | // identifier generated by util.uniqueIdentifier(path.scope)) when we 61 | // transform super calls, so we have to call assignClassId(path) 62 | // before we call this.traverse(path). With that one exception, 63 | // visitClassDeclaration is mostly a post-order traversal, so that we 64 | // don't transform ClassDeclaration nodes before we transform the 65 | // super calls they might contain. 66 | assignClassId(path); 67 | 68 | // This needs to be a post-order traversal so that super calls in this 69 | // subtree can discover this (untransformed) ClassDeclaration node. 70 | this.traverse(path); 71 | 72 | var node = path.value; 73 | 74 | // All our definition statements go into an IIFE whose result goes 75 | // into a variable at the same scope as the original class. 76 | path.replace( 77 | b.variableDeclaration( 78 | 'var', 79 | [b.variableDeclarator( 80 | node.id, 81 | generateClassDefinitionExpression.call(path, path.value) 82 | )] 83 | ) 84 | ); 85 | }, 86 | 87 | /** 88 | * Visits a `ClassExpression` node and replaces it with an equivalent node 89 | * with the class definition or expression turned into a function with 90 | * prototype properties for method definitions. 91 | * 92 | * @private 93 | * @param {NodePath} path 94 | * @this {PathVisitor} 95 | */ 96 | visitClassExpression: function(path) { 97 | // See analogous comment in visitClassDeclaration (above). 98 | assignClassId(path); 99 | 100 | // This needs to be a post-order traversal so that super calls in this 101 | // subtree can discover this (untransformed) ClassExpression node. 102 | this.traverse(path); 103 | 104 | return generateClassDefinitionExpression.call(path, path.value); 105 | }, 106 | 107 | visitCallExpression: function(path) { 108 | var node = path.value; 109 | 110 | if (n.Identifier.check(node.callee) && 111 | node.callee.name === 'super') { 112 | // super() 113 | return this.visitSuperCall(path); 114 | 115 | } else if (n.MemberExpression.check(node.callee) && 116 | n.Identifier.check(node.callee.object) && 117 | node.callee.object.name === 'super') { 118 | // super.foo() 119 | return this.visitSuperCallMemberExpression(path); 120 | } 121 | 122 | this.traverse(path); 123 | }, 124 | 125 | visitSuperCall: visitSuperCall, 126 | visitSuperCallMemberExpression: visitSuperCallMemberExpression, 127 | 128 | visitMemberExpression: function(path) { 129 | var node = path.value; 130 | 131 | if (n.Identifier.check(node.object) && 132 | node.object.name === 'super') { 133 | return this.visitSuperMemberExpression(path); 134 | } 135 | 136 | this.traverse(path); 137 | }, 138 | 139 | visitSuperMemberExpression: visitSuperMemberExpression 140 | }); 141 | 142 | // ES6 Working Draft 14.5.15 143 | var DEFAULT_DESCRIPTORS = { 144 | default: { 145 | enumerable: b.literal(false), 146 | writable: b.literal(true) 147 | }, 148 | 149 | get: { 150 | enumerable: b.literal(true), 151 | configurable: b.literal(true) 152 | }, 153 | 154 | set: { 155 | enumerable: b.literal(true), 156 | configurable: b.literal(true) 157 | } 158 | }; 159 | 160 | function assignClassId(classPath) { 161 | assert.ok(classPath instanceof types.NodePath); 162 | var classNode = classPath.value; 163 | Class.assert(classNode); 164 | var classId = classNode.CLASS_ID; 165 | if (classId) { 166 | return classId; 167 | } 168 | classId = classNode.id || util.uniqueIdentifier(classPath.scope); 169 | n.Identifier.assert(classId); 170 | Object.defineProperty(classNode, 'CLASS_ID', { 171 | value: classId, 172 | enumerable: false 173 | }); 174 | return classId; 175 | } 176 | 177 | /** 178 | * Generates an expression that resolves to an equivalent function as the class 179 | * represented by `node`. 180 | * 181 | * @private 182 | * @param {ClassDeclaration|ClassExpression} node 183 | * @return {Expression} 184 | * @this {NodePath} 185 | */ 186 | function generateClassDefinitionExpression(node) { 187 | var body = node.body.body; 188 | var methodDefinitions = []; 189 | var propertyDescriptors = []; 190 | var propertyDescriptorMap = Object.create(null); 191 | var staticPropertyDescriptors = []; 192 | var staticPropertyDescriptorMap = Object.create(null); 193 | var constructor; 194 | 195 | var scope = this.scope; 196 | var globalScope = scope.getGlobalScope(); 197 | var classId = assignClassId(this); 198 | var superClassId = node.superClass && util.uniqueIdentifier(scope, 'super'); 199 | 200 | function addPropertyDescriptor(property, key, value, isStatic) { 201 | var map = isStatic ? staticPropertyDescriptorMap : propertyDescriptorMap; 202 | var list = isStatic ? staticPropertyDescriptors : propertyDescriptors; 203 | 204 | if (!map[property]) { 205 | list.push({ 206 | name: property, 207 | descriptor: map[property] = Object.create( 208 | DEFAULT_DESCRIPTORS[key] || DEFAULT_DESCRIPTORS.default 209 | ) 210 | }); 211 | } 212 | 213 | map[property][key] = value; 214 | } 215 | 216 | /** 217 | * Process each "method" definition. Methods, getters, setters, and the 218 | * constructor are all treated as method definitions. 219 | */ 220 | body.forEach(function(statement) { 221 | if (n.MethodDefinition.check(statement)) { 222 | var fn = statement.value; 223 | var methodName = statement.key.name; 224 | 225 | if (methodName === 'constructor') { 226 | constructor = fn; 227 | } else if (statement.kind) { 228 | addPropertyDescriptor( 229 | methodName, 230 | statement.kind, 231 | copyFunctionExpression(statement.value), 232 | statement.static 233 | ); 234 | } else { 235 | addPropertyDescriptor( 236 | methodName, 237 | 'value', 238 | copyFunctionExpression(fn), 239 | statement.static 240 | ); 241 | } 242 | } 243 | }); 244 | 245 | if (!constructor) { 246 | var constructorBody = []; 247 | if (superClassId) { 248 | /** 249 | * There's no constructor, so build a default one that calls the super 250 | * class function with the instance as the context. 251 | * 252 | * Object.getPrototypeOf(MyClass.prototype).constructor.apply(this, arguments) 253 | */ 254 | var protoId = util.uniqueIdentifier(scope); 255 | 256 | constructorBody.push( 257 | // var $__0 = Object.getPrototypeOf(MyClass.prototype); 258 | b.variableDeclaration( 259 | 'var', 260 | [b.variableDeclarator( 261 | protoId, 262 | util.callGetPrototypeOf( 263 | globalScope, 264 | b.memberExpression(classId, b.identifier('prototype'), false) 265 | ) 266 | )] 267 | ), 268 | b.ifStatement( 269 | b.binaryExpression('!==', protoId, b.literal(null)), 270 | b.expressionStatement( 271 | b.callExpression( 272 | b.memberExpression( 273 | b.memberExpression( 274 | protoId, 275 | b.identifier('constructor'), 276 | false 277 | ), 278 | b.identifier('apply'), false 279 | ), 280 | [b.thisExpression(), b.identifier('arguments')] 281 | ) 282 | ) 283 | ) 284 | ); 285 | } 286 | constructor = b.functionExpression(null, [], b.blockStatement(constructorBody)); 287 | } 288 | 289 | /** 290 | * Define the constructor. 291 | * 292 | * function MyClass() {} 293 | */ 294 | var classDeclaration = b.functionDeclaration( 295 | classId, 296 | constructor.params, 297 | constructor.body 298 | ); 299 | if ('rest' in constructor) { 300 | classDeclaration.rest = constructor.rest; 301 | } 302 | if ('defaults' in constructor) { 303 | classDeclaration.defaults = constructor.defaults; 304 | } 305 | var definitionStatements = [ 306 | // ES6 Working Draft 10.2.1 307 | b.expressionStatement(b.literal('use strict')), 308 | classDeclaration 309 | ]; 310 | 311 | if (superClassId) { 312 | /** 313 | * Set up inheritance. 314 | * 315 | * MyClass.__proto__ = MySuper; 316 | * MyClass.prototype = Object.create(MySuper.prototype); 317 | * Object.defineProperty(MyClass.prototype, 'constructor', { value: MyClass }); 318 | */ 319 | definitionStatements.push( 320 | b.expressionStatement(b.assignmentExpression( 321 | '=', 322 | b.memberExpression(classId, b.identifier('__proto__'), false), 323 | b.conditionalExpression( 324 | b.binaryExpression('!==', superClassId, b.literal(null)), 325 | superClassId, 326 | b.memberExpression(b.identifier('Function'), b.identifier('prototype'), false) 327 | ) 328 | )), 329 | b.expressionStatement(b.assignmentExpression( 330 | '=', 331 | b.memberExpression(classId, b.identifier('prototype'), false), 332 | util.callSharedMethod( 333 | globalScope, 334 | 'Object.create', 335 | [b.conditionalExpression( 336 | b.binaryExpression('!==', superClassId, b.literal(null)), 337 | b.memberExpression(superClassId, b.identifier('prototype'), false), 338 | b.literal(null) 339 | )] 340 | ) 341 | )), 342 | b.expressionStatement(util.callSharedMethod( 343 | globalScope, 344 | 'Object.defineProperty', 345 | [ 346 | b.memberExpression(classId, b.identifier('prototype'), false), 347 | b.literal('constructor'), 348 | b.objectExpression([b.property('init', b.identifier('value'), classId)]) 349 | ] 350 | )) 351 | ); 352 | } 353 | 354 | /** 355 | * Add the method definitions. 356 | * 357 | * MyClass.prototype.toString = function(){}; 358 | */ 359 | definitionStatements.push.apply(definitionStatements, methodDefinitions); 360 | 361 | /** 362 | * Add methods, getters, and setters for the prototype and the class. 363 | * 364 | * Object.defineProperty(MyClass.prototype, 'name', { 365 | * get: function(){} 366 | * }); 367 | */ 368 | addDefinePropertyDescriptorCalls( 369 | b.memberExpression(classId, b.identifier('prototype'), false), 370 | propertyDescriptors 371 | ); 372 | 373 | addDefinePropertyDescriptorCalls( 374 | classId, 375 | staticPropertyDescriptors 376 | ); 377 | 378 | /** 379 | * Add calls to `Object.defineProperty` with the given descriptor information 380 | * on the given object. This is used to add properties to the "class" itself 381 | * (static properties) and its prototype (instance properties). 382 | * 383 | * @private 384 | */ 385 | function addDefinePropertyDescriptorCalls(object, descriptors) { 386 | var descriptorProperties = []; 387 | 388 | descriptors.forEach(function(propertyDescriptor) { 389 | var descriptorObjectProperties = []; 390 | for (var key in propertyDescriptor.descriptor) { 391 | var value = propertyDescriptor.descriptor[key]; 392 | descriptorObjectProperties.push(b.property('init', b.identifier(key), value)); 393 | } 394 | 395 | descriptorProperties.push( 396 | b.property( 397 | 'init', 398 | b.identifier(propertyDescriptor.name), 399 | b.objectExpression(descriptorObjectProperties) 400 | ) 401 | ); 402 | }); 403 | 404 | if (descriptors.length) { 405 | definitionStatements.push(b.expressionStatement(util.callSharedMethod( 406 | globalScope, 407 | 'Object.defineProperties', 408 | [ 409 | object, 410 | b.objectExpression(descriptorProperties) 411 | ] 412 | ))); 413 | } 414 | } 415 | 416 | /** 417 | * Finally, return the constructor from the IIFE. 418 | */ 419 | definitionStatements.push(b.returnStatement(classId)); 420 | 421 | /* 422 | * All our definition statements go into an IIFE that takes the superclass 423 | * and returns the class. 424 | */ 425 | return b.callExpression( 426 | b.functionExpression( 427 | null, 428 | superClassId ? [superClassId] : [], 429 | b.blockStatement(definitionStatements), 430 | false, false, false 431 | ), 432 | superClassId ? [node.superClass] : [] 433 | ); 434 | } 435 | 436 | /** 437 | * Finds a parent node of the correct type and returns it. Returns null if no 438 | * such node is found. 439 | * 440 | * @private 441 | * @param {NodePath} path 442 | * @param {Type} type 443 | * @return {?Node} 444 | */ 445 | function getEnclosingNodeOfType(path, type) { 446 | var ancestor = path; 447 | 448 | while (ancestor) { 449 | if (type.check(ancestor.node)) { 450 | return ancestor.node; 451 | } 452 | ancestor = ancestor.parent; 453 | } 454 | 455 | return null; 456 | } 457 | 458 | /** 459 | * Visits a `CallExpression` node which calls `super()` and replaces it with a 460 | * call to the current method on the superclass for the containing class. 461 | * 462 | * @private 463 | * @param {NodePath} path 464 | * @this {PathVisitor} 465 | */ 466 | function visitSuperCall(path) { 467 | assert.ok(this instanceof types.PathVisitor); 468 | assert.ok(path instanceof types.NodePath); 469 | 470 | var node = path.value; 471 | var classNode = getEnclosingNodeOfType(path, Class); 472 | var methodDefinition = getEnclosingNodeOfType(path, n.MethodDefinition); 473 | 474 | if (classNode && methodDefinition) { 475 | // Replace `super()` with `Object.getPrototypeOf(MyClass[.prototype]).myMethod.call(this)`. 476 | var context = methodDefinition.static ? 477 | classNode.CLASS_ID : 478 | b.memberExpression(classNode.CLASS_ID, b.identifier('prototype'), false); 479 | 480 | return b.callExpression( 481 | b.memberExpression( 482 | b.memberExpression( 483 | util.callGetPrototypeOf( 484 | path.scope.getGlobalScope(), 485 | context 486 | ), 487 | methodDefinition.key, 488 | false 489 | ), 490 | b.identifier('call'), 491 | false 492 | ), 493 | [b.thisExpression()].concat(node.arguments) 494 | ); 495 | } 496 | 497 | this.traverse(path); 498 | } 499 | 500 | /** 501 | * @private 502 | */ 503 | function visitSuperMemberExpression(path) { 504 | assert.ok(this instanceof types.PathVisitor); 505 | assert.ok(path instanceof types.NodePath); 506 | 507 | var node = path.value; 508 | var classNode = getEnclosingNodeOfType(path, Class); 509 | 510 | if (classNode) { 511 | var globalScope = path.scope.getGlobalScope(); 512 | // Replace `super.foo` with an expression that returns the value of the 513 | // `foo` property as evaluated by the superclass with the current `this` 514 | // context. 515 | return util.callGet( 516 | globalScope, 517 | util.callGetPrototypeOf( 518 | globalScope, 519 | b.memberExpression( 520 | classNode.CLASS_ID, 521 | b.identifier('prototype'), 522 | false 523 | ) 524 | ), 525 | b.literal(node.property.name), 526 | b.thisExpression() 527 | ); 528 | } 529 | 530 | this.traverse(path); 531 | } 532 | 533 | /** 534 | * @private 535 | * @param {NodePath} path 536 | */ 537 | function visitSuperCallMemberExpression(path) { 538 | assert.ok(this instanceof types.PathVisitor); 539 | assert.ok(path instanceof types.NodePath); 540 | 541 | var node = path.value; 542 | var classNode = getEnclosingNodeOfType(path, Class); 543 | 544 | if (classNode) { 545 | var globalScope = path.scope.getGlobalScope(); 546 | // Replace `super.foo()` with an expression that calls the value of the 547 | // `foo` property as evaluated by the superclass with the current `this` 548 | // context. 549 | return b.callExpression( 550 | b.memberExpression( 551 | util.callGet( 552 | globalScope, 553 | util.callGetPrototypeOf( 554 | globalScope, 555 | b.memberExpression( 556 | classNode.CLASS_ID, 557 | b.identifier('prototype'), 558 | false 559 | ) 560 | ), 561 | b.literal(node.callee.property.name), 562 | b.thisExpression() 563 | ), 564 | b.identifier('call'), 565 | false 566 | ), 567 | [b.thisExpression()].concat(node.arguments) 568 | ); 569 | } 570 | 571 | this.traverse(path); 572 | } 573 | 574 | /** 575 | * Transform an Esprima AST generated from ES6 by replacing all 576 | * classes with an equivalent approach in ES5. 577 | * 578 | * NOTE: The argument may be modified by this function. To prevent modification 579 | * of your AST, pass a copy instead of a direct reference: 580 | * 581 | * // instead of transform(ast), pass a copy 582 | * transform(JSON.parse(JSON.stringify(ast)); 583 | * 584 | * @param {Object} ast 585 | * @return {Object} 586 | */ 587 | function transform(ast) { 588 | return recast.visit(ast, visitor); 589 | } 590 | 591 | /** 592 | * Transform JavaScript written using ES6 by replacing all classes 593 | * with the equivalent ES5. 594 | * 595 | * compile('class Foo {}'); // 'var Foo = (function() { function Foo() {}; return Foo; })();' 596 | * 597 | * @param {string} source 598 | * @param {Object} mapOptions 599 | * @return {string} 600 | */ 601 | function compile(source, mapOptions) { 602 | mapOptions = mapOptions || {}; 603 | 604 | var recastOptions = { 605 | sourceFileName: mapOptions.sourceFileName, 606 | sourceMapName: mapOptions.sourceMapName 607 | }; 608 | 609 | var ast = recast.parse(source, recastOptions); 610 | return recast.print(transform(ast), recastOptions); 611 | } 612 | 613 | module.exports = function() { 614 | var data = ''; 615 | return through(write, end); 616 | 617 | function write(buf) { data += buf; } 618 | function end() { 619 | this.queue(module.exports.compile(data).code); 620 | this.queue(null); 621 | } 622 | }; 623 | 624 | module.exports.compile = compile; 625 | module.exports.transform = transform; 626 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "es6-class", 3 | "version": "0.9.5", 4 | "description": "ES6 classes compiled to ES5.", 5 | "main": "lib/index.js", 6 | "bin": "bin/es6-class", 7 | "directories": { 8 | "test": "test" 9 | }, 10 | "files": [ 11 | "lib/index.js", 12 | "bin/es6-class", 13 | "LICENSE" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git@github.com:esnext/es6-class.git" 18 | }, 19 | "dependencies": { 20 | "ast-util": "^0.6.0", 21 | "recast": "^0.9.11", 22 | "through": "~2.3.6" 23 | }, 24 | "devDependencies": { 25 | "example-runner": "^0.2.0", 26 | "es6-rest-params": "^0.3.1", 27 | "es6-default-params": "^0.4.3" 28 | }, 29 | "scripts": { 30 | "test": "node test/runner.js" 31 | }, 32 | "author": "Square, Inc.", 33 | "license": "Apache 2" 34 | } -------------------------------------------------------------------------------- /test/examples/anonymous-class.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | var Animal = class { 4 | sayHi() { 5 | return 'Hi, I am a '+this.type()+'.'; 6 | } 7 | 8 | static getName() { 9 | return 'Animal'; 10 | } 11 | }; 12 | 13 | var Dog = class extends Animal { 14 | type() { return 'dog'; } 15 | 16 | sayHi() { 17 | return super() + ' WOOF!'; 18 | } 19 | 20 | static getName() { 21 | return super() + '/Dog'; 22 | } 23 | }; 24 | 25 | assert.equal(new Dog().sayHi(), 'Hi, I am a dog. WOOF!'); 26 | assert.equal(Dog.getName(), 'Animal/Dog'); 27 | 28 | var count = 0; 29 | var Cat = class extends (function(){ count++; return Animal; })() {}; 30 | 31 | assert.equal(count, 1); 32 | -------------------------------------------------------------------------------- /test/examples/call-super-function.js: -------------------------------------------------------------------------------- 1 | class Animal { 2 | sayHi() { 3 | return 'I am an animal.' 4 | } 5 | 6 | sayOther() { 7 | return 'WAT?!'; 8 | } 9 | } 10 | 11 | class Horse extends Animal { 12 | sayHi() { 13 | return super.sayOther(); 14 | } 15 | 16 | sayOther() { 17 | return 'I see dead objects.'; 18 | } 19 | } 20 | 21 | assert.equal(new Horse().sayHi(), 'WAT?!'); 22 | -------------------------------------------------------------------------------- /test/examples/class-expressions.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | var Person = (class Person {}); 4 | assert.equal(typeof Person, 'function'); 5 | 6 | assert.equal( 7 | (function(){ return (class Person {}); })().name, 8 | 'Person' 9 | ); 10 | 11 | assert.equal(typeof (class {}), 'function'); 12 | -------------------------------------------------------------------------------- /test/examples/class-extend.js: -------------------------------------------------------------------------------- 1 | class Animal { 2 | sayHi() { 3 | return 'Hi, I am a '+this.type()+'.'; 4 | } 5 | } 6 | 7 | class Dog extends Animal { 8 | type() { return 'dog'; } 9 | 10 | sayHi() { 11 | return super() + ' WOOF!'; 12 | } 13 | } 14 | 15 | assert.equal(new Dog().sayHi(), 'Hi, I am a dog. WOOF!'); 16 | -------------------------------------------------------------------------------- /test/examples/class-with-constructor.js: -------------------------------------------------------------------------------- 1 | class Multiplier { 2 | constructor(n=1) { 3 | this.n = n; 4 | } 5 | 6 | multiply(n=1) { 7 | return n * this.n; 8 | } 9 | } 10 | 11 | assert.equal(new Multiplier().n, 1); 12 | assert.equal(new Multiplier(6).n, 6); 13 | assert.equal(new Multiplier().multiply(), 1); 14 | assert.equal(new Multiplier(2).multiply(3), 6); 15 | -------------------------------------------------------------------------------- /test/examples/class-with-method-declaration.js: -------------------------------------------------------------------------------- 1 | class Person { 2 | getName() { 3 | return this.firstName + ' ' + this.lastName; 4 | } 5 | } 6 | 7 | var me = new Person(); 8 | me.firstName = 'Brian'; 9 | me.lastName = 'Donovan'; 10 | assert.equal(me.getName(), 'Brian Donovan'); 11 | -------------------------------------------------------------------------------- /test/examples/empty-named-class.js: -------------------------------------------------------------------------------- 1 | class Foo { 2 | } 3 | 4 | assert.equal(new Foo().constructor, Foo, 'Foo instances should have Foo as constructor'); 5 | assert.ok(new Foo() instanceof Foo, 'Foo instances should be `instanceof` Foo'); 6 | -------------------------------------------------------------------------------- /test/examples/enumerable.js: -------------------------------------------------------------------------------- 1 | class Point { 2 | constructor(x, y) { 3 | this.x = x; 4 | this.y = y; 5 | } 6 | 7 | toString() { 8 | return '(' + this.x + ', ' + this.y + ')'; 9 | } 10 | } 11 | 12 | var point = new Point(1, 2); 13 | var keys = []; 14 | 15 | for (var key in point) { 16 | keys.push(key); 17 | } 18 | 19 | assert.equal(point.toString(), '(1, 2)'); 20 | assert.deepEqual(keys.sort(), ['x', 'y']); 21 | -------------------------------------------------------------------------------- /test/examples/explicit-super-in-constructor.js: -------------------------------------------------------------------------------- 1 | class Point { 2 | constructor(x, y) { 3 | this.x = x; 4 | this.y = y; 5 | } 6 | } 7 | 8 | class ZeroPoint extends Point { 9 | constructor() { 10 | super(0, 0); 11 | } 12 | } 13 | 14 | assert.equal(new ZeroPoint().x, 0); 15 | assert.equal(new ZeroPoint().y, 0); 16 | -------------------------------------------------------------------------------- /test/examples/extends-null.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | class Obj extends null {} 4 | 5 | assert.strictEqual(Obj.toString, Function.toString); 6 | assert.strictEqual(new Obj().toString, undefined); 7 | -------------------------------------------------------------------------------- /test/examples/getter-setter-super.js: -------------------------------------------------------------------------------- 1 | class Animal { 2 | get sound() { 3 | return 'I am a ' + this.type + '.'; 4 | } 5 | } 6 | 7 | class Cat extends Animal { 8 | get type() { return 'cat'; } 9 | 10 | get sound() { 11 | return super.sound + ' MEOW!'; 12 | } 13 | } 14 | 15 | assert.equal(new Cat().sound, 'I am a cat. MEOW!'); 16 | -------------------------------------------------------------------------------- /test/examples/getter-setter.js: -------------------------------------------------------------------------------- 1 | class Person { 2 | constructor(firstName, lastName) { 3 | this.firstName = firstName; 4 | this.lastName = lastName; 5 | } 6 | 7 | get name() { 8 | return this.firstName + ' ' + this.lastName; 9 | } 10 | 11 | set name(name) { 12 | var parts = name.split(' '); 13 | this.firstName = parts[0]; 14 | this.lastName = parts[1]; 15 | } 16 | } 17 | 18 | var mazer = new Person('Mazer', 'Rackham'); 19 | assert.equal(mazer.name, 'Mazer Rackham'); 20 | mazer.name = 'Ender Wiggin'; 21 | assert.equal(mazer.firstName, 'Ender'); 22 | assert.equal(mazer.lastName, 'Wiggin'); 23 | 24 | var forLoopProperties = []; 25 | for (var key in mazer) { 26 | forLoopProperties.push(key); 27 | } 28 | assert.ok(forLoopProperties.indexOf('name') >= 0, 'getters/setters are enumerable'); 29 | -------------------------------------------------------------------------------- /test/examples/implicit-superclass.js: -------------------------------------------------------------------------------- 1 | class Obj { 2 | constructor() { 3 | super(); 4 | } 5 | } 6 | 7 | assert.doesNotThrow(function() { 8 | new Obj(); 9 | }); 10 | -------------------------------------------------------------------------------- /test/examples/method-declaration-with-arguments.js: -------------------------------------------------------------------------------- 1 | class Tripler { 2 | triple(n) { 3 | return n * 3; 4 | } 5 | } 6 | 7 | assert.equal(new Tripler().triple(2), 6); 8 | -------------------------------------------------------------------------------- /test/examples/methods-are-writable.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | var value; 4 | 5 | class Foo { 6 | foo() { 7 | value = 1; 8 | } 9 | } 10 | 11 | var foo = new Foo(); 12 | foo.foo = function() { value = 2; }; 13 | foo.foo(); 14 | assert.equal(value, 2); 15 | -------------------------------------------------------------------------------- /test/examples/methods-with-rest-params.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | class Joiner { 4 | constructor(string) { 5 | this.string = string; 6 | } 7 | 8 | join(...items) { 9 | return items.join(this.string); 10 | } 11 | 12 | static join(string, ...items) { 13 | var joiner = new this(string); 14 | // TODO: use spread params here 15 | return joiner.join.apply(joiner, items); 16 | } 17 | } 18 | 19 | class ArrayLike { 20 | constructor(...items) { 21 | items.forEach(function(item, i) { 22 | this[i] = item; 23 | }.bind(this)); 24 | this.length = items.length; 25 | } 26 | } 27 | 28 | var joiner = new Joiner(' & '); 29 | assert.equal(joiner.join(4, 5, 6), '4 & 5 & 6'); 30 | assert.equal(new ArrayLike('a', 'b')[1], 'b'); 31 | -------------------------------------------------------------------------------- /test/examples/static-getter.js: -------------------------------------------------------------------------------- 1 | class Point { 2 | constructor(x, y) { 3 | this.x = x; 4 | this.y = y; 5 | } 6 | 7 | static get ORIGIN() { 8 | return new this(0, 0); 9 | } 10 | } 11 | 12 | assert.deepEqual(Point.ORIGIN, new Point(0, 0)); -------------------------------------------------------------------------------- /test/examples/static-method.js: -------------------------------------------------------------------------------- 1 | class Tripler { 2 | static triple(n=1) { 3 | return n * 3; 4 | } 5 | 6 | static toString() { 7 | return '3' + super() + '3'; 8 | } 9 | } 10 | 11 | class MegaTripler extends Tripler { 12 | static triple(n=1) { 13 | return super(n) * super(n); 14 | } 15 | } 16 | 17 | var tripler = new Tripler(); 18 | 19 | assert.equal(Tripler.triple(), 3); 20 | assert.equal(Tripler.triple(2), 6); 21 | assert.equal(tripler.triple, undefined); 22 | 23 | assert.equal(Tripler.toString(), '3' + Object.toString.call(Tripler) + '3'); 24 | 25 | var mega = new MegaTripler(); 26 | 27 | assert.equal(MegaTripler.triple(2), 36); 28 | assert.equal(mega.triple, undefined); 29 | 30 | assert.equal(MegaTripler.toString(), '3' + Object.toString.call(MegaTripler) + '3'); 31 | -------------------------------------------------------------------------------- /test/examples/static-setter.js: -------------------------------------------------------------------------------- 1 | class Person { 2 | static set DB(value) { 3 | assert.equal(value, 'mysql'); 4 | } 5 | } 6 | 7 | Person.DB = 'mysql'; 8 | -------------------------------------------------------------------------------- /test/examples/strict-mode.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | class StrictModeTest { 4 | test() { 5 | var implicitThisInsideClassBody = (function() { 6 | return this; 7 | })(); 8 | assert.strictEqual( 9 | implicitThisInsideClassBody, 10 | undefined, 11 | 'implicit `this` inside class body is undefined' 12 | ); 13 | } 14 | } 15 | 16 | new StrictModeTest().test(); 17 | 18 | var implicitThisOutsideClass = (function() { 19 | return this; 20 | })(); 21 | assert.notStrictEqual( 22 | implicitThisOutsideClass, 23 | undefined, 24 | 'implicit `this` outside class body is not undefined' 25 | ); 26 | -------------------------------------------------------------------------------- /test/examples/super-change-proto.js: -------------------------------------------------------------------------------- 1 | var log = ''; 2 | 3 | class Base { 4 | p() { log += '[Base]'; } 5 | } 6 | 7 | class OtherBase { 8 | p() { log += '[OtherBase]'; } 9 | } 10 | class Derived extends Base { 11 | p() { 12 | log += '[Derived]'; 13 | super(); 14 | Derived.prototype.__proto__ = OtherBase.prototype; 15 | super(); 16 | } 17 | } 18 | 19 | new Derived().p(); 20 | assert.equal(log, '[Derived][Base][OtherBase]'); 21 | -------------------------------------------------------------------------------- /test/runner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * We pull in example files from test/examples/*.js. Write your assertions in 3 | * the file alongside the ES6 class "setup" code. The node `assert` library 4 | * will already be in the context. 5 | */ 6 | 7 | Error.stackTraceLimit = 20; 8 | 9 | var recast = require('recast'); 10 | 11 | var es6class = require('../lib'); 12 | var es6restParams = require('es6-rest-params'); 13 | var es6defaultParams = require('es6-default-params'); 14 | 15 | var fs = require('fs'); 16 | var path = require('path'); 17 | var RESULTS = 'test/results'; 18 | 19 | if (!fs.existsSync(RESULTS)) { 20 | fs.mkdirSync(RESULTS); 21 | } 22 | 23 | require('example-runner').runCLI(process.argv.slice(2), { 24 | transform: function(source, testName, filename) { 25 | var recastOptions = { 26 | sourceFileName: filename, 27 | sourceMapName: filename + '.map' 28 | }; 29 | 30 | var ast = recast.parse(source, recastOptions); 31 | ast = es6defaultParams.transform(es6restParams.transform(es6class.transform(ast))); 32 | var result = recast.print(ast, recastOptions); 33 | 34 | fs.writeFileSync(path.join(RESULTS, testName + '.js'), result.code, 'utf8'); 35 | fs.writeFileSync(path.join(RESULTS, testName + '.js.map'), JSON.stringify(result.map), 'utf8'); 36 | return result.code; 37 | } 38 | }); 39 | --------------------------------------------------------------------------------