-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
521d21d
commit 3c054a2
Showing
12 changed files
with
447 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
module.exports = require('./dist/plugin'); | ||
module.exports.default = module.exports; |
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 @@ | ||
export * from './meta-storage'; |
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,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; | ||
} |
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,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>; | ||
} |
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,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, | ||
}); | ||
}; | ||
} |
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,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() { | ||
// | ||
} |
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,5 @@ | ||
import { before } from './emitter'; | ||
|
||
export const name = 'nestjs-auto-reflect-metadata-emitter'; | ||
export const version = 1; | ||
export const factory = before; |
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,3 @@ | ||
export * from './decorators'; | ||
export * from './emitter'; | ||
export * from './factory'; |
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,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; | ||
} | ||
} |
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,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; | ||
} |
Oops, something went wrong.