├── .gitignore ├── .babelrc ├── package.json ├── test └── react.js ├── README.md └── lib └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ], 4 | "plugins": ["transform-alkali"] 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "babel-plugin-transform-class-properties": "^6.18.0", 4 | "babel-plugin-transform-flow-strip-types": "^6.21.0", 5 | "babel-plugin-syntax-decorators": "^6.13.0", 6 | "babel-plugin-syntax-flow": "^6.18.0" 7 | }, 8 | "description": "Transform reactive code to use alkali API", 9 | "devDependencies": { 10 | "babel-cli": "^6.18.0", 11 | "babel-helper-plugin-test-runner": "^6.8.0" 12 | }, 13 | "directories": {}, 14 | "keywords": [ 15 | "babel-plugin" 16 | ], 17 | "license": "MIT", 18 | "main": "lib/index.js", 19 | "maintainers": [ 20 | { 21 | "email": "kriszyp@gmail.com", 22 | "name": "kriszyp" 23 | } 24 | ], 25 | "name": "babel-plugin-transform-alkali", 26 | "readme": "ERROR: No README data found!", 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/kriszyp/babel-plugin-transform-alkali/tree/master/babel-plugin-transform-alkali" 30 | }, 31 | "scripts": {}, 32 | "version": "0.2.0" 33 | } 34 | -------------------------------------------------------------------------------- /test/react.js: -------------------------------------------------------------------------------- 1 | var tests = { 2 | basic: function() { 3 | let num = react(3) 4 | }, 5 | sum: function() { 6 | let sum = react(num + 5) 7 | }, 8 | bool: function() { 9 | let bool = react(true) 10 | let f = react(!bool) 11 | }, 12 | cond: function() { 13 | let cond = { 14 | condProp: react(f ? num : sum) 15 | } 16 | }, 17 | call: function() { 18 | let result 19 | react(result = Math.min(num, sum)) 20 | }, 21 | object: function() { 22 | react({ 23 | num, 24 | sum: sum * 2, 25 | three: 3 26 | }) 27 | }, 28 | array: function() { 29 | react(Math.max.apply(null, [num, 3, sum])) 30 | }, 31 | boundGenerator: function() { 32 | react((function*() { 33 | var a = 3 34 | test(2 * num) 35 | }).bind(this)) 36 | }, 37 | decorator: ` 38 | () => { 39 | const VF = VArray.of(Foo) 40 | class Foo { 41 | @reactive foo: {goo: VF[], noo: string, b: boolean, n: number, o: {}} 42 | } 43 | }` 44 | } 45 | var test 46 | for (var testName in tests) { 47 | var result = require("babel-core").transform('test=' + tests[testName].toString(), { 48 | plugins: ["transform-alkali", 'transform-class-properties', 'transform-flow-strip-types'] 49 | }) 50 | console.log('transformed', result.code) 51 | //eval(result.code)() 52 | } 53 | 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-transform-alkali 2 | This babel plugin will transform expressions that use a `react` keyword/call to produce reactive variables. This relies on [alkali](https://github.com/kriszyp/alkali) for variable operations that produce reactively bound variables. 3 | 4 | ## Installation 5 | 6 | ```sh 7 | $ npm install babel-plugin-transform-alkali 8 | ``` 9 | 10 | ## Usage 11 | 12 | The basic format of using the transform is to write reactive expressions in the form: 13 | ``` 14 | react(expression) 15 | ``` 16 | The `react` variable should be imported from alkali. The `expression` will be transformed to code that will reactively respond to any changes in inputs values, reflecting them in the output variable. For example: 17 | ``` 18 | import { react } from 'alkali' 19 | let a = react(2) 20 | let b = react(4) 21 | let sum = react(a + b) 22 | sum.valueOf() -> 6 23 | a.put(4) 24 | sum.valueOf() -> 8 25 | ``` 26 | Reactive properties and assignments are supported as well. Property access within a reactive expression will be converted to a property variable (basically `obj.prop` -> `obj.property('prop')`, with object mappings and safety checks). And assignments within a reactive expression will be converted to a `put` call (basically `v = 'hi'` -> `v.put('hi')` with similar variable mapping/creation as necessary). For example: 27 | ``` 28 | let obj = react({ 29 | foo: 3 30 | }) 31 | let doubleFoo = react(obj.foo * 2) 32 | doubleFoo.valueOf() -> 6 33 | react(obj.foo = 5) 34 | doubleFoo.valueOf() -> 10 35 | ``` 36 | Function and method calls can be made written in reactive expressions as well. These calls will be performed lazily/on-demand, and reexecuted as needed. The target function will be called with the values of the variables (not the variables themselves). For example: 37 | ``` 38 | let smallest = react(Math.min(a, b)) 39 | ``` 40 | 41 | The `react` operator returns alkali variables, that can be bound to DOM elements or any other alkali target. 42 | ``` 43 | import { react, Div } from 'alkali' 44 | // create a div with its text bound to the sum 45 | parent.appendChild(new Div(sum)) 46 | ``` 47 | And the reactive expressions maintain operator relationships, so alkali's reversible data flow is supported as well: 48 | ``` 49 | let a = react(2) 50 | let doubleA = react(a * 2) 51 | react(doubleA = 10) // will flow back through the expression 52 | a.valueOf() -> 5 53 | ``` 54 | The `react` function can take multiple arguments, the last argument output will be returned as the variable from the `react` call. 55 | 56 | ## Transform Usage 57 | 58 | ### Via `.babelrc` (Recommended) 59 | 60 | **.babelrc** 61 | 62 | ```json 63 | { 64 | "plugins": ["transform-alkali"] 65 | } 66 | ``` 67 | 68 | ### Via CLI 69 | 70 | ```sh 71 | $ babel --plugins transform-alkali 72 | ``` 73 | 74 | ### Via Node API 75 | 76 | ```javascript 77 | require("babel-core").transform("code", { 78 | plugins: ["transform-alkali"] 79 | }); 80 | ``` 81 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | "use strict" 3 | const t = config.types 4 | const operators = { 5 | '+': 'add', 6 | '-': 'subtract', 7 | '*': 'multiply', 8 | '/': 'divide', 9 | '!': 'not', 10 | '%': 'remainder', 11 | '>': 'greater', 12 | '>=': 'greaterOrEqual', 13 | '<': 'less', 14 | '<=': 'lessOrEqual', 15 | '==': 'looseEqual', 16 | '===': 'equal', 17 | '&&': 'and', 18 | '||': 'or' 19 | } 20 | 21 | function getTempId(scope) { 22 | let id = scope.path.getData('functionBind'); 23 | if (id) return id; 24 | 25 | id = scope.generateDeclaredUidIdentifier('context'); 26 | return scope.path.setData('functionBind', id); 27 | } 28 | 29 | function getStaticContext(bind, scope) { 30 | let object = bind.object || bind.callee.object; 31 | return scope.isStatic(object) && object; 32 | } 33 | 34 | function markAsOutput(node) { 35 | node.isReactiveCompiled = true 36 | return node 37 | } 38 | 39 | var queuedIdentifiers = {} 40 | function retrieveReactively(path) { 41 | let node = path.node 42 | if (node.isReactiveCompiled) { 43 | return node 44 | } else if (node.type === 'Identifier') { 45 | return node 46 | } else { 47 | let visitorHandler = reactVisitors[node.type] 48 | 49 | if (visitorHandler) { 50 | if (visitorHandler(path) === false) { 51 | visitorHandler = false 52 | } 53 | node = path.node 54 | } 55 | if (!visitorHandler) { 56 | path.traverse(identifierVisitors) 57 | let identifierMap = queuedIdentifiers 58 | queuedIdentifiers = {} 59 | function getIdentifiers(input) { 60 | let identifiers = [] 61 | for (let name in identifierMap) { 62 | identifiers.push(input ? identifierMap[name] : t.identifier(name)) 63 | } 64 | return identifiers 65 | } 66 | if (getIdentifiers().length === 0) { 67 | // nothing referenced, nothing to do 68 | return node 69 | } 70 | node = markAsOutput(t.callExpression( 71 | markAsOutput(t.memberExpression(t.identifier('react'), t.identifier('fcall'))), 72 | [t.arrowFunctionExpression(getIdentifiers(), node), 73 | t.arrayExpression(getIdentifiers(true))])) 74 | path.replaceWith(node) 75 | } 76 | } 77 | return node 78 | } 79 | 80 | const identifierVisitors = { 81 | Identifier(path) { 82 | let node = path.node 83 | if (!(path.parent.type === 'MemberExpression' && path.parent.property === node) && 84 | !(node.name === 'react') && 85 | !(node.isReactiveCompiled)) { 86 | queuedIdentifiers[node.name] = node 87 | } 88 | }, 89 | MemberExpression(path) { 90 | let node = path.node 91 | if (node.isReactiveCompiled) { 92 | return 93 | } 94 | let replacedName = (node.object.name || 'temp') + (node.property.name || 'temp') 95 | queuedIdentifiers[replacedName] = markAsOutput(t.callExpression( 96 | markAsOutput(t.memberExpression(t.identifier('react'), t.identifier('prop'))), 97 | [retrieveReactively(path.get('object')), node.computed ? retrieveReactively(path.get('property')) : t.stringLiteral(node.property.name)])) 98 | path.replaceWith(markAsOutput(t.identifier(replacedName))) 99 | }, 100 | Statement(path) { 101 | throw new Error('Statements not allowed in reactive expressions') 102 | } 103 | } 104 | 105 | const reactVisitors = { 106 | AssignmentExpression(path) { 107 | let left = path.get('left') 108 | let right = path.get('right') 109 | if (t.isIdentifier(path.node.left)) { 110 | let assignee = retrieveReactively(left) 111 | path.replaceWith(markAsOutput(t.callExpression( 112 | t.memberExpression( 113 | t.conditionalExpression( 114 | t.logicalExpression('&&', assignee, t.memberExpression(assignee, t.identifier('put'))), 115 | assignee, 116 | t.assignmentExpression('=', assignee, t.callExpression(t.memberExpression(t.identifier('react'), t.identifier('from')), [])) 117 | ), 118 | t.identifier('put') 119 | ), [retrieveReactively(right)]))) 120 | return 121 | } 122 | path.replaceWith(markAsOutput(t.callExpression( 123 | t.memberExpression(left.node, t.identifier('put')), [retrieveReactively(right)]))) 124 | retrieveReactively(path.get('callee').get('object')) // do the replacement afterwards, so we do get assignment error 125 | }, 126 | MemberExpression(path) { 127 | let node = path.node 128 | if (node.isReactiveCompiled) { 129 | return 130 | } 131 | path.replaceWith(markAsOutput(t.callExpression( 132 | markAsOutput(t.memberExpression(t.identifier('react'), t.identifier('prop'))), 133 | [retrieveReactively(path.get('object')), node.computed ? retrieveReactively(path.get('property')) : t.stringLiteral(node.property.name)]))) 134 | }, 135 | NewExpression(path) { 136 | return reactVisitors.CallExpression(path) 137 | }, 138 | CallExpression(path) { 139 | let node = path.node 140 | let callee = node.callee 141 | if (node.isReactiveCompiled) { 142 | return 143 | } 144 | let reactiveArguments = [] 145 | let args = path.get('arguments').forEach(arg => reactiveArguments.push(retrieveReactively(arg))) 146 | var argumentArray = t.arrayExpression(reactiveArguments) 147 | if (callee.type === 'MemberExpression') { 148 | path.replaceWith(markAsOutput(t.callExpression( 149 | markAsOutput(t.memberExpression(t.identifier('react'), t.identifier('mcall'))), 150 | [callee.object, callee.computed ? 151 | retrieveReactively(path.get('callee').get('property')) : 152 | t.stringLiteral(callee.property.name), 153 | argumentArray]))) 154 | } else { 155 | path.replaceWith(markAsOutput(t.callExpression( 156 | markAsOutput(t.memberExpression(t.identifier('react'), t.identifier(t.isNewExpression(node) ? 'ncall' : 'fcall'))), 157 | [callee, argumentArray]))) 158 | } 159 | }, 160 | BinaryExpression(path) { 161 | let operatorName = operators[path.node.operator] 162 | if (operatorName) { 163 | path.replaceWith(markAsOutput(t.callExpression( 164 | markAsOutput(t.memberExpression(t.identifier('react'), t.identifier(operatorName))), 165 | [retrieveReactively(path.get('left')), retrieveReactively(path.get('right'))]))) 166 | } else { 167 | return false 168 | } 169 | }, 170 | UnaryExpression(path) { 171 | let operatorName = operators[path.node.operator] 172 | if (operatorName) { 173 | path.replaceWith(markAsOutput(t.callExpression( 174 | markAsOutput(t.memberExpression(t.identifier('react'), t.identifier(operatorName))), 175 | [retrieveReactively(path.get('argument'))]))) 176 | } else { 177 | return false 178 | } 179 | }, 180 | LogicalExpression(path) { 181 | let operatorName = operators[path.node.operator] 182 | if (operatorName) { 183 | path.replaceWith(markAsOutput(t.callExpression( 184 | markAsOutput(t.memberExpression(t.identifier('react'), t.identifier(operatorName))), 185 | [retrieveReactively(path.get('left')), retrieveReactively(path.get('right'))]))) 186 | } else { 187 | return false 188 | } 189 | }, 190 | ConditionalExpression(path) { 191 | path.replaceWith(markAsOutput(t.callExpression( 192 | markAsOutput(t.memberExpression(t.identifier('react'), t.identifier('cond'))), 193 | [retrieveReactively(path.get('test')), retrieveReactively(path.get('consequent')), retrieveReactively(path.get('alternate'))]))) 194 | }, 195 | ObjectExpression(path) { 196 | // react.obj((v1, v2) => ['static', v1, v2], [v1, v2]) 197 | // we can keep it as an object 198 | path.get('properties').forEach(property => { 199 | retrieveReactively(property.get('value')) 200 | }) 201 | var inputs = [] 202 | var args = [] 203 | let index = 0 204 | path.get('properties').forEach(property => { 205 | let node = retrieveReactively(property.get('value')) 206 | if (node.isReactiveCompiled || t.isIdentifier(node)) { 207 | property.get('value').replaceWith(t.identifier('v' + index)) 208 | args.push(t.identifier('v' + index)) 209 | inputs.push(node) 210 | index++ 211 | } 212 | }) 213 | if (index > 0) { 214 | let node = markAsOutput(t.callExpression( 215 | markAsOutput(t.memberExpression(t.identifier('react'), t.identifier('obj'))), 216 | [t.arrowFunctionExpression(args, path.node), 217 | t.arrayExpression(inputs)])) 218 | path.replaceWith(node) 219 | } 220 | }, 221 | ArrayExpression(path) { 222 | // react.obj((v1, v2) => ['static', v1, v2], [v1, v2]) 223 | var transformedElements = [] 224 | var inputs = [] 225 | var args = [] 226 | let index = 0 227 | path.get('elements').forEach(element => { 228 | let node = retrieveReactively(element) 229 | if (node.isReactiveCompiled || t.isIdentifier(node)) { 230 | transformedElements.push(t.identifier('v' + index)) 231 | args.push(t.identifier('v' + index)) 232 | inputs.push(node) 233 | index++ 234 | } else { 235 | transformedElements.push(node) 236 | } 237 | }) 238 | if (index > 0) { 239 | let node = markAsOutput(t.callExpression( 240 | markAsOutput(t.memberExpression(t.identifier('react'), t.identifier('obj'))), 241 | [t.arrowFunctionExpression(args, t.arrayExpression(transformedElements)), 242 | t.arrayExpression(inputs)])) 243 | path.replaceWith(node) 244 | } 245 | }, 246 | 247 | FunctionExpression() { 248 | // this basically exits out of the react transformation 249 | // TODO: if it is a fat arrow expression and the body is an expression, keep transforming 250 | } 251 | } 252 | 253 | return { 254 | manipulateOptions(opts, parserOpts) { 255 | parserOpts.plugins.push('decorators', 'flow') 256 | }, 257 | visitor: { 258 | CallExpression(path) { 259 | let node = path.node 260 | let scope = path.scope 261 | let firstArg = node.arguments[0] 262 | let callee = node.callee 263 | if (callee.name === 'react' && firstArg) { 264 | //node.callee.name = 'react.from'// may not need this 265 | markAsOutput(node) 266 | path.get('callee').replaceWith(t.memberExpression(t.identifier('react'), t.identifier('from'))) 267 | path.get('arguments').forEach(retrieveReactively) 268 | let name 269 | if (t.isAssignmentExpression(path.parent)) { 270 | name = path.parent.left.name 271 | } 272 | if (t.isVariableDeclarator(path.parent)) { 273 | name = path.parent.id.name 274 | } 275 | if (t.isProperty(path.parent)) { 276 | name = path.parent.key.name 277 | } 278 | if (name) { 279 | path.replaceWith(t.callExpression(t.memberExpression(path.node, t.identifier('_sN')), [t.stringLiteral(name)])) 280 | } 281 | } 282 | }, 283 | Class(path) { 284 | path.get('body').get('body').forEach(entry => { 285 | if (!entry.node.decorators || !entry.get('decorators').forEach) { 286 | return 287 | } 288 | entry.get('decorators').forEach(path => { 289 | let decoratorExpression = path.node.expression 290 | if (decoratorExpression.name !== 'reactive') { 291 | return 292 | } 293 | let typeToExpression = (type) => { 294 | switch (type.type) { 295 | case 'GenericTypeAnnotation': 296 | return type.id 297 | case 'ObjectTypeAnnotation': 298 | return t.objectExpression(type.properties.map(property => 299 | t.objectProperty(property.key, typeToExpression(property.value)))) 300 | case 'ArrayTypeAnnotation': 301 | return t.arrayExpression([typeToExpression(type.elementType)]) 302 | case 'StringTypeAnnotation': 303 | return t.stringLiteral('string') 304 | case 'NumberTypeAnnotation': 305 | return t.stringLiteral('number') 306 | case 'BooleanTypeAnnotation': 307 | return t.stringLiteral('boolean') 308 | default: 309 | throw new Error(type.type + ' not implemented') 310 | } 311 | } 312 | let type = typeToExpression(path.parent.typeAnnotation.typeAnnotation) 313 | let name = path.parent.key 314 | 315 | path.parentPath.replaceWithMultiple([ 316 | // get name() { return ... } 317 | t.classMethod('get', name, [], t.blockStatement([t.returnStatement( 318 | t.logicalExpression('||', 319 | t.logicalExpression('&&', 320 | t.memberExpression(t.thisExpression(), t.identifier('_properties')), 321 | t.memberExpression(t.memberExpression(t.thisExpression(), t.identifier('_properties')), name)), 322 | t.callExpression(t.memberExpression(decoratorExpression, t.identifier('get')), 323 | [t.thisExpression(), t.stringLiteral(name.name), type]))) 324 | ])), 325 | t.classMethod('set', name, [t.identifier('value')], t.blockStatement([t.expressionStatement( 326 | t.callExpression(t.memberExpression(decoratorExpression, t.identifier('set')), 327 | [t.thisExpression(), t.stringLiteral(name.name), t.identifier('value')]) 328 | )]))]) 329 | }) 330 | 331 | }) 332 | } 333 | } 334 | }; 335 | } 336 | --------------------------------------------------------------------------------