-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: optional access to
Reflect.metadata
(#165)
Co-authored-by: Pooya Parsa <pooya@pi0.io>
- Loading branch information
1 parent
eb06426
commit 87df308
Showing
8 changed files
with
554 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
src/plugins/babel-plugin-transform-typescript-metadata/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/** | ||
* Based on https://github.com/leonardfactory/babel-plugin-transform-typescript-metadata | ||
* Copyright (c) 2019 Leonardo Ascione [MIT] | ||
*/ | ||
|
||
import type { PluginObj } from "@babel/core"; | ||
import { declare } from "@babel/helper-plugin-utils"; | ||
import { parameterVisitor } from "./parameter-visitor"; | ||
import { metadataVisitor } from "./metadata-visitor"; | ||
|
||
export default declare((api: any): PluginObj => { | ||
api.assertVersion(7); | ||
|
||
return { | ||
visitor: { | ||
Program(programPath) { | ||
/** | ||
* We need to traverse the program right here since | ||
* `@babel/preset-typescript` removes imports at this level. | ||
* | ||
* Since we need to convert some typings into **bindings**, used in | ||
* `Reflect.metadata` calls, we need to process them **before** | ||
* the typescript preset. | ||
*/ | ||
programPath.traverse({ | ||
ClassDeclaration(path) { | ||
for (const field of path.get("body").get("body")) { | ||
if ( | ||
field.type !== "ClassMethod" && | ||
field.type !== "ClassProperty" | ||
) { | ||
continue; | ||
} | ||
|
||
parameterVisitor(path, field as any); | ||
metadataVisitor(path, field as any); | ||
} | ||
|
||
/** | ||
* We need to keep binding in order to let babel know where imports | ||
* are used as a Value (and not just as a type), so that | ||
* `babel-transform-typescript` do not strip the import. | ||
*/ | ||
(path.parentPath.scope as any).crawl(); | ||
}, | ||
}); | ||
}, | ||
}, | ||
}; | ||
}); |
84 changes: 84 additions & 0 deletions
84
src/plugins/babel-plugin-transform-typescript-metadata/metadata-visitor.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
/** | ||
* Based on https://github.com/leonardfactory/babel-plugin-transform-typescript-metadata | ||
* Copyright (c) 2019 Leonardo Ascione [MIT] | ||
*/ | ||
|
||
import { NodePath, types as t } from "@babel/core"; | ||
import { serializeType } from "./serialize-type"; | ||
|
||
function createMetadataDesignDecorator( | ||
design: | ||
| "design:type" | ||
| "design:paramtypes" | ||
| "design:returntype" | ||
| "design:typeinfo", | ||
typeArg: t.Expression | t.SpreadElement | t.JSXNamespacedName, | ||
): t.Decorator { | ||
return t.decorator( | ||
t.logicalExpression( | ||
"||", | ||
t.optionalCallExpression( | ||
t.memberExpression(t.identifier("Reflect"), t.identifier("metadata")), | ||
[t.stringLiteral(design), typeArg as unknown as t.Expression], | ||
true, | ||
), | ||
t.arrowFunctionExpression([t.identifier("t")], t.identifier("t")), | ||
), | ||
); | ||
} | ||
|
||
export function metadataVisitor( | ||
classPath: NodePath<t.ClassDeclaration>, | ||
path: NodePath<t.ClassProperty | t.ClassMethod>, | ||
) { | ||
const field = path.node; | ||
const classNode = classPath.node; | ||
|
||
switch (field.type) { | ||
case "ClassMethod": { | ||
const decorators = | ||
field.kind === "constructor" ? classNode.decorators : field.decorators; | ||
|
||
if (!decorators || decorators.length === 0) { | ||
return; | ||
} | ||
|
||
decorators!.push( | ||
createMetadataDesignDecorator("design:type", t.identifier("Function")), | ||
); | ||
decorators!.push( | ||
createMetadataDesignDecorator( | ||
"design:paramtypes", | ||
t.arrayExpression( | ||
field.params.map((param) => serializeType(classPath, param)), | ||
), | ||
), | ||
); | ||
// Hint: `design:returntype` could also be implemented here, although this seems | ||
// quite complicated to achieve without the TypeScript compiler. | ||
// See https://github.com/microsoft/TypeScript/blob/f807b57356a8c7e476fedc11ad98c9b02a9a0e81/src/compiler/transformers/ts.ts#L1315 | ||
break; | ||
} | ||
|
||
case "ClassProperty": { | ||
if (!field.decorators || field.decorators.length === 0) { | ||
return; | ||
} | ||
|
||
if ( | ||
!field.typeAnnotation || | ||
field.typeAnnotation.type !== "TSTypeAnnotation" | ||
) { | ||
return; | ||
} | ||
|
||
field.decorators!.push( | ||
createMetadataDesignDecorator( | ||
"design:type", | ||
serializeType(classPath, field), | ||
), | ||
); | ||
break; | ||
} | ||
} | ||
} |
108 changes: 108 additions & 0 deletions
108
src/plugins/babel-plugin-transform-typescript-metadata/parameter-visitor.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
/** | ||
* Based on https://github.com/leonardfactory/babel-plugin-transform-typescript-metadata | ||
* Copyright (c) 2019 Leonardo Ascione [MIT] | ||
*/ | ||
|
||
import { NodePath, types as t } from "@babel/core"; | ||
|
||
/** | ||
* Helper function to create a field/class decorator from a parameter decorator. | ||
* Field/class decorators get three arguments: the class, the name of the method | ||
* (or 'undefined' in the case of the constructor) and the position index of the | ||
* parameter in the argument list. | ||
* Some of this information, the index, is only available at transform time, and | ||
* has to be stored. The other arguments are part of the decorator signature and | ||
* will be passed to the decorator anyway. But the decorator has to be called | ||
* with all three arguments at runtime, so this creates a function wrapper, which | ||
* takes the target and the key, and adds the index to it. | ||
* | ||
* Inject() becomes function(target, key) { return Inject()(target, key, 0) } | ||
* | ||
* @param paramIndex the index of the parameter inside the function call | ||
* @param decoratorExpression the decorator expression, the return object of SomeParameterDecorator() | ||
* @param isConstructor indicates if the key should be set to 'undefined' | ||
*/ | ||
function createParamDecorator( | ||
paramIndex: number, | ||
decoratorExpression: t.Expression, | ||
isConstructor = false, | ||
) { | ||
return t.decorator( | ||
t.functionExpression( | ||
null, // anonymous function | ||
[t.identifier("target"), t.identifier("key")], | ||
t.blockStatement([ | ||
t.returnStatement( | ||
t.callExpression(decoratorExpression, [ | ||
t.identifier("target"), | ||
t.identifier(isConstructor ? "undefined" : "key"), | ||
t.numericLiteral(paramIndex), | ||
]), | ||
), | ||
]), | ||
), | ||
); | ||
} | ||
|
||
export function parameterVisitor( | ||
classPath: NodePath<t.ClassDeclaration>, | ||
path: NodePath<t.ClassMethod> | NodePath<t.ClassProperty>, | ||
) { | ||
if (path.type !== "ClassMethod") { | ||
return; | ||
} | ||
if (path.node.type !== "ClassMethod") { | ||
return; | ||
} | ||
if (path.node.key.type !== "Identifier") { | ||
return; | ||
} | ||
|
||
const methodPath = path as NodePath<t.ClassMethod>; | ||
const params = methodPath.get("params") || []; | ||
|
||
for (const param of params) { | ||
const identifier = | ||
param.node.type === "Identifier" || param.node.type === "ObjectPattern" | ||
? param.node | ||
: // eslint-disable-next-line unicorn/no-nested-ternary | ||
param.node.type === "TSParameterProperty" && | ||
param.node.parameter.type === "Identifier" | ||
? param.node.parameter | ||
: null; | ||
|
||
if (identifier == null) { | ||
continue; | ||
} | ||
|
||
let resultantDecorator: t.Decorator | undefined; | ||
|
||
for (const decorator of (param.node as t.Identifier).decorators || []) { | ||
if (methodPath.node.kind === "constructor") { | ||
resultantDecorator = createParamDecorator( | ||
param.key as number, | ||
decorator.expression, | ||
true, | ||
); | ||
if (!classPath.node.decorators) { | ||
classPath.node.decorators = []; | ||
} | ||
classPath.node.decorators.push(resultantDecorator); | ||
} else { | ||
resultantDecorator = createParamDecorator( | ||
param.key as number, | ||
decorator.expression, | ||
false, | ||
); | ||
if (!methodPath.node.decorators) { | ||
methodPath.node.decorators = []; | ||
} | ||
methodPath.node.decorators.push(resultantDecorator); | ||
} | ||
} | ||
|
||
if (resultantDecorator) { | ||
(param.node as t.Identifier).decorators = null; | ||
} | ||
} | ||
} |
Oops, something went wrong.