├── .babelrc ├── .github └── workflows │ └── action-ci.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── example ├── .babelrc ├── index.html ├── package.json ├── src │ ├── decorator.js │ └── index.js └── webpack.config.js ├── lib └── index.js ├── package-lock.json ├── package.json ├── src └── index.js ├── test ├── greeter.js ├── index.js └── src │ ├── decorators │ └── index.js │ ├── js │ ├── .babelrc │ └── index.js │ └── ts │ ├── .babelrc │ ├── Greeter.ts │ ├── GreeterFactory.ts │ ├── Sentinel.ts │ └── UserRepo.ts └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | "@babel/plugin-transform-runtime" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/action-ci.yml: -------------------------------------------------------------------------------- 1 | name: Action CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [ '10' ] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install, build, and test 21 | run: | 22 | npm install 23 | npm run build --if-present 24 | npm test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | dist/ 4 | test/lib -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example/ 2 | test/ 3 | src/ 4 | .idea/ 5 | .github/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | script: 5 | - babel -x .js,.ts test/src/ -d test/lib && ava --tap 6 | after_success: 7 | - bash <(curl -s https://codecov.io/bash) 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Babel Plugin Parameter Decorator 2 | 3 | [![](https://travis-ci.com/WarnerHooh/babel-plugin-parameter-decorator.svg?branch=master)](https://travis-ci.com/WarnerHooh/babel-plugin-parameter-decorator) 4 | [![](https://badge.fury.io/js/babel-plugin-parameter-decorator.svg)](https://badge.fury.io/js/babel-plugin-parameter-decorator) 5 | [![](https://img.shields.io/npm/dt/babel-plugin-parameter-decorator.svg)](https://www.npmjs.com/package/babel-plugin-parameter-decorator) 6 | [![](https://img.shields.io/npm/dm/babel-plugin-parameter-decorator.svg)](https://www.npmjs.com/package/babel-plugin-parameter-decorator) 7 | 8 | Function parameter decorator transform plugin for babel v7, just like typescript [parameter decorator](https://www.typescriptlang.org/docs/handbook/decorators.html#parameter-decorators) 9 | 10 | ```javascript 11 | function validate(target, property, descriptor) { 12 | const fn = descriptor.value; 13 | 14 | descriptor.value = function (...args) { 15 | const metadata = `meta_${property}`; 16 | target[metadata].forEach(function (metadata) { 17 | if (args[metadata.index] === undefined) { 18 | throw new Error(`${metadata.key} is required`); 19 | } 20 | }); 21 | 22 | return fn.apply(this, args); 23 | }; 24 | 25 | return descriptor; 26 | } 27 | 28 | function required(key) { 29 | return function (target, propertyKey, parameterIndex) { 30 | const metadata = `meta_${propertyKey}`; 31 | target[metadata] = [ 32 | ...(target[metadata] || []), 33 | { 34 | index: parameterIndex, 35 | key 36 | } 37 | ] 38 | }; 39 | } 40 | 41 | class Greeter { 42 | constructor(message) { 43 | this.greeting = message; 44 | } 45 | 46 | @validate 47 | greet(@required('name') name) { 48 | return "Hello " + name + ", " + this.greeting; 49 | } 50 | } 51 | ``` 52 | 53 | #### NOTE: 54 | 55 | This package depends on `@babel/plugin-proposal-decorators`. 56 | 57 | ## Installation & Usage 58 | 59 | `npm install @babel/plugin-proposal-decorators babel-plugin-parameter-decorator -D` 60 | 61 | And the `.babelrc` looks like: 62 | 63 | ``` 64 | { 65 | "plugins": [ 66 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 67 | "babel-plugin-parameter-decorator" 68 | ] 69 | } 70 | ``` 71 | 72 | By default, `@babel/preset-typescript` will remove imports only referenced in Decorators. 73 | Since this is prone to break Decorators, make sure [disable it by setting `onlyRemoveTypeImports` to true](https://babeljs.io/docs/en/babel-preset-typescript#onlyremovetypeimports): 74 | 75 | ``` 76 | { 77 | ... 78 | "presets": [ 79 | [ 80 | "@babel/preset-typescript", 81 | { "onlyRemoveTypeImports": true } 82 | ] 83 | ] 84 | ... 85 | } 86 | ``` 87 | 88 | ## Additional 89 | 90 | If you'd like to compile typescript files by babel, the file extensions `.ts` or `.tsx` expected, or we will get runtime error! 91 | 92 | 🎊 Hopefully this plugin would get along with typescript `private/public` keywords in `constructor`. For [example](https://github.com/WarnerHooh/babel-plugin-parameter-decorator/blob/dev/test/src/ts/Greeter.ts), 93 | 94 | ```typescript 95 | @Factory 96 | class Greeter { 97 | 98 | private counter: Counter = this.sentinel.counter; 99 | 100 | constructor(private greeting: string, @Inject(Sentinel) private sentinel: Sentinel) { 101 | } 102 | 103 | @validate 104 | greet(@required('name') name: string) { 105 | return "Hello " + name + ", " + this.greeting; 106 | } 107 | 108 | count() { 109 | return this.counter.number; 110 | } 111 | } 112 | ``` 113 | And your `.babelrc` looks like: 114 | 115 | ``` 116 | { 117 | "presets": [ 118 | "@babel/preset-env", 119 | "@babel/preset-typescript" 120 | ], 121 | "plugins": [ 122 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 123 | ["@babel/plugin-proposal-class-properties", { "loose" : true }], 124 | "babel-plugin-parameter-decorator" 125 | ] 126 | } 127 | ``` 128 | -------------------------------------------------------------------------------- /example/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 4 | "../lib/index.js" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Document 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-parameter-decorator", 3 | "version": "1.0.0", 4 | "description": "Function parameter decorator transform plugin for babel v7, just like typescript.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --mode development" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/WarnerHooh/babel-plugin-parameter-decorator.git" 12 | }, 13 | "author": "Warner", 14 | "license": "MIT", 15 | "keywords": [ 16 | "babel", 17 | "babel-plugin", 18 | "function", 19 | "parameter", 20 | "decorators", 21 | "typescript" 22 | ], 23 | "devDependencies": { 24 | "@babel/core": "^7.2.2", 25 | "@babel/plugin-proposal-decorators": "^7.2.3", 26 | "@babel/preset-env": "^7.2.3", 27 | "babel-loader": "^8.0.5", 28 | "webpack": "^4.28.4", 29 | "webpack-cli": "^3.2.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example/src/decorator.js: -------------------------------------------------------------------------------- 1 | function decoratorBuilder(type) { 2 | return function (key) { 3 | return function (target, methodName, paramIndex) { 4 | console.log(`---- @${type} ----`); 5 | console.log('key: ', key); 6 | console.log('target: ', target); 7 | console.log('methodName: ', methodName); 8 | console.log('paramIndex: ', paramIndex); 9 | console.log('paramIndex: ', paramIndex); 10 | }; 11 | }; 12 | } 13 | 14 | export const Foo = decoratorBuilder('Foo'); 15 | export const Bar = decoratorBuilder('Bar'); 16 | export const Baz = decoratorBuilder('Baz'); -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import {Bar, Baz, Foo} from "./decorator"; 2 | 3 | export default class Demo{ 4 | hello(@Foo('foo')@Bar('bar') param1, @Baz('baz') param2) { 5 | } 6 | } 7 | 8 | const demo = new Demo(); 9 | demo.hello(11, 22); 10 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | output: { 6 | path: path.resolve(__dirname, 'dist'), 7 | filename: 'index.bundle.js' 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.js$/, 13 | exclude: /node_modules/, 14 | use: { 15 | loader: 'babel-loader', 16 | options: { 17 | presets: ['@babel/preset-env'] 18 | } 19 | } 20 | } 21 | ] 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _path = require("path"); 4 | 5 | function isInType(path) { 6 | switch (path.parent.type) { 7 | case "TSTypeReference": 8 | case "TSQualifiedName": 9 | case "TSExpressionWithTypeArguments": 10 | case "TSTypeQuery": 11 | return true; 12 | 13 | default: 14 | return false; 15 | } 16 | } 17 | 18 | module.exports = function (_ref) { 19 | var types = _ref.types; 20 | 21 | var decoratorExpressionForConstructor = function decoratorExpressionForConstructor(decorator, param) { 22 | return function (className) { 23 | var resultantDecorator = types.callExpression(decorator.expression, [types.Identifier(className), types.Identifier('undefined'), types.NumericLiteral(param.key)]); 24 | var resultantDecoratorWithFallback = types.logicalExpression("||", resultantDecorator, types.Identifier(className)); 25 | var assignment = types.assignmentExpression('=', types.Identifier(className), resultantDecoratorWithFallback); 26 | return types.expressionStatement(assignment); 27 | }; 28 | }; 29 | 30 | var decoratorExpressionForMethod = function decoratorExpressionForMethod(decorator, param) { 31 | return function (className, functionName) { 32 | var resultantDecorator = types.callExpression(decorator.expression, [types.Identifier("".concat(className, ".prototype")), types.StringLiteral(functionName), types.NumericLiteral(param.key)]); 33 | return types.expressionStatement(resultantDecorator); 34 | }; 35 | }; 36 | 37 | var findIdentifierAfterAssignment = function findIdentifierAfterAssignment(path) { 38 | var assignment = path.findParent(function (p) { 39 | return p.node.type === 'AssignmentExpression'; 40 | }); 41 | 42 | if (assignment.node.right.type === 'SequenceExpression') { 43 | return assignment.node.right.expressions[1].name; 44 | } else if (assignment.node.right.type === 'ClassExpression') { 45 | return assignment.node.left.name; 46 | } 47 | 48 | return null; 49 | }; 50 | 51 | var getParamReplacement = function getParamReplacement(path) { 52 | switch (path.node.type) { 53 | case 'ObjectPattern': 54 | return types.ObjectPattern(path.node.properties); 55 | 56 | case 'AssignmentPattern': 57 | return types.AssignmentPattern(path.node.left, path.node.right); 58 | 59 | case 'TSParameterProperty': 60 | return types.Identifier(path.node.parameter.name); 61 | 62 | default: 63 | return types.Identifier(path.node.name); 64 | } 65 | }; 66 | 67 | return { 68 | visitor: { 69 | /** 70 | * For typescript compilation. Avoid import statement of param decorator functions being Elided. 71 | */ 72 | Program: function Program(path, state) { 73 | var extension = (0, _path.extname)(state.file.opts.filename); 74 | 75 | if (extension === '.ts' || extension === '.tsx') { 76 | (function () { 77 | var decorators = Object.create(null); 78 | path.node.body.filter(function (it) { 79 | var type = it.type, 80 | declaration = it.declaration; 81 | 82 | switch (type) { 83 | case "ClassDeclaration": 84 | return true; 85 | 86 | case "ExportNamedDeclaration": 87 | case "ExportDefaultDeclaration": 88 | return declaration && declaration.type === "ClassDeclaration"; 89 | 90 | default: 91 | return false; 92 | } 93 | }).map(function (it) { 94 | return it.type === 'ClassDeclaration' ? it : it.declaration; 95 | }).forEach(function (clazz) { 96 | clazz.body.body.forEach(function (body) { 97 | (body.params || []).forEach(function (param) { 98 | (param.decorators || []).forEach(function (decorator) { 99 | if (decorator.expression.callee) { 100 | decorators[decorator.expression.callee.name] = decorator; 101 | } else { 102 | decorators[decorator.expression.name] = decorator; 103 | } 104 | }); 105 | }); 106 | }); 107 | }); 108 | var _iteratorNormalCompletion = true; 109 | var _didIteratorError = false; 110 | var _iteratorError = undefined; 111 | 112 | try { 113 | for (var _iterator = path.get("body")[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 114 | var stmt = _step.value; 115 | 116 | if (stmt.node.type === 'ImportDeclaration') { 117 | if (stmt.node.specifiers.length === 0) { 118 | continue; 119 | } 120 | 121 | var _iteratorNormalCompletion2 = true; 122 | var _didIteratorError2 = false; 123 | var _iteratorError2 = undefined; 124 | 125 | try { 126 | var _loop = function _loop() { 127 | var specifier = _step2.value; 128 | var binding = stmt.scope.getBinding(specifier.local.name); 129 | 130 | if (!binding.referencePaths.length) { 131 | if (decorators[specifier.local.name]) { 132 | binding.referencePaths.push({ 133 | parent: decorators[specifier.local.name] 134 | }); 135 | } 136 | } else { 137 | var allTypeRefs = binding.referencePaths.reduce(function (prev, next) { 138 | return prev || isInType(next); 139 | }, false); 140 | 141 | if (allTypeRefs) { 142 | Object.keys(decorators).forEach(function (k) { 143 | var decorator = decorators[k]; 144 | (decorator.expression.arguments || []).forEach(function (arg) { 145 | if (arg.name === specifier.local.name) { 146 | binding.referencePaths.push({ 147 | parent: decorator.expression 148 | }); 149 | } 150 | }); 151 | }); 152 | } 153 | } 154 | }; 155 | 156 | for (var _iterator2 = stmt.node.specifiers[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { 157 | _loop(); 158 | } 159 | } catch (err) { 160 | _didIteratorError2 = true; 161 | _iteratorError2 = err; 162 | } finally { 163 | try { 164 | if (!_iteratorNormalCompletion2 && _iterator2["return"] != null) { 165 | _iterator2["return"](); 166 | } 167 | } finally { 168 | if (_didIteratorError2) { 169 | throw _iteratorError2; 170 | } 171 | } 172 | } 173 | } 174 | } 175 | } catch (err) { 176 | _didIteratorError = true; 177 | _iteratorError = err; 178 | } finally { 179 | try { 180 | if (!_iteratorNormalCompletion && _iterator["return"] != null) { 181 | _iterator["return"](); 182 | } 183 | } finally { 184 | if (_didIteratorError) { 185 | throw _iteratorError; 186 | } 187 | } 188 | } 189 | })(); 190 | } 191 | }, 192 | Function: function Function(path) { 193 | var functionName = ''; 194 | 195 | if (path.node.id) { 196 | functionName = path.node.id.name; 197 | } else if (path.node.key) { 198 | functionName = path.node.key.name; 199 | } 200 | 201 | (path.get('params') || []).slice().forEach(function (param) { 202 | var decorators = param.node.decorators || []; 203 | var transformable = decorators.length; 204 | decorators.slice().forEach(function (decorator) { 205 | // For class support env 206 | if (path.type === 'ClassMethod') { 207 | var parentNode = path.parentPath.parentPath; 208 | var classDeclaration = path.findParent(function (p) { 209 | return p.type === 'ClassDeclaration'; 210 | }); 211 | var classIdentifier; // without class decorator 212 | 213 | if (classDeclaration) { 214 | classIdentifier = classDeclaration.node.id.name; // with class decorator 215 | } else { 216 | // Correct the temp identifier reference 217 | parentNode.insertAfter(null); 218 | classIdentifier = findIdentifierAfterAssignment(path); 219 | } 220 | 221 | if (functionName === 'constructor') { 222 | var expression = decoratorExpressionForConstructor(decorator, param)(classIdentifier); // TODO: the order of insertion 223 | 224 | parentNode.insertAfter(expression); 225 | } else { 226 | var _expression = decoratorExpressionForMethod(decorator, param)(classIdentifier, functionName); // TODO: the order of insertion 227 | 228 | 229 | parentNode.insertAfter(_expression); 230 | } 231 | } else { 232 | var classDeclarator = path.findParent(function (p) { 233 | return p.node.type === 'VariableDeclarator'; 234 | }); 235 | var className = classDeclarator.node.id.name; 236 | 237 | if (functionName === className) { 238 | var _expression2 = decoratorExpressionForConstructor(decorator, param)(className); // TODO: the order of insertion 239 | 240 | 241 | if (path.parentKey === 'body') { 242 | path.insertAfter(_expression2); // In case there is only a constructor method 243 | } else { 244 | var bodyParent = path.findParent(function (p) { 245 | return p.parentKey === 'body'; 246 | }); 247 | bodyParent.insertAfter(_expression2); 248 | } 249 | } else { 250 | var classParent = path.findParent(function (p) { 251 | return p.node.type === 'CallExpression'; 252 | }); 253 | 254 | var _expression3 = decoratorExpressionForMethod(decorator, param)(className, functionName); // TODO: the order of insertion 255 | 256 | 257 | classParent.insertAfter(_expression3); 258 | } 259 | } 260 | }); 261 | 262 | if (transformable) { 263 | var replacement = getParamReplacement(param); 264 | param.replaceWith(replacement); 265 | } 266 | }); 267 | } 268 | } 269 | }; 270 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-parameter-decorator", 3 | "version": "1.0.16", 4 | "description": "Function parameter decorator transform plugin for babel v7, just like typescript.", 5 | "main": "lib/index.js", 6 | "engine": { 7 | "node": "*" 8 | }, 9 | "scripts": { 10 | "build": "babel src/index.js -d lib", 11 | "test": "babel -x .js,.ts test/src/ -d test/lib && ava", 12 | "prepublish": "npm run build" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/WarnerHooh/babel-plugin-parameter-decorator.git" 17 | }, 18 | "author": "Warner", 19 | "license": "MIT", 20 | "keywords": [ 21 | "babel", 22 | "babel-plugin", 23 | "function", 24 | "parameter", 25 | "decorators", 26 | "typescript" 27 | ], 28 | "devDependencies": { 29 | "@babel/cli": "^7.5.5", 30 | "@babel/core": "^7.5.5", 31 | "@babel/plugin-proposal-class-properties": "^7.5.5", 32 | "@babel/plugin-proposal-decorators": "^7.4.4", 33 | "@babel/plugin-transform-runtime": "^7.5.5", 34 | "@babel/preset-env": "^7.5.5", 35 | "@babel/preset-typescript": "^7.9.0", 36 | "@babel/runtime": "^7.5.5", 37 | "ava": "^2.3.0" 38 | }, 39 | "ava": { 40 | "files": [ 41 | "./test/*.js" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { extname } from 'path'; 2 | 3 | function isInType(path) { 4 | switch (path.parent.type) { 5 | case "TSTypeReference": 6 | case "TSQualifiedName": 7 | case "TSExpressionWithTypeArguments": 8 | case "TSTypeQuery": 9 | return true; 10 | 11 | default: 12 | return false; 13 | } 14 | } 15 | 16 | module.exports = function ({ types }) { 17 | const decoratorExpressionForConstructor = (decorator, param) => (className) => { 18 | const resultantDecorator = types.callExpression( 19 | decorator.expression, [ 20 | types.Identifier(className), 21 | types.Identifier('undefined'), 22 | types.NumericLiteral(param.key) 23 | ] 24 | ); 25 | const resultantDecoratorWithFallback = types.logicalExpression("||", resultantDecorator, types.Identifier(className)); 26 | const assignment = types.assignmentExpression('=', types.Identifier(className), resultantDecoratorWithFallback); 27 | return types.expressionStatement(assignment); 28 | }; 29 | 30 | const decoratorExpressionForMethod = (decorator, param) => (className, functionName) => { 31 | const resultantDecorator = types.callExpression( 32 | decorator.expression, [ 33 | types.Identifier(`${className}.prototype`), 34 | types.StringLiteral(functionName), 35 | types.NumericLiteral(param.key) 36 | ] 37 | ); 38 | 39 | return types.expressionStatement(resultantDecorator); 40 | }; 41 | 42 | const findIdentifierAfterAssignment = (path) => { 43 | const assignment = path.findParent(p => p.node.type === 'AssignmentExpression'); 44 | 45 | if (assignment.node.right.type === 'SequenceExpression') { 46 | return assignment.node.right.expressions[1].name; 47 | } else if (assignment.node.right.type === 'ClassExpression') { 48 | return assignment.node.left.name; 49 | } 50 | 51 | return null; 52 | }; 53 | 54 | const getParamReplacement = (path) => { 55 | switch (path.node.type) { 56 | case 'ObjectPattern': 57 | return types.ObjectPattern(path.node.properties); 58 | case 'AssignmentPattern': 59 | return types.AssignmentPattern(path.node.left, path.node.right); 60 | case 'TSParameterProperty': 61 | return types.Identifier(path.node.parameter.name); 62 | default: 63 | return types.Identifier(path.node.name); 64 | } 65 | }; 66 | 67 | return { 68 | visitor: { 69 | /** 70 | * For typescript compilation. Avoid import statement of param decorator functions being Elided. 71 | */ 72 | Program(path, state) { 73 | const extension = extname(state.file.opts.filename); 74 | 75 | if (extension === '.ts' || extension === '.tsx') { 76 | const decorators = Object.create(null); 77 | 78 | path.node.body 79 | .filter(it => { 80 | const { type, declaration } = it; 81 | 82 | switch (type) { 83 | case "ClassDeclaration": 84 | return true; 85 | 86 | case "ExportNamedDeclaration": 87 | case "ExportDefaultDeclaration": 88 | return declaration && declaration.type === "ClassDeclaration"; 89 | 90 | default: 91 | return false; 92 | } 93 | }) 94 | .map(it => { 95 | return it.type === 'ClassDeclaration' ? it : it.declaration; 96 | }) 97 | .forEach(clazz => { 98 | clazz.body.body.forEach(function (body) { 99 | 100 | (body.params || []).forEach(function (param) { 101 | (param.decorators || []).forEach(function (decorator) { 102 | if (decorator.expression.callee) { 103 | decorators[decorator.expression.callee.name] = decorator; 104 | } else { 105 | decorators[decorator.expression.name] = decorator; 106 | } 107 | }); 108 | }); 109 | }) 110 | }); 111 | 112 | for (const stmt of path.get("body")) { 113 | if (stmt.node.type === 'ImportDeclaration') { 114 | 115 | if (stmt.node.specifiers.length === 0) { 116 | continue; 117 | } 118 | 119 | for (const specifier of stmt.node.specifiers) { 120 | const binding = stmt.scope.getBinding(specifier.local.name); 121 | 122 | if (!binding.referencePaths.length) { 123 | if (decorators[specifier.local.name]) { 124 | binding.referencePaths.push({ 125 | parent: decorators[specifier.local.name] 126 | }); 127 | } 128 | } else { 129 | const allTypeRefs = binding.referencePaths.reduce((prev, next) => prev || isInType(next), false); 130 | if (allTypeRefs) { 131 | Object.keys(decorators).forEach(k => { 132 | const decorator = decorators[k]; 133 | 134 | (decorator.expression.arguments || []).forEach(arg => { 135 | if (arg.name === specifier.local.name) { 136 | binding.referencePaths.push({ 137 | parent: decorator.expression 138 | }); 139 | } 140 | }) 141 | }) 142 | } 143 | } 144 | } 145 | } 146 | } 147 | } 148 | }, 149 | Function: function (path) { 150 | let functionName = ''; 151 | 152 | if (path.node.id) { 153 | functionName = path.node.id.name; 154 | } else if (path.node.key) { 155 | functionName = path.node.key.name; 156 | } 157 | 158 | (path.get('params') || []) 159 | .slice() 160 | .forEach(function (param) { 161 | const decorators = (param.node.decorators || []); 162 | const transformable = decorators.length; 163 | 164 | decorators.slice() 165 | .forEach(function (decorator) { 166 | 167 | // For class support env 168 | if (path.type === 'ClassMethod') { 169 | const parentNode = path.parentPath.parentPath; 170 | const classDeclaration = path.findParent(p => p.type === 'ClassDeclaration'); 171 | 172 | let classIdentifier; 173 | 174 | // without class decorator 175 | if (classDeclaration) { 176 | classIdentifier = classDeclaration.node.id.name; 177 | // with class decorator 178 | } else { 179 | // Correct the temp identifier reference 180 | parentNode.insertAfter(null); 181 | classIdentifier = findIdentifierAfterAssignment(path); 182 | } 183 | 184 | if (functionName === 'constructor') { 185 | const expression = decoratorExpressionForConstructor(decorator, param)(classIdentifier); 186 | // TODO: the order of insertion 187 | parentNode.insertAfter(expression); 188 | } else { 189 | const expression = decoratorExpressionForMethod(decorator, param)(classIdentifier, functionName); 190 | // TODO: the order of insertion 191 | parentNode.insertAfter(expression); 192 | } 193 | } else { 194 | const classDeclarator = path.findParent(p => p.node.type === 'VariableDeclarator'); 195 | const className = classDeclarator.node.id.name; 196 | 197 | if (functionName === className) { 198 | const expression = decoratorExpressionForConstructor(decorator, param)(className); 199 | // TODO: the order of insertion 200 | if (path.parentKey === 'body') { 201 | path.insertAfter(expression); 202 | // In case there is only a constructor method 203 | } else { 204 | const bodyParent = path.findParent(p => p.parentKey === 'body'); 205 | bodyParent.insertAfter(expression); 206 | } 207 | } else { 208 | const classParent = path.findParent(p => p.node.type === 'CallExpression'); 209 | const expression = decoratorExpressionForMethod(decorator, param)(className, functionName); 210 | // TODO: the order of insertion 211 | classParent.insertAfter(expression); 212 | } 213 | } 214 | }); 215 | 216 | if (transformable) { 217 | const replacement = getParamReplacement(param); 218 | param.replaceWith(replacement); 219 | } 220 | }); 221 | } 222 | } 223 | } 224 | }; 225 | -------------------------------------------------------------------------------- /test/greeter.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { GreeterFactory } from "./lib/ts/GreeterFactory"; 3 | 4 | test('Should the original function work correctly.', t => { 5 | const greeter = GreeterFactory.build('Nice to meet you!'); 6 | const message = greeter.greet('Warner', '😆'); 7 | 8 | t.is(message, 'Hello Warner, Nice to meet you!'); 9 | }); 10 | 11 | test('Should greet with default greeting.', t => { 12 | const greeter = GreeterFactory.build(); 13 | const message = greeter.greet('Warner'); 14 | 15 | t.is(message, 'Hello Warner, how are you?'); 16 | }); 17 | 18 | test('Should throw required error when name not passed.', t => { 19 | const error = t.throws(() => { 20 | const greeter = GreeterFactory.build('Nice to meet you!'); 21 | const message = greeter.greet(); 22 | }, Error); 23 | 24 | t.is(error.message, 'name is required'); 25 | }); 26 | 27 | test('Should support multiple parameters, validate failed', t => { 28 | const error = t.throws(() => { 29 | const greeter = GreeterFactory.build(); 30 | const message = greeter.welcome('Hooh'); 31 | }, Error); 32 | 33 | t.is(error.message, 'lastName is required'); 34 | }); 35 | 36 | test('Should support multiple parameters, validate success', t => { 37 | const greeter = GreeterFactory.build(); 38 | const message = greeter.welcome('Hooh', 'Warner'); 39 | 40 | t.is(message, 'Welcome Warner.Hooh'); 41 | }); 42 | 43 | test('Should support destructured parameters, validate failed', t => { 44 | const error = t.throws(() => { 45 | const greeter = GreeterFactory.build(); 46 | const message = greeter.meet(); 47 | }, Error); 48 | 49 | t.is(error.message, 'guest is required'); 50 | }); 51 | 52 | test('Should support destructured parameters, validate success', t => { 53 | const greeter = GreeterFactory.build(); 54 | const message = greeter.meet({ name: 'Hooh', title: 'Mr' }); 55 | 56 | t.is(message, 'Nice to meet you Mr Hooh.'); 57 | }); 58 | 59 | test('Should count the greeting times', t => { 60 | const greeter = GreeterFactory.build(); 61 | greeter.greet('bro'); 62 | greeter.welcome('Hooh', 'Warner'); 63 | 64 | t.is(2, greeter.count()); 65 | }); 66 | 67 | test('Should talk to somebody', t => { 68 | const greeter = GreeterFactory.build(); 69 | const message = greeter.talk('Hooh'); 70 | 71 | t.is(message, 'Nice talk to you Hooh.'); 72 | }); 73 | 74 | test('Should talk to default', t => { 75 | const greeter = GreeterFactory.build(); 76 | const message = greeter.talk(); 77 | 78 | t.is(message, 'Nice talk to you friend.'); 79 | }); 80 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import Greeter from './lib/js'; 3 | 4 | test('Should the original function work correctly.', t => { 5 | const greeter = new Greeter('Nice to meet you!'); 6 | const message = greeter.greet('Warner'); 7 | 8 | t.is(message, 'Hello Warner, Nice to meet you!'); 9 | }); 10 | 11 | test('Should greet with default greeting.', t => { 12 | const greeter = new Greeter(); 13 | const message = greeter.greet('Warner'); 14 | 15 | t.is(message, 'Hello Warner, how are you?'); 16 | }); 17 | 18 | test('Should throw required error when name not passed.', t => { 19 | const error = t.throws(() => { 20 | const greeter = new Greeter('Nice to meet you!'); 21 | const message = greeter.greet(); 22 | }, Error); 23 | 24 | t.is(error.message, 'name is required'); 25 | }); 26 | 27 | test('Should support multiple parameters, validate failed', t => { 28 | const error = t.throws(() => { 29 | const greeter = new Greeter(); 30 | const message = greeter.welcome('Hooh'); 31 | }, Error); 32 | 33 | t.is(error.message, 'lastName is required'); 34 | }); 35 | 36 | test('Should support multiple parameters, validate success', t => { 37 | const greeter = new Greeter(); 38 | const message = greeter.welcome('Hooh', 'Warner'); 39 | 40 | t.is(message, 'Welcome Warner.Hooh'); 41 | }); 42 | 43 | test('Should support destructured parameters, validate failed', t => { 44 | const error = t.throws(() => { 45 | const greeter = new Greeter(); 46 | const message = greeter.meet(); 47 | }, Error); 48 | 49 | t.is(error.message, 'guest is required'); 50 | }); 51 | 52 | test('Should support destructured parameters, validate success', t => { 53 | const greeter = new Greeter(); 54 | const message = greeter.meet({ name: 'Hooh', title: 'Mr' }); 55 | 56 | t.is(message, 'Nice to meet you Mr Hooh.'); 57 | }); 58 | 59 | test('Should talk to somebody', t => { 60 | const greeter = new Greeter(); 61 | const message = greeter.talk('Hooh'); 62 | 63 | t.is(message, 'Nice talk to you Hooh.'); 64 | }); 65 | 66 | test('Should talk to default', t => { 67 | const greeter = new Greeter(); 68 | const message = greeter.talk(); 69 | 70 | t.is(message, 'Nice talk to you friend.'); 71 | }); 72 | -------------------------------------------------------------------------------- /test/src/decorators/index.js: -------------------------------------------------------------------------------- 1 | export function validate(target, property, descriptor) { 2 | const fn = descriptor.value; 3 | 4 | descriptor.value = function (...args) { 5 | const req_metadata = `meta_req_${property}`; 6 | (target[req_metadata] || []).forEach(function (metadata) { 7 | if (args[metadata.index] === undefined) { 8 | throw new Error(`${metadata.key} is required`); 9 | } 10 | }); 11 | 12 | const opt_metadata = `meta_opt_${property}`; 13 | (target[opt_metadata] || []).forEach(function (metadata) { 14 | if (args[metadata.index] === undefined) { 15 | console.warn(`The ${metadata.index + 1}(th) optional argument is missing of method ${fn.name}`); 16 | } 17 | }); 18 | 19 | return fn.apply(this, args); 20 | }; 21 | 22 | return descriptor; 23 | } 24 | 25 | export function required(key) { 26 | return function (target, propertyKey, parameterIndex) { 27 | const metadata = `meta_req_${propertyKey}`; 28 | target[metadata] = [ 29 | ...(target[metadata] || []), 30 | { 31 | index: parameterIndex, 32 | key 33 | } 34 | ] 35 | }; 36 | } 37 | 38 | export function optional(target, propertyKey, parameterIndex) { 39 | const metadata = `meta_opt_${propertyKey}`; 40 | target[metadata] = [ 41 | ...(target[metadata] || []), 42 | { 43 | index: parameterIndex, 44 | } 45 | ] 46 | } 47 | 48 | export function Inject(Clazz) { 49 | return function (target, unusedKey, parameterIndex) { 50 | const metadata = `meta_ctr_inject`; 51 | target[metadata] = target[metadata] || []; 52 | target[metadata][parameterIndex] = Clazz; 53 | 54 | return target; 55 | }; 56 | } 57 | 58 | export function Factory(target) { 59 | const metadata = `meta_ctr_inject`; 60 | 61 | return class extends target { 62 | constructor(...args) { 63 | const metaInject = target[metadata] || []; 64 | for (let i = 0; i < metaInject.length; i++) { 65 | const Clazz = metaInject[i]; 66 | if (Clazz && args[i] === null) { 67 | args[i] = Reflect.construct(Clazz, []); 68 | } 69 | } 70 | super(...args); 71 | } 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /test/src/js/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 7 | "../../../" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /test/src/js/index.js: -------------------------------------------------------------------------------- 1 | import {validate, required, optional} from '../decorators' 2 | 3 | export default class Greeter { 4 | constructor(message) { 5 | this.greeting = message; 6 | } 7 | 8 | @validate 9 | greet(@required('name') name) { 10 | const greeting = 'how are you?'; 11 | return "Hello " + name + ", " + (this.greeting || greeting); 12 | } 13 | 14 | @validate 15 | talk(@optional name = 'friend') { 16 | return "Nice talk to you " + name + "."; 17 | } 18 | 19 | @validate 20 | welcome(@required('firstName') firstName, @required('lastName') lastName) { 21 | return "Welcome " + lastName + "." + firstName; 22 | } 23 | 24 | @validate 25 | meet(@required('guest') { name: nickname, title }) { 26 | return "Nice to meet you " + title + ' ' + nickname + '.'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/src/ts/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | [ "@babel/preset-typescript", { "onlyRemoveTypeImports": true } ] 5 | ], 6 | "plugins": [ 7 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 8 | ["@babel/plugin-proposal-class-properties", { "loose" : true }], 9 | "../../../" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /test/src/ts/Greeter.ts: -------------------------------------------------------------------------------- 1 | import {validate, required, optional, Inject, Factory} from '../decorators' 2 | import Sentinel, {Counter} from './Sentinel' 3 | import { UserRepo } from './UserRepo'; 4 | 5 | @Factory 6 | export class Greeter { 7 | 8 | private counter: Counter = this.sentinel.counter; 9 | 10 | constructor( 11 | private greeting: string, 12 | @Inject(Sentinel) private sentinel: Sentinel, 13 | @Inject(UserRepo) private userRepo: UserRepo 14 | ) {} 15 | 16 | @validate 17 | greet(@required('name') name: string, @optional emoj) { 18 | this.sentinel.count(); 19 | 20 | const greeting = 'how are you?'; 21 | return "Hello " + name + ", " + (this.greeting || greeting); 22 | } 23 | 24 | @validate 25 | talk(@optional name: string = 'friend') { 26 | return "Nice talk to you " + name + "."; 27 | } 28 | 29 | @validate 30 | welcome(@required('firstName') firstName: string, @required('lastName') lastName: string) { 31 | this.sentinel.count(); 32 | 33 | return "Welcome " + lastName + "." + firstName; 34 | } 35 | 36 | @validate 37 | meet(@required('guest') { name: nickname, title }) { 38 | this.sentinel.count(); 39 | 40 | return "Nice to meet you " + title + ' ' + nickname + '.'; 41 | } 42 | 43 | count() { 44 | return this.counter.number; 45 | } 46 | } 47 | 48 | export default Greeter; 49 | 50 | function myFunctionToBeExported() {} 51 | 52 | export { 53 | myFunctionToBeExported 54 | } 55 | -------------------------------------------------------------------------------- /test/src/ts/GreeterFactory.ts: -------------------------------------------------------------------------------- 1 | import Greeter from "./Greeter"; 2 | 3 | export class GreeterFactory { 4 | static build(greeting): Greeter { 5 | return new Greeter(greeting, null, null); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/src/ts/Sentinel.ts: -------------------------------------------------------------------------------- 1 | export interface Counter { 2 | number: number 3 | } 4 | 5 | export default class Sentinel { 6 | public counter:Counter = { number: 0 }; 7 | 8 | count() { 9 | this.counter.number++; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/src/ts/UserRepo.ts: -------------------------------------------------------------------------------- 1 | export class UserRepo {} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2016", 4 | "experimentalDecorators": true, 5 | "moduleResolution": "node" 6 | } 7 | } 8 | --------------------------------------------------------------------------------