Skip to content

Commit

Permalink
fix: optional access to Reflect.metadata (#165)
Browse files Browse the repository at this point in the history
Co-authored-by: Pooya Parsa <pooya@pi0.io>
  • Loading branch information
sirenkovladd and pi0 authored Sep 25, 2024
1 parent eb06426 commit 87df308
Show file tree
Hide file tree
Showing 8 changed files with 554 additions and 22 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@
"@vitest/coverage-v8": "^2.1.1",
"acorn": "^8.12.1",
"babel-plugin-parameter-decorator": "^1.0.16",
"babel-plugin-transform-typescript-metadata": "^0.3.2",
"changelogen": "^0.5.7",
"config": "^3.3.12",
"destr": "^2.0.3",
Expand Down
19 changes: 0 additions & 19 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/babel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import syntaxImportAssertionsPlugin from "@babel/plugin-syntax-import-assertions
import transformExportNamespaceFromPlugin from "@babel/plugin-transform-export-namespace-from";
import transformTypeScriptPlugin from "@babel/plugin-transform-typescript";
import parameterDecoratorPlugin from "babel-plugin-parameter-decorator";
import transformTypeScriptMetaPlugin from "babel-plugin-transform-typescript-metadata";
import transformTypeScriptMetaPlugin from "./plugins/babel-plugin-transform-typescript-metadata";
import syntaxJSXPlugin from "@babel/plugin-syntax-jsx";
import transformReactJSX from "@babel/plugin-transform-react-jsx";
import { TransformImportMetaPlugin } from "./plugins/babel-plugin-transform-import-meta";
Expand Down
50 changes: 50 additions & 0 deletions src/plugins/babel-plugin-transform-typescript-metadata/index.ts
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();
},
});
},
},
};
});
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;
}
}
}
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;
}
}
}
Loading

0 comments on commit 87df308

Please sign in to comment.