Skip to content

Commit

Permalink
feat: adding modifiers metadata
Browse files Browse the repository at this point in the history
Modifiers metadata will allow you to have access to
class, methods and properties detailed informations,
giving the opportunity to create more complex decorating
automations
  • Loading branch information
Farenheith committed Sep 24, 2024
1 parent 521d21d commit 3c054a2
Show file tree
Hide file tree
Showing 12 changed files with 447 additions and 0 deletions.
2 changes: 2 additions & 0 deletions plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module.exports = require('./dist/plugin');
module.exports.default = module.exports;
1 change: 1 addition & 0 deletions src/internal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './meta-storage';
13 changes: 13 additions & 0 deletions src/internal/meta-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ClassMetadata } from '../meta-type';

let blocked = false;
const metadata = new Map<object, ClassMetadata>();

export function getMetadataStorage() {
if (!blocked) return metadata;
throw new Error('Invalid Operation');
}

export function blockAccess() {
blocked = true;
}
42 changes: 42 additions & 0 deletions src/meta-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ClassType = abstract new (...args: any[]) => unknown;
export type Key = string | symbol;

export interface ModifiersMetadata {
public: boolean;
readonly: boolean;
static: boolean;
abstract: boolean;
accessor: boolean;
async: boolean;
const: boolean;
override: boolean;
private: boolean;
protected: boolean;
exported: boolean;
}

export interface BaseMetadata {
modifiers: ModifiersMetadata;
}

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

export interface ClassMetadata {
ctor: ConstructorMetadata;
properties: Map<Key, PropertyMetadata>;
methods: Map<Key, MethodMetadata>;
}
64 changes: 64 additions & 0 deletions src/plugin/decorators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { getMetadataStorage } from '../internal';
import {
ClassType,
ConstructorMetadata,
Key,
ModifiersMetadata,
} from '../meta-type';

const metadata = getMetadataStorage();

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(modifiers: ModifiersMetadata) {
return (cls: ClassType) => {
const { prototype } = cls;
const ref = getMeta(prototype);
ref.ctor = {
cls,
args: Reflect.getMetadata('design:paramtypes', cls),
modifiers,
};
};
}

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

export function registerMethodMetadata(modifiers: ModifiersMetadata) {
return (
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,
modifiers,
});
};
}
192 changes: 192 additions & 0 deletions src/plugin/emitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/* eslint-disable no-bitwise */
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as ts from 'typescript';
import { moduleExists } from './module-exists';
import { tsBinary } from './ts-loader';
import { Key, ModifiersMetadata } from '../meta-type';

function isStatic(
node: ts.MethodDeclaration | ts.ClassDeclaration | ts.PropertyDeclaration,
) {
return node.modifiers?.find((x) => x.kind === ts.SyntaxKind.StaticKeyword);
}

type TypedNode = Pick<ts.ParameterDeclaration, 'type'>;

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

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* emitPropertyAssignments(obj: Record<Key, boolean>) {
for (const k in obj) {
if (k in obj) {
yield tsBinary.factory.createPropertyAssignment(
k,
obj[k] ? tsBinary.factory.createTrue() : tsBinary.factory.createFalse(),
);
}
}
}

function getProperties(node: ts.HasModifiers): ts.ObjectLiteralElementLike[] {
const modifiers = new Set<ts.Modifier['kind']>();
for (const modifier of tsBinary.getModifiers(node) ?? []) {
modifiers.add(modifier.kind);
}
const access = {
private: modifiers.has(ts.SyntaxKind.PrivateKeyword),
protected: modifiers.has(ts.SyntaxKind.ProtectedKeyword),
};
const properties: ModifiersMetadata = {
exported: modifiers.has(ts.SyntaxKind.ExportKeyword),
...access,
public: !access.private && !access.protected,
readonly: modifiers.has(ts.SyntaxKind.ReadonlyKeyword),
static: modifiers.has(ts.SyntaxKind.StaticKeyword),
abstract: modifiers.has(ts.SyntaxKind.AbstractKeyword),
accessor: modifiers.has(ts.SyntaxKind.AccessorKeyword),
async: modifiers.has(ts.SyntaxKind.AsyncKeyword),
const: modifiers.has(ts.SyntaxKind.ConstKeyword),
override: modifiers.has(ts.SyntaxKind.OverrideKeyword),
};
return [
...emitPropertyAssignments(properties as unknown as Record<Key, boolean>),
];
}

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> => {
return (sf: ts.SourceFile) => {
const imports = new Map();
const mustImport = new Set();
const visitNode = (node: ts.Node) => {
try {
if (tsBinary.isImportClause(node)) {
const { namedBindings } = node;
if (namedBindings) {
for (const item of ts.isNamedImports(namedBindings)
? namedBindings.elements
: []) {
imports.set(item.name.getText(), node);
}
}
} else if (
(tsBinary.isMethodDeclaration(node) ||
tsBinary.isPropertyDeclaration(node) ||
tsBinary.isClassDeclaration(node)) &&
!tsBinary.getDecorators(node)?.length &&
!isStatic(node)
) {
let identifier: string;
const modifiers = getProperties(node);
if (!tsBinary.isClassDeclaration(node)) {
if (node.type) addRef(node, imports, mustImport);
if (tsBinary.isMethodDeclaration(node)) {
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 requireCall = tsBinary.factory.createCallExpression(
tsBinary.factory.createIdentifier('require'),
undefined,
[
tsBinary.factory.createStringLiteral(
'nestjs-auto-reflect-metadata-emitter/plugin',
),
],
);
const decoratorAccess = tsBinary.factory.createPropertyAccessChain(
requireCall,
undefined,
tsBinary.factory.createIdentifier(identifier),
);
const decoratorCall = tsBinary.factory.createCallExpression(
decoratorAccess,
undefined,
[tsBinary.factory.createObjectLiteralExpression(modifiers)],
);
const decorator = tsBinary.factory.createDecorator(decoratorCall);
node = tsBinary.factory.replaceDecoratorsAndModifiers(node, [
...(node.modifiers ?? []),
decorator,
]);
return tsBinary.isClassDeclaration(node)
? tsBinary.visitEachChild(node, visitNode, ctx)
: node;
}
return tsBinary.visitEachChild(node, visitNode, ctx);
} catch (err) {
console.log(err.stack);
return node;
}
};
const processImports = (node: ts.Node) => {
try {
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,
// typescript will be forced to emit its import during transpiling.
// Maybe there's a cleaner way to do it.
for (const item of ts.isNamedImports(namedBindings)
? namedBindings.elements
: []) {
(item as any).symbol.flags |= 33554432 /* Transient */;
(item as any).symbol.links = { referenced: true };
break;
}
}
return tsBinary.factory.updateImportClause(
node,
false,
node.name,
node.namedBindings,
);
}
return tsBinary.visitEachChild(node, processImports, ctx);
} catch (err) {
console.log(err.stack);
return node;
}
};
const nodeResult = tsBinary.visitNode(sf, visitNode);
return tsBinary.visitNode(nodeResult, processImports);
};
};
}

export function simpleDecorator() {
//
}
5 changes: 5 additions & 0 deletions src/plugin/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/plugin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './decorators';
export * from './emitter';
export * from './factory';
25 changes: 25 additions & 0 deletions src/plugin/module-exists.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { existsSync } from 'fs';
import { dirname } from 'path';
import { getModuleRealPath } from './ts-loader';
import ts from 'typescript';

export function moduleExists(sf: ts.SourceFile, moduleName: string) {
try {
let tsConfigTreatedModuleName = getModuleRealPath(moduleName);
if (!tsConfigTreatedModuleName && moduleName.startsWith('.')) {
tsConfigTreatedModuleName = `${dirname(sf.fileName)}/${moduleName}`;
}
if (
tsConfigTreatedModuleName &&
(existsSync(tsConfigTreatedModuleName) ||
existsSync(`${tsConfigTreatedModuleName}.ts`) ||
existsSync(`${tsConfigTreatedModuleName}.js`))
) {
return true;
}
require(tsConfigTreatedModuleName ?? moduleName);
return true;
} catch {
return false;
}
}
42 changes: 42 additions & 0 deletions src/plugin/ts-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { TsConfigProvider } from './tsconfig-provider';
import { TypeScriptBinaryLoader } from './typescript-loader';
import tsPaths = require('tsconfig-paths');
import os from 'os';
import { posix } from 'path';

export const tsLoader = new TypeScriptBinaryLoader();
export const tsBinary = tsLoader.load();
export const tsConfigProvider = new TsConfigProvider(tsLoader);
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) {
let result = matcher(text, undefined, undefined, [
'.ts',
'.tsx',
'.js',
'.jsx',
]);
if (!result) {
return;
}
if (os.platform() === 'win32') {
result = result.replace(/\\/g, '/');
}
try {
// Installed packages (node modules) should take precedence over root files with the same name.
// Ref: https://github.com/nestjs/nest-cli/issues/838
const packagePath = require.resolve(text, {
paths: [process.cwd(), ...module.paths],
});
if (packagePath) {
return text;
}
} catch {
// ignore
}

const resolvedPath = posix.relative(process.cwd(), result) || './';
return resolvedPath[0] === '.' ? resolvedPath : './' + resolvedPath;
}
Loading

0 comments on commit 3c054a2

Please sign in to comment.