├── .babelrc ├── .gitignore ├── .npmignore ├── HISTORY.md ├── README.md ├── lib ├── index.js ├── most.js ├── rx.js └── rxjs.js ├── package.json ├── src ├── index.js ├── most.js ├── rx.js └── rxjs.js └── test ├── fixtures ├── exclude │ ├── source.js │ └── transformed.js ├── most-basic │ ├── source.js │ └── transformed.js ├── rx-basic │ ├── source.js │ └── transformed.js ├── rx-filter-identifier │ ├── source.js │ └── transformed.js ├── rx-subject-imported │ ├── source.js │ └── transformed.js └── rxjs-basic │ ├── source.js │ └── transformed.js ├── index.js ├── make-fixtures.js └── transform.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | _* 3 | node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | test 3 | _* -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ### v0.2.0 2 | * Added experimental support for `ReplaySubject(1)` with `replay` option 3 | * Rx `finally` check proxy.observers count before subscribtion `dispose` 4 | * minimatch dependency 5 | 6 | ### v0.1.0 Imports insertion and rxjs/most support 7 | * imports of `Subject` object are added if not found 8 | * added initial most.js and rxjs5 support 9 | 10 | ### v0.0.1 Initial pre-release 11 | * only rx@4.x.x 12 | * requires `Rx` or `Subject` reference 13 | * no tests released -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-cycle-circular 2 | 3 | Babel plugin allowing to have circular dependencies with [cycle.js](http://cycle.js.org) 4 | 5 | ## What? 6 | 7 | This (note that **`bar`** is **used before declared** in the code): 8 | 9 | ```js 10 | const foo = Foo({value$: bar.value$, DOM}) 11 | const bar = Bar({HTTP, prop$: foo.prop$}) 12 | ``` 13 | 14 | will just work. 15 | 16 | **This is experimented feature** - try and see if it fits your needs, if something wrong or 17 | it doesn't cover you usage scenarios just create [an issue](issues) and we'll try to fix it. 18 | 19 | ## How does it work 20 | 21 | This is your ES6 source: 22 | 23 | ```js 24 | import {ComponentFoo} from './ComponentFoo' 25 | import {ComponentBar} from './ComponentBar' 26 | 27 | const main = ({DOM, HTTP}) => { 28 | 29 | const componentFoo = ComponentFoo({value$: componentBar.value$, DOM}) 30 | const componentBar = ComponentBar({HTTP, componentFoo.prop$}) 31 | 32 | return { 33 | DOM: componentFoo.DOM, 34 | HTTP: componentBar.HTTP 35 | } 36 | } 37 | ``` 38 | 39 | To get the same result without this `plugin` you may 40 | sacrifice functional style and do the following with your hands: 41 | 42 | ```js 43 | // import subject which is usually not needed 44 | import {Subject} from 'rx' 45 | import {ComponentFoo} from './ComponentFoo' 46 | import {ComponentBar} from './ComponentBar' 47 | 48 | const main = ({DOM, HTTP}) => { 49 | // declare proxy subject which will be used to subscribe 50 | // to target stream, and be a source for consumption 51 | const valueProxy$ = new Subject() 52 | // make proxy stream safe - when it ends (or terminates) 53 | // remove subscription to prevent memory leak 54 | const valueSafeProxy$ = valueProxy$.finally(() => { 55 | valueProxySub.dispose() 56 | }) 57 | 58 | const componentFoo = ComponentFoo({valueSafeProxy$, DOM}) 59 | const componentBar = ComponentBar({HTTP, componentFoo.prop$}) 60 | 61 | // create subscription for target stream 62 | // subscription is actually `side effect` 63 | const valueProxySub = componentBar.value$.subscribe(valueProxy$) 64 | 65 | return { 66 | DOM: componentFoo.DOM, 67 | HTTP: componentBar.HTTP 68 | } 69 | } 70 | 71 | ``` 72 | 73 | This also work for [`most.js`](https://github.com/cujojs/most) library, as if you write this: 74 | ```js 75 | import {subject} from 'most-subject' 76 | import {ComponentFoo} from './ComponentFoo' 77 | import {ComponentBar} from './ComponentBar' 78 | 79 | const main = ({DOM, HTTP}) => { 80 | 81 | const proxy = subject() 82 | 83 | const componentFoo = ComponentFoo({proxy.stream, DOM}) 84 | const componentBar = ComponentBar({HTTP, componentFoo.prop$}) 85 | 86 | const valueProxySub = componentBar.value$ 87 | .observe(proxy.observer.next) 88 | .then(proxy.observer.complete) 89 | .catch(proxy.observer.error) 90 | 91 | return { 92 | DOM: componentFoo.DOM, 93 | HTTP: componentBar.HTTP 94 | } 95 | } 96 | ``` 97 | 98 | ## Usage 99 | 100 | ```bash 101 | npm install babel-plugin-cycle-circular 102 | ``` 103 | 104 | Just add plugin to to your `.babelrc` file or transform options: 105 | ```json 106 | { 107 | "presets": ["es2015"], 108 | "plugins": ["cycle-circular"] 109 | } 110 | ``` 111 | 112 | ### Conventions 113 | 114 | *NB!* 115 | Works with [`rxjs v4`](https://github.com/Reactive-Extensions/RxJS), 116 | [`rxjs v5`](https://github.com/ReactiveX/rxjs) and [`most.js`](https://github.com/cujojs/most) ES6 sources. 117 | If you want to use CJS source you should `require` `Subject` manually. 118 | 119 | ### Options 120 | 121 | There are some options that you can supply to the plugin: 122 | * **lib** (default: 'rx') - what library rules to use for creating proxies, possilbe values: `"rx", "rxjs", "most"`. 123 | * **identifiers** (default: null) - regExp pattern(s) for matching identifiers names that should be proxied. 124 | * **include** (default: '') - includes files my `minimatch` mask (can be array) 125 | * **exclude** (default: '') - includes files my `minimatch` mask (can be array) 126 | 127 | **Options example:** 128 | This options for plugin will exclude from processing all files in `models/` folder 129 | and will proxy only if last identifier of reference ends with `$` for example `component.value$`, 130 | (references like `component.value` won't be handled) 131 | ```json 132 | { 133 | "presets": ["es2015"], 134 | "plugins": [ 135 | ["cycle-circular", { 136 | "lib": "rxjs", 137 | "identifiers": ["\\$$"], 138 | "exlude": ["**/models/**"] 139 | }] 140 | ] 141 | } 142 | ``` 143 | 144 | ## Is it safe to use? 145 | 146 | Technically, it just traverse (scans) each function during `babel` transpilation of your code 147 | to find variable references that go before declaration and applies *proxy* via *subject* . It was said that 148 | the plugin is experimental so if something goes wrong you should see it during development. 149 | 150 | ## Tests 151 | Tests checks if actual transformed source from `fixtures` 152 | corresponds to fixed transformed version in the same `fixtures/{case}` folder. 153 | ```bash 154 | npm run test 155 | ``` -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports.default = function (_ref) { 8 | var t = _ref.types; 9 | 10 | var makeVisitor = function makeVisitor(scope, options, lib) { 11 | 12 | var matchIdentifiers = toArray(options.identifiers).map(function (match) { 13 | return new RegExp(match); 14 | }); 15 | 16 | var mainScope = scope; 17 | 18 | var matchIdentifierName = function matchIdentifierName(name) { 19 | if (matchIdentifiers.length) { 20 | return matchIdentifiers.reduce(function (matched, match) { 21 | return matched || match.test(name); 22 | }, false); 23 | } 24 | return true; 25 | }; 26 | 27 | return { 28 | ReferencedIdentifier: function ReferencedIdentifier(path) { 29 | if (!checkScopeIsFunction(path.scope)) return; 30 | var _circular = getScopeCircular(path.scope); 31 | 32 | var name = '__' + path.node.name; 33 | 34 | if (_circular.declarations[name]) { 35 | return; 36 | } 37 | _circular.identifiers[name] = _circular.identifiers[name] || { paths: [] }; 38 | _circular.identifiers[name].paths.push(path); 39 | }, 40 | VariableDeclaration: function VariableDeclaration(path) { 41 | if (!checkScopeIsFunction(path.scope)) return; 42 | 43 | var _circular = getScopeCircular(path.scope); 44 | var body = path.scope.block.body.body; 45 | 46 | path.node.declarations.forEach(function (dec) { 47 | var identifier = _circular.identifiers['__' + dec.id.name]; 48 | if (identifier) { 49 | identifier.paths.forEach(function (path) { 50 | var identifierPath = getIdentifierFromPath(path); 51 | var identifierSource = identifierPath.getSource(); 52 | if (!matchIdentifierName(identifierSource)) { 53 | return; 54 | } 55 | var proxyName = '__Proxy' + _circular.proxiesCount; 56 | var proxy = lib.makeProxy(proxyName, identifierSource, options); 57 | 58 | body.unshift(proxy.declaration); 59 | identifierPath.replaceWith(proxy.replaceWith); 60 | 61 | var ret = body[body.length - 1].type == 'ReturnStatement' ? body.pop() : null; 62 | body.push(proxy.subscription); 63 | ret && body.push(ret); 64 | 65 | _circular.proxiesCount++; 66 | mainScope._hasCircularProxies = true; 67 | }); 68 | } 69 | _circular.declarations[dec.id.name] = true; 70 | }); 71 | } 72 | }; 73 | }; 74 | 75 | return { 76 | visitor: { 77 | Program: function Program(path, state) { 78 | var scope = path.context.scope; 79 | var options = this.opts; 80 | var filename = this.file.opts.filename; 81 | 82 | var filterFiles = options.include || options.exclude; 83 | if (filterFiles) { 84 | var match = checkMinimatch(filterFiles, filename); 85 | match = options.include ? !match && !checkMinimatch(options.exclude, filename) : match; 86 | if (match) { 87 | return; 88 | } 89 | } 90 | 91 | var lib = void 0, 92 | libName = options.lib || 'rx'; 93 | lib = require('./' + libName); 94 | 95 | path.traverse(makeVisitor(scope, options, lib)); 96 | 97 | if (scope._hasCircularProxies) { 98 | lib.addImports(path.node, scope, options); 99 | } 100 | } 101 | } 102 | }; 103 | }; 104 | 105 | var _minimatch = require('minimatch'); 106 | 107 | var _minimatch2 = _interopRequireDefault(_minimatch); 108 | 109 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 110 | 111 | var getIdentifierFromPath = function getIdentifierFromPath(path) { 112 | var type = path.parentPath.type; 113 | return type == 'MemberExpression' || type == 'CallExpression' ? getIdentifierFromPath(path.parentPath) : path; 114 | }; 115 | 116 | var toArray = function toArray(thing) { 117 | return thing && !Array.isArray(thing) ? [thing] : thing || []; 118 | }; 119 | 120 | var checkItems = function checkItems(items, checkFn) { 121 | return items.reduce(function (checked, item) { 122 | return checked || checkFn(item); 123 | }, false); 124 | }; 125 | 126 | var checkMinimatch = function checkMinimatch(masks, filename) { 127 | return checkItems(toArray(masks), function (mask) { 128 | return (0, _minimatch2.default)(filename, mask); 129 | }); 130 | }; 131 | 132 | var checkScopeIsFunction = function checkScopeIsFunction(scope) { 133 | // FunctionDeclaration 134 | // ArrowFunctionExpression 135 | return (/Function/.test(scope.block.type) 136 | ); 137 | }; 138 | 139 | var getScopeCircular = function getScopeCircular(scope) { 140 | if (scope._cycleCircular) { 141 | return scope._cycleCircular; 142 | } 143 | scope._cycleCircular = { 144 | identifiers: {}, 145 | declarations: {}, 146 | proxies: {}, 147 | proxiesCount: 0 148 | }; 149 | return scope._cycleCircular; 150 | }; -------------------------------------------------------------------------------- /lib/most.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.makeProxy = exports.addImports = undefined; 7 | 8 | var _babelTypes = require('babel-types'); 9 | 10 | var t = _interopRequireWildcard(_babelTypes); 11 | 12 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 13 | 14 | var subjectLiteral = 'subject'; 15 | var subjectIdentifier = t.identifier(subjectLiteral); 16 | 17 | var addImports = exports.addImports = function addImports(node, scope) { 18 | if (!scope.hasBinding('subject')) { 19 | var subjectImportDeclaration = t.importDeclaration([t.importSpecifier(t.identifier('subject'), t.identifier('subject'))], t.stringLiteral('most-subject')); 20 | node.body.unshift(subjectImportDeclaration); 21 | } 22 | }; 23 | 24 | var addChainCall = function addChainCall(identifier, methodName, argIdentifier, propertyName) { 25 | return t.callExpression(t.memberExpression(identifier, t.identifier(methodName)), [t.memberExpression(argIdentifier, t.identifier(propertyName))]); 26 | }; 27 | 28 | var makeProxy = exports.makeProxy = function makeProxy(name, source) { 29 | var streamIdentifier = t.identifier(name + '_Stream'); 30 | var observerIdentifier = t.identifier(name + '_Observer'); 31 | var declareIdentifier = t.objectPattern([t.ObjectProperty(streamIdentifier, streamIdentifier, false, true), t.ObjectProperty(observerIdentifier, observerIdentifier, false, true)]); 32 | 33 | var callExpression = t.callExpression(subjectIdentifier, []); 34 | var declaration = t.variableDeclaration('const', [t.variableDeclarator(declareIdentifier, callExpression)]); 35 | 36 | var originIdentifier = t.identifier(source); 37 | 38 | var subscriberIdentifier = t.identifier(name + '.asObserver()'); 39 | var subCallExpression = addChainCall(addChainCall(addChainCall(originIdentifier, 'observe', observerIdentifier, 'next'), 'then', observerIdentifier, 'complete'), 'catch', observerIdentifier, 'error'); 40 | 41 | var subIdentifier = t.identifier(name + '_Sub'); 42 | var subscription = t.variableDeclaration('const', [t.variableDeclarator(subIdentifier, subCallExpression)]); 43 | 44 | var replaceWith = streamIdentifier; 45 | 46 | return { 47 | declaration: declaration, 48 | subscription: subscription, 49 | replaceWith: replaceWith 50 | }; 51 | }; -------------------------------------------------------------------------------- /lib/rx.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.makeProxy = exports.addImports = exports.getSubjectArguments = exports.getSubjectLiteral = undefined; 7 | 8 | var _babelTypes = require('babel-types'); 9 | 10 | var t = _interopRequireWildcard(_babelTypes); 11 | 12 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 13 | 14 | //const subjectLiteral = 'Subject' 15 | //const subjectLiteral = 'ReplaySubject' 16 | //const subjectArguments = [t.identifier('1')] 17 | //const subjectArguments = [] 18 | //const subjectIdentifier = t.identifier(subjectLiteral) 19 | 20 | var getSubjectLiteral = exports.getSubjectLiteral = function getSubjectLiteral(options) { 21 | return options && options.replay ? 'ReplaySubject' : 'Subject'; 22 | }; 23 | 24 | var getSubjectArguments = exports.getSubjectArguments = function getSubjectArguments(options) { 25 | return options && options.replay ? [t.identifier('1')] : []; 26 | }; 27 | 28 | var addImports = exports.addImports = function addImports(node, scope, options) { 29 | var subjectLiteral = getSubjectLiteral(options); 30 | var subjectIdentifier = t.identifier(subjectLiteral); 31 | if (!scope.hasBinding(subjectLiteral)) { 32 | var subjectImportDeclaration = t.importDeclaration([t.importSpecifier(subjectIdentifier, subjectIdentifier)], t.stringLiteral('rx')); 33 | node.body.unshift(subjectImportDeclaration); 34 | } 35 | }; 36 | var getSubFinallyExpression = function getSubFinallyExpression(proxyIdentifier, subIdentifier) { 37 | var disposeExpression = t.callExpression(t.memberExpression(subIdentifier, t.identifier('dispose')), []); 38 | var observersCount = t.memberExpression(proxyIdentifier, t.identifier('observers.length')); 39 | var condition = t.ifStatement(t.binaryExpression("===", observersCount, t.identifier('0')), t.expressionStatement(disposeExpression)); 40 | return t.callExpression(t.memberExpression(proxyIdentifier, t.identifier('finally')), [t.arrowFunctionExpression([], t.blockStatement([condition]))]); 41 | }; 42 | 43 | var makeProxy = exports.makeProxy = function makeProxy(name, source, options) { 44 | var identifier = t.identifier(name); 45 | var subjectLiteral = getSubjectLiteral(options); 46 | var subjectIdentifier = t.identifier(subjectLiteral); 47 | var subjectArguments = getSubjectArguments(options); 48 | var newExpression = t.newExpression(subjectIdentifier, subjectArguments); 49 | var declaration = t.variableDeclaration('const', [t.variableDeclarator(identifier, newExpression)]); 50 | 51 | var originIdentifier = t.identifier(source + '.subscribe'); 52 | //let subscriberIdentifier = t.identifier(name + '.asObserver()') 53 | var subscriberIdentifier = identifier; 54 | var subCallExpression = t.callExpression(originIdentifier, [subscriberIdentifier]); 55 | var subIdentifier = t.identifier(name + '_Sub'); 56 | var subscription = t.variableDeclaration('const', [t.variableDeclarator(subIdentifier, subCallExpression)]); 57 | 58 | var replaceWith = getSubFinallyExpression(identifier, subIdentifier); 59 | 60 | return { 61 | declaration: declaration, 62 | subscription: subscription, 63 | replaceWith: replaceWith 64 | }; 65 | }; -------------------------------------------------------------------------------- /lib/rxjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.makeProxy = exports.addImports = undefined; 7 | 8 | var _babelTypes = require('babel-types'); 9 | 10 | var t = _interopRequireWildcard(_babelTypes); 11 | 12 | var _rx = require('./rx'); 13 | 14 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 15 | 16 | var addImports = exports.addImports = function addImports(node, scope, options) { 17 | var subjectLiteral = (0, _rx.getSubjectLiteral)(options); 18 | var subjectIdentifier = t.identifier(subjectLiteral); 19 | if (!scope.hasBinding(subjectLiteral)) { 20 | var subjectImportDeclaration = t.importDeclaration([t.importSpecifier(subjectIdentifier, subjectIdentifier)], t.stringLiteral('rxjs')); 21 | node.body.unshift(subjectImportDeclaration); 22 | } 23 | }; 24 | 25 | exports.makeProxy = _rx.makeProxy; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-cycle-circular", 3 | "version": "0.2.0", 4 | "description": "Babel plugin allowing to have circular dependencies in cycle.js functions.", 5 | "main": "lib/index.js", 6 | "keywords": [ 7 | "babel-plugin", 8 | "dx", 9 | "cyclejs" 10 | ], 11 | "scripts": { 12 | "clean": "rimraf lib", 13 | "build": "babel src -d lib", 14 | "build:watch": "babel src -w -d lib", 15 | "make-fixtures": "node -r babel-register test/make-fixtures", 16 | "make-fixtures:watch": "node-dev --respawn -r babel-register test/make-fixtures --yes", 17 | "test": "mocha --compilers js:babel-register test/index.js", 18 | "test:watch": "npm run test -- --watch", 19 | "prepublish": "npm run clean && npm run build" 20 | }, 21 | "author": "whitecolor", 22 | "license": "ISC", 23 | "devDependencies": { 24 | "assert": "^1.3.0", 25 | "babel-cli": "^6.6.5", 26 | "babel-core": "^6.7.4", 27 | "babel-preset-es2015": "^6.6.0", 28 | "babel-preset-stage-0": "^6.5.0", 29 | "mocha": "^2.4.5" 30 | }, 31 | "dependencies": { 32 | "minimatch": "^3.0.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import minimatch from 'minimatch' 2 | 3 | const getIdentifierFromPath = (path) => { 4 | let type = path.parentPath.type 5 | return type == 'MemberExpression' || type == 'CallExpression' 6 | ? getIdentifierFromPath(path.parentPath) : path 7 | } 8 | 9 | const toArray = (thing) => 10 | thing && !Array.isArray(thing) ? [thing] : (thing || []) 11 | 12 | 13 | const checkItems = (items, checkFn) => 14 | items.reduce((checked, item) => { 15 | return checked || checkFn(item) 16 | }, false) 17 | 18 | let checkMinimatch = (masks, filename) => checkItems(toArray(masks), 19 | (mask) => minimatch(filename, mask) 20 | ) 21 | 22 | const checkScopeIsFunction = (scope) => { 23 | // FunctionDeclaration 24 | // ArrowFunctionExpression 25 | return /Function/.test(scope.block.type) 26 | } 27 | 28 | const getScopeCircular = (scope) => { 29 | if (scope._cycleCircular){ 30 | return scope._cycleCircular 31 | } 32 | scope._cycleCircular = { 33 | identifiers: {}, 34 | declarations: {}, 35 | proxies: {}, 36 | proxiesCount: 0 37 | } 38 | return scope._cycleCircular 39 | } 40 | 41 | export default function ({types: t}) { 42 | const makeVisitor = (scope, options, lib) => { 43 | 44 | let matchIdentifiers = toArray(options.identifiers) 45 | .map(match => new RegExp(match)) 46 | 47 | let mainScope = scope 48 | 49 | const matchIdentifierName = (name) => { 50 | if (matchIdentifiers.length){ 51 | return matchIdentifiers.reduce((matched, match) => 52 | matched || match.test(name) 53 | , false) 54 | } 55 | return true 56 | } 57 | 58 | return { 59 | ReferencedIdentifier (path) { 60 | if (!checkScopeIsFunction(path.scope)) return 61 | let _circular = getScopeCircular(path.scope) 62 | 63 | let name = '__' + path.node.name 64 | 65 | if (_circular.declarations[name]){ 66 | return 67 | } 68 | _circular.identifiers[name] = _circular.identifiers[name] || {paths: []} 69 | _circular.identifiers[name].paths.push(path) 70 | }, 71 | 72 | VariableDeclaration (path) { 73 | if (!checkScopeIsFunction(path.scope)) return 74 | 75 | let _circular = getScopeCircular(path.scope) 76 | var body = path.scope.block.body.body 77 | 78 | path.node.declarations.forEach(dec => { 79 | let identifier = _circular.identifiers['__' + dec.id.name] 80 | if (identifier){ 81 | identifier.paths.forEach((path) => { 82 | let identifierPath = getIdentifierFromPath(path) 83 | let identifierSource = identifierPath.getSource() 84 | if (!matchIdentifierName(identifierSource)){ 85 | return 86 | } 87 | let proxyName = '__Proxy' + _circular.proxiesCount 88 | let proxy = lib.makeProxy(proxyName, identifierSource, options) 89 | 90 | body.unshift(proxy.declaration) 91 | identifierPath.replaceWith(proxy.replaceWith) 92 | 93 | let ret = body[body.length - 1].type == 'ReturnStatement' 94 | ? body.pop() : null 95 | body.push(proxy.subscription) 96 | ret && body.push(ret) 97 | 98 | _circular.proxiesCount++ 99 | mainScope._hasCircularProxies = true 100 | }) 101 | } 102 | _circular.declarations[dec.id.name] = true 103 | }) 104 | } 105 | } 106 | } 107 | 108 | return { 109 | visitor: { 110 | Program (path, state) { 111 | const scope = path.context.scope 112 | const options = this.opts 113 | const filename = this.file.opts.filename 114 | 115 | const filterFiles = options.include || options.exclude 116 | if (filterFiles){ 117 | let match = checkMinimatch(filterFiles, filename) 118 | match = options.include 119 | ? (!match && !checkMinimatch(options.exclude, filename)) 120 | : match 121 | if (match){ return } 122 | } 123 | 124 | let lib, libName = options.lib || 'rx' 125 | lib = require('./' + libName) 126 | 127 | path.traverse(makeVisitor(scope, options, lib)) 128 | 129 | if (scope._hasCircularProxies){ 130 | lib.addImports(path.node, scope, options) 131 | } 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/most.js: -------------------------------------------------------------------------------- 1 | import * as t from 'babel-types' 2 | 3 | let subjectLiteral = 'subject' 4 | let subjectIdentifier = t.identifier(subjectLiteral) 5 | 6 | export const addImports = (node, scope) => { 7 | if (!scope.hasBinding('subject')){ 8 | const subjectImportDeclaration = t.importDeclaration([ 9 | t.importSpecifier(t.identifier('subject'), t.identifier('subject')) 10 | ], t.stringLiteral('most-subject')); 11 | node.body.unshift(subjectImportDeclaration); 12 | } 13 | } 14 | 15 | const addChainCall = (identifier, methodName, argIdentifier, propertyName) => { 16 | return t.callExpression( 17 | t.memberExpression(identifier, t.identifier(methodName)), 18 | [t.memberExpression(argIdentifier, t.identifier(propertyName))] 19 | ) 20 | } 21 | 22 | export const makeProxy = (name, source) => { 23 | let streamIdentifier = t.identifier(name + '_Stream') 24 | let observerIdentifier = t.identifier(name + '_Observer') 25 | let declareIdentifier = t.objectPattern([ 26 | t.ObjectProperty(streamIdentifier, streamIdentifier, false, true), 27 | t.ObjectProperty(observerIdentifier, observerIdentifier, false, true) 28 | ]) 29 | 30 | let callExpression = t.callExpression(subjectIdentifier, []) 31 | let declaration = t.variableDeclaration('const', [ 32 | t.variableDeclarator(declareIdentifier, callExpression) 33 | ]) 34 | 35 | let originIdentifier = t.identifier(source) 36 | 37 | let subscriberIdentifier = t.identifier(name + '.asObserver()') 38 | let subCallExpression = 39 | addChainCall( 40 | addChainCall( 41 | addChainCall( 42 | originIdentifier, 'observe', observerIdentifier, 'next'), 43 | 'then', observerIdentifier, 'complete'), 44 | 'catch', observerIdentifier, 'error') 45 | 46 | let subIdentifier = t.identifier(name + '_Sub') 47 | let subscription = t.variableDeclaration('const', [ 48 | t.variableDeclarator(subIdentifier, subCallExpression) 49 | ]) 50 | 51 | let replaceWith = streamIdentifier 52 | 53 | return { 54 | declaration, 55 | subscription, 56 | replaceWith 57 | } 58 | } -------------------------------------------------------------------------------- /src/rx.js: -------------------------------------------------------------------------------- 1 | import * as t from 'babel-types' 2 | 3 | //const subjectLiteral = 'Subject' 4 | //const subjectLiteral = 'ReplaySubject' 5 | //const subjectArguments = [t.identifier('1')] 6 | //const subjectArguments = [] 7 | //const subjectIdentifier = t.identifier(subjectLiteral) 8 | 9 | export const getSubjectLiteral = (options) => { 10 | return (options && options.replay) ? 'ReplaySubject' : 'Subject' 11 | } 12 | 13 | export const getSubjectArguments = (options) => { 14 | return (options && options.replay) ? [t.identifier('1')] : [] 15 | } 16 | 17 | export const addImports = (node, scope, options) => { 18 | const subjectLiteral = getSubjectLiteral(options) 19 | const subjectIdentifier = t.identifier(subjectLiteral) 20 | if (!scope.hasBinding(subjectLiteral)){ 21 | const subjectImportDeclaration = t.importDeclaration([ 22 | t.importSpecifier(subjectIdentifier, subjectIdentifier) 23 | ], t.stringLiteral('rx')); 24 | node.body.unshift(subjectImportDeclaration); 25 | } 26 | } 27 | const getSubFinallyExpression = function(proxyIdentifier, subIdentifier){ 28 | const disposeExpression = t.callExpression( 29 | t.memberExpression(subIdentifier, t.identifier('dispose')), 30 | [] 31 | ) 32 | let observersCount = t.memberExpression(proxyIdentifier, t.identifier('observers.length')); 33 | const condition = t.ifStatement( 34 | t.binaryExpression("===", observersCount, t.identifier('0')), 35 | t.expressionStatement(disposeExpression) 36 | ) 37 | return t.callExpression( 38 | t.memberExpression(proxyIdentifier, t.identifier('finally')), 39 | [t.arrowFunctionExpression([], t.blockStatement([condition]))] 40 | ) 41 | } 42 | 43 | export const makeProxy = (name, source, options) => { 44 | let identifier = t.identifier(name) 45 | const subjectLiteral = getSubjectLiteral(options) 46 | const subjectIdentifier = t.identifier(subjectLiteral) 47 | const subjectArguments = getSubjectArguments(options) 48 | let newExpression = t.newExpression(subjectIdentifier, subjectArguments) 49 | let declaration = t.variableDeclaration('const', [ 50 | t.variableDeclarator(identifier, newExpression) 51 | ]) 52 | 53 | let originIdentifier = t.identifier(source + '.subscribe') 54 | //let subscriberIdentifier = t.identifier(name + '.asObserver()') 55 | let subscriberIdentifier = identifier 56 | let subCallExpression = t.callExpression(originIdentifier, [subscriberIdentifier]) 57 | let subIdentifier = t.identifier(name + '_Sub') 58 | let subscription = t.variableDeclaration('const', [ 59 | t.variableDeclarator(subIdentifier, subCallExpression) 60 | ]) 61 | 62 | let replaceWith = getSubFinallyExpression(identifier, subIdentifier) 63 | 64 | return { 65 | declaration, 66 | subscription, 67 | replaceWith 68 | } 69 | } -------------------------------------------------------------------------------- /src/rxjs.js: -------------------------------------------------------------------------------- 1 | import * as t from 'babel-types' 2 | import {makeProxy, getSubjectLiteral} from './rx' 3 | 4 | export const addImports = (node, scope, options) => { 5 | const subjectLiteral = getSubjectLiteral(options) 6 | const subjectIdentifier = t.identifier(subjectLiteral) 7 | if (!scope.hasBinding(subjectLiteral)){ 8 | const subjectImportDeclaration = t.importDeclaration([ 9 | t.importSpecifier(subjectIdentifier, subjectIdentifier) 10 | ], t.stringLiteral('rxjs')); 11 | node.body.unshift(subjectImportDeclaration); 12 | } 13 | } 14 | 15 | export {makeProxy} 16 | -------------------------------------------------------------------------------- /test/fixtures/exclude/source.js: -------------------------------------------------------------------------------- 1 | import {Subject} from 'rx' 2 | import {Component1} from './component1' 3 | import {Component2} from './component2' 4 | 5 | function main ({DOM, HTTP}) { 6 | 7 | var component1 = Component1({value$: component2.value$, DOM}) 8 | var component2 = Component2({HTTP}) 9 | 10 | return { 11 | DOM: component1.DOM, 12 | HTTP: component2.HTTP 13 | } 14 | } -------------------------------------------------------------------------------- /test/fixtures/exclude/transformed.js: -------------------------------------------------------------------------------- 1 | import { Subject } from 'rx'; 2 | import { Component1 } from './component1'; 3 | import { Component2 } from './component2'; 4 | 5 | function main({ DOM, HTTP }) { 6 | 7 | var component1 = Component1({ value$: component2.value$, DOM }); 8 | var component2 = Component2({ HTTP }); 9 | 10 | return { 11 | DOM: component1.DOM, 12 | HTTP: component2.HTTP 13 | }; 14 | } -------------------------------------------------------------------------------- /test/fixtures/most-basic/source.js: -------------------------------------------------------------------------------- 1 | import {Component1} from './component1' 2 | import {Component2} from './component2' 3 | 4 | const main = ({DOM, HTTP: string}) => { 5 | var component1 = Component1({value$: component2.value$, DOM}) 6 | var component2 = Component2({HTTP}) 7 | 8 | return { 9 | DOM: component1.DOM, 10 | HTTP: component2.HTTP 11 | } 12 | } -------------------------------------------------------------------------------- /test/fixtures/most-basic/transformed.js: -------------------------------------------------------------------------------- 1 | import { subject } from 'most-subject'; 2 | import { Component1 } from './component1'; 3 | import { Component2 } from './component2'; 4 | 5 | const main = ({ DOM, HTTP: string }) => { 6 | const { 7 | __Proxy0_Stream, 8 | __Proxy0_Observer 9 | } = subject(); 10 | 11 | var component1 = Component1({ value$: __Proxy0_Stream, DOM }); 12 | var component2 = Component2({ HTTP }); 13 | 14 | const __Proxy0_Sub = component2.value$.observe(__Proxy0_Observer.next).then(__Proxy0_Observer.complete).catch(__Proxy0_Observer.error); 15 | 16 | return { 17 | DOM: component1.DOM, 18 | HTTP: component2.HTTP 19 | }; 20 | }; -------------------------------------------------------------------------------- /test/fixtures/rx-basic/source.js: -------------------------------------------------------------------------------- 1 | import {Component1} from './component1' 2 | import {Component2} from './component2' 3 | 4 | const main = ({DOM, HTTP: string}) => { 5 | var component1 = Component1({value$: component2.value$.startWith(1), DOM}) 6 | var component2 = Component2({HTTP}) 7 | 8 | return { 9 | DOM: component1.DOM, 10 | HTTP: component2.HTTP 11 | } 12 | } -------------------------------------------------------------------------------- /test/fixtures/rx-basic/transformed.js: -------------------------------------------------------------------------------- 1 | import { ReplaySubject } from 'rx'; 2 | import { Component1 } from './component1'; 3 | import { Component2 } from './component2'; 4 | 5 | const main = ({ DOM, HTTP: string }) => { 6 | const __Proxy0 = new ReplaySubject(1); 7 | 8 | var component1 = Component1({ value$: __Proxy0.finally(() => { 9 | if (__Proxy0.observers.length === 0) __Proxy0_Sub.dispose(); 10 | }), DOM }); 11 | var component2 = Component2({ HTTP }); 12 | 13 | const __Proxy0_Sub = component2.value$.startWith(1).subscribe(__Proxy0); 14 | 15 | return { 16 | DOM: component1.DOM, 17 | HTTP: component2.HTTP 18 | }; 19 | }; -------------------------------------------------------------------------------- /test/fixtures/rx-filter-identifier/source.js: -------------------------------------------------------------------------------- 1 | import {Component1} from './component1' 2 | import {Component2} from './component2' 3 | 4 | function mainFiltered ({DOM, HTTP}) { 5 | 6 | var component1 = Component1({value$: component2.value$, DOM}) 7 | var x = doNotProxyMe 8 | var component2 = Component2({HTTP}) 9 | var doNotProxyMe = 2 10 | 11 | return { 12 | DOM: component1.DOM, 13 | HTTP: component2.HTTP 14 | } 15 | } -------------------------------------------------------------------------------- /test/fixtures/rx-filter-identifier/transformed.js: -------------------------------------------------------------------------------- 1 | import { ReplaySubject } from 'rx'; 2 | import { Component1 } from './component1'; 3 | import { Component2 } from './component2'; 4 | 5 | function mainFiltered({ DOM, HTTP }) { 6 | const __Proxy0 = new ReplaySubject(1); 7 | 8 | var component1 = Component1({ value$: __Proxy0.finally(() => { 9 | if (__Proxy0.observers.length === 0) __Proxy0_Sub.dispose(); 10 | }), DOM }); 11 | var x = doNotProxyMe; 12 | var component2 = Component2({ HTTP }); 13 | var doNotProxyMe = 2; 14 | 15 | const __Proxy0_Sub = component2.value$.subscribe(__Proxy0); 16 | 17 | return { 18 | DOM: component1.DOM, 19 | HTTP: component2.HTTP 20 | }; 21 | } -------------------------------------------------------------------------------- /test/fixtures/rx-subject-imported/source.js: -------------------------------------------------------------------------------- 1 | import {Component1} from './component1' 2 | import {Component2} from './component2' 3 | import {Subject} from 'rx' 4 | 5 | const main = ({DOM, HTTP: string}) => { 6 | var component1 = Component1({value$: component2.value$, DOM}) 7 | var component2 = Component2({HTTP}) 8 | 9 | return { 10 | DOM: component1.DOM, 11 | HTTP: component2.HTTP 12 | } 13 | } -------------------------------------------------------------------------------- /test/fixtures/rx-subject-imported/transformed.js: -------------------------------------------------------------------------------- 1 | import { ReplaySubject } from 'rx'; 2 | import { Component1 } from './component1'; 3 | import { Component2 } from './component2'; 4 | import { Subject } from 'rx'; 5 | 6 | const main = ({ DOM, HTTP: string }) => { 7 | const __Proxy0 = new ReplaySubject(1); 8 | 9 | var component1 = Component1({ value$: __Proxy0.finally(() => { 10 | if (__Proxy0.observers.length === 0) __Proxy0_Sub.dispose(); 11 | }), DOM }); 12 | var component2 = Component2({ HTTP }); 13 | 14 | const __Proxy0_Sub = component2.value$.subscribe(__Proxy0); 15 | 16 | return { 17 | DOM: component1.DOM, 18 | HTTP: component2.HTTP 19 | }; 20 | }; -------------------------------------------------------------------------------- /test/fixtures/rxjs-basic/source.js: -------------------------------------------------------------------------------- 1 | import {Component1} from './component1' 2 | import {Component2} from './component2' 3 | 4 | const main = ({DOM, HTTP}) => { 5 | var component1 = Component1({value$: component2.value$, DOM}) 6 | var component2 = Component2({HTTP}) 7 | 8 | return { 9 | DOM: component1.DOM, 10 | HTTP: component2.HTTP 11 | } 12 | } -------------------------------------------------------------------------------- /test/fixtures/rxjs-basic/transformed.js: -------------------------------------------------------------------------------- 1 | import { ReplaySubject } from 'rxjs'; 2 | import { Component1 } from './component1'; 3 | import { Component2 } from './component2'; 4 | 5 | const main = ({ DOM, HTTP }) => { 6 | const __Proxy0 = new ReplaySubject(1); 7 | 8 | var component1 = Component1({ value$: __Proxy0.finally(() => { 9 | if (__Proxy0.observers.length === 0) __Proxy0_Sub.dispose(); 10 | }), DOM }); 11 | var component2 = Component2({ HTTP }); 12 | 13 | const __Proxy0_Sub = component2.value$.subscribe(__Proxy0); 14 | 15 | return { 16 | DOM: component1.DOM, 17 | HTTP: component2.HTTP 18 | }; 19 | }; -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import transform from './transform' 3 | 4 | describe('finds references to proxy', () => { 5 | transform(({actual, expected, caseName}) => { 6 | it(`handles ${caseName.split('-').join(' ')} case`, () => { 7 | assert.equal(actual, expected) 8 | }) 9 | }) 10 | }) -------------------------------------------------------------------------------- /test/make-fixtures.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import transform from './transform' 3 | import reedline from 'readline' 4 | 5 | const make = (answer) => { 6 | const doForAll = answer === 'yes' 7 | transform(({caseName, actual, expected, expectedPath}) => { 8 | if (expected && expected !== actual){ 9 | console.warn('Expected result is not equal to actual for', caseName) 10 | } 11 | if (!expected || doForAll){ 12 | console.log('Save transformed fixture for ', caseName) 13 | fs.writeFileSync(expectedPath, actual) 14 | } else { 15 | console.log('Skipping save transformed fixture for', caseName) 16 | } 17 | }) 18 | } 19 | 20 | if (process.argv[2]){ 21 | make(process.argv[2].replace(/\W/g, '')) 22 | } else { 23 | const rl = reedline.createInterface({ 24 | input: process.stdin, 25 | output: process.stdout 26 | }) 27 | 28 | rl.question('Save all even if fixture presents? (no)', make) 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/transform.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { transformFileSync } from 'babel-core' 4 | import plugin from '../lib' 5 | 6 | function trim(str) { 7 | return str.replace(/^\s+|\s+$/, '') 8 | } 9 | 10 | const fixturesDir = path.join(__dirname, 'fixtures') 11 | 12 | export const transformFixtures = (handler) => { 13 | fs.readdirSync(fixturesDir) 14 | //.filter(path => /rx-basic/.test(path)) 15 | .map((caseName) => { 16 | var lib = caseName.split('-')[0] 17 | const options = { 18 | babelrc: false, 19 | plugins: [ 20 | [plugin, { 21 | identifiers: '\\$($|\\.)', 22 | include: '', 23 | exclude: '**/exclude/**', 24 | replay: true, 25 | lib: lib 26 | }] 27 | ] 28 | } 29 | const fixtureDir = path.join(fixturesDir, caseName) 30 | const actualPath = path.join(fixtureDir, 'source.js') 31 | const expectedPath = path.join(fixtureDir, 'transformed.js') 32 | const actual = trim(transformFileSync(actualPath, options).code) 33 | 34 | const expected = fs.existsSync(expectedPath) 35 | && trim(fs.readFileSync(expectedPath, 'utf-8')) 36 | 37 | handler && handler({caseName, actual, expected, expectedPath}) 38 | }) 39 | } 40 | 41 | export default transformFixtures --------------------------------------------------------------------------------