├── .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)
4 | [](https://badge.fury.io/js/babel-plugin-parameter-decorator)
5 | [](https://www.npmjs.com/package/babel-plugin-parameter-decorator)
6 | [](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 |
--------------------------------------------------------------------------------