Skip to content

Commit

Permalink
fix: adding access api
Browse files Browse the repository at this point in the history
The metadata was being registered, but not easily
accessible. Some functions were introducuted to
make it possible. Also, a jest friendly export had
been added to make it easier to integrate with it
  • Loading branch information
Farenheith committed Sep 24, 2024
1 parent d483571 commit eff2f49
Show file tree
Hide file tree
Showing 11 changed files with 213 additions and 34 deletions.
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,34 @@ Now, to use this plugin, do the following steps:
* Use, at least, **typescript 5.3.3**.
* Compile your application using nest build, and test it using nest start

After compiling your project, you can check that all the compiled classes are correctly annotated, but probably still not working in most cases. Imported classes, for example, are not being correctly imported if they're only imported for typing (while decorating manually your class, it'll work). That's where I'm stuck in. I'll update the project as soon as I discover how to solve it.
## How to access metadata

Metadata of all classes is accessible through the method **getClassMetadata**, where you just need to inform the class which you want the metadata of.
You can also iterate over all metadata registered through **iterateMetadata**.
Finally, metadata may be a sensitive data of your application, so, you can erase all its information using **clearAllMetadata**. We recommend you to do so, if you use this library, don't keep any hard logic depending on what this package will register, just construct whenever you need and clear it all at the end.

## What we're not doing yet.

* We're not generating metadata of get and set accessors;
* We're not discriminating between private and public properties;

Those are points of evolution of this library and we'll address them as soon as possible. If you have any suggestions or contributions to do, feel free to contact us!

## How to use it with Jest?

You can set the transformer of this library to run with jest following the example below:

```json
"transform": {
"^.+\\.(t|j)s$": [
"ts-jest",
{
"astTransformers": {
"before": ["node_modules/nestjs-auto-reflect-metadata-emitter"]
}
}
]
}
```

This will be enough to make it apply it during transpilation.
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module.exports = require('./dist');
module.exports.default = module.exports;
9 changes: 9 additions & 0 deletions package-lock.json

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

21 changes: 19 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"name": "nestjs-auto-reflect-metadata-emitter",
"version": "1.0.14",
"main": "dist/emitter.js",
"main": "dist/index.js",
"files": [
"dist"
"dist",
"index.js"
],
"scripts": {
"lint": "npm run lint:format && npm run lint:style",
Expand All @@ -24,14 +25,30 @@
"eslint": "^8.0.0",
"eslint-plugin-unused-imports": "^4.1.4",
"prettier": "^3.3.3",
"reflect-metadata": "^0.1.0",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.3.3"
},
"peerDependencies": {
"@nestjs/cli": "*",
"reflect-metadata": "*",
"tsconfig-paths": "*",
"typescript": "^5.3.3"
},
"contributors": [
"Thiago O Santos <tos.oliveira@gmail.com>"
],
"engines": {
"node": ">=20"
},
"repository": {
"type": "git",
"url": "git+https://github.com/codibre/nestjs-auto-reflect-metadata-emitter"
},
"homepage": "https://codibre.github.io/nestjs-auto-reflect-metadata-emitter",
"bugs": {
"url": "https://github.com/codibre/nestjs-auto-reflect-metadata-emitter/issues"
},
"keywords": [
"typescript",
"@nestjs/cli",
Expand Down
59 changes: 39 additions & 20 deletions src/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,25 @@ type TypedNode = Pick<ts.ParameterDeclaration, 'type'>;

type NodeWithParameters = Pick<ts.MethodDeclaration, 'parameters'>;

function addRef(p: TypedNode, imports: Map<any, any>, mustImport: Set<unknown>) {
function addRef(
p: TypedNode,
imports: Map<any, any>,
mustImport: Set<unknown>,
) {
if (p.type) {
const ref = imports.get(p.type.getText());
if (ref) mustImport.add(ref);
}
}

function addParameterRefs(node: NodeWithParameters, imports: Map<any, any>, mustImport: Set<unknown>) {
for (const p of node.parameters) {
addRef(p, imports, mustImport);
}
function addParameterRefs(
node: NodeWithParameters,
imports: Map<any, any>,
mustImport: Set<unknown>,
) {
for (const p of node.parameters) {
addRef(p, imports, mustImport);
}
}
export function before() {
return (ctx: ts.TransformationContext): ts.Transformer<any> => {
Expand All @@ -49,32 +57,39 @@ export function before() {
!tsBinary.getDecorators(node)?.length &&
!isStatic(node)
) {
const decorator = tsBinary.factory.createDecorator(
tsBinary.factory.createPropertyAccessChain(
tsBinary.factory.createCallExpression(
tsBinary.factory.createIdentifier('require'),
undefined,
[
tsBinary.factory.createStringLiteral('assert'),
]
),
undefined,
tsBinary.factory.createIdentifier('ok')
),
);
let identifier: string;
if (!tsBinary.isClassDeclaration(node)) {
if (node.type) addRef(node, imports, mustImport);
if (tsBinary.isMethodDeclaration(node)) {
addParameterRefs(node, imports, mustImport);
addParameterRefs(node, imports, mustImport);
identifier = 'registerMethodMetadata';
} else {
identifier = 'registerPropertyMetadata';
}
} else {
identifier = 'registerClassMetadata';
for (const member of node.members) {
if (tsBinary.isConstructorDeclaration(member)) {
addParameterRefs(member, imports, mustImport);
break;
}
}
}
const decorator = tsBinary.factory.createDecorator(
tsBinary.factory.createPropertyAccessChain(
tsBinary.factory.createCallExpression(
tsBinary.factory.createIdentifier('require'),
undefined,
[
tsBinary.factory.createStringLiteral(
'nestjs-auto-reflect-metadata-emitter',
),
],
),
undefined,
tsBinary.factory.createIdentifier(identifier),
),
);
node = tsBinary.factory.replaceDecoratorsAndModifiers(node, [
...(node.modifiers ?? []),
decorator,
Expand All @@ -91,7 +106,11 @@ export function before() {
};
const processImports = (node: ts.Node) => {
try {
if (tsBinary.isImportClause(node) && mustImport.has(node) && moduleExists(sf, (node.parent.moduleSpecifier as any).text)) {
if (
tsBinary.isImportClause(node) &&
mustImport.has(node) &&
moduleExists(sf, (node.parent.moduleSpecifier as any).text)
) {
const { namedBindings } = node;
if (namedBindings) {
// Hack: if a import is flagged as transient and has links.referenced = true,
Expand Down
5 changes: 5 additions & 0 deletions src/factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { before } from './emitter';

export const name = 'nestjs-auto-reflect-metadata-emitter';
export const version = 1;
export const factory = before;
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './emitter';
export * from './factory';
export * from './meta-info';
96 changes: 96 additions & 0 deletions src/meta-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import 'reflect-metadata';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ClassType = abstract new (...args: any[]) => unknown;
export type Key = string | symbol;

export interface ConstructorMetadata {
cls: ClassType;
args: unknown[];
}
export interface MethodMetadata {
name: Key;
args: unknown[];
returnType: unknown;
propertyDescriptor: PropertyDescriptor;
}
export interface PropertyMetadata {
name: Key;
type: unknown;
}

interface ClassMetadata {
ctor: ConstructorMetadata;
properties: Map<Key, PropertyMetadata>;
methods: Map<Key, MethodMetadata>;
}
const metadata = new Map<object, ClassMetadata>();

function getMeta(prototype: object) {
let ref = metadata.get(prototype);
if (!ref) {
ref = {
ctor: undefined as unknown as ConstructorMetadata,
properties: new Map(),
methods: new Map(),
};
metadata.set(prototype, ref);
}
return ref;
}

export function registerClassMetadata(cls: ClassType) {
const { prototype } = cls;
const ref = getMeta(prototype);
ref.ctor = {
cls,
args: Reflect.getMetadata('design:paramtypes', cls),
};
}

export function registerPropertyMetadata(prototype: object, key: Key) {
const ref = getMeta(prototype);
const type = Reflect.getMetadata('design:type', prototype, key);
ref.properties.set(key, {
name: key,
type,
});
}

export function registerMethodMetadata(
prototype: object,
key: Key,
propertyDescriptor: PropertyDescriptor,
) {
const ref = getMeta(prototype);
const returnType = Reflect.getMetadata('design:returntype', prototype, key);
ref.methods.set(key, {
name: key,
args: Reflect.getMetadata('design:paramtypes', prototype, key),
returnType,
propertyDescriptor,
});
}

/**
* Return metadata of the class informed, or undefined if there is none
* @param cls The Class to get metadata from
*/
export function getClassMetadata(cls: ClassType) {
return metadata.get(cls.prototype);
}

/**
* Returns an iterable that allows you to iterate over all the metadata
* collected. You must filter it as you need
*/
export function iterateMetadata() {
return metadata.values();
}

/**
* Clear all the metadata collected. We recommend you to call this function
* at the and of the metadata consumption
*/
export function clearAllMetadata() {
metadata.clear();
}
13 changes: 8 additions & 5 deletions src/module-exists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ export function moduleExists(sf: ts.SourceFile, moduleName: string) {
if (!tsConfigTreatedModuleName && moduleName.startsWith('.')) {
tsConfigTreatedModuleName = `${dirname(sf.fileName)}/${moduleName}`;
}
if (tsConfigTreatedModuleName
&& (existsSync(tsConfigTreatedModuleName)
|| existsSync(`${tsConfigTreatedModuleName}.ts`)
|| existsSync(`${tsConfigTreatedModuleName}.js`))
) return true;
if (
tsConfigTreatedModuleName &&
(existsSync(tsConfigTreatedModuleName) ||
existsSync(`${tsConfigTreatedModuleName}.ts`) ||
existsSync(`${tsConfigTreatedModuleName}.js`))
) {
return true;
}
require(tsConfigTreatedModuleName ?? moduleName);
return true;
} catch {
Expand Down
3 changes: 0 additions & 3 deletions src/simple-decorator.ts

This file was deleted.

4 changes: 1 addition & 3 deletions src/ts-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ export const tsConfig = tsConfigProvider.getByConfigFilename('tsconfig.json');
const { paths = {}, baseUrl = './' } = tsConfig.options;
export const matcher = tsPaths.createMatchPath(baseUrl, paths, ['main']);

export function getModuleRealPath(
text: string,
) {
export function getModuleRealPath(text: string) {
let result = matcher(text, undefined, undefined, [
'.ts',
'.tsx',
Expand Down

0 comments on commit eff2f49

Please sign in to comment.