Skip to content

Commit

Permalink
docs: introduced tsdoc eslint plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
L2jLiga committed Jul 29, 2024
1 parent 5403d1f commit eb16859
Show file tree
Hide file tree
Showing 15 changed files with 185 additions and 73 deletions.
28 changes: 24 additions & 4 deletions lib/bootstrap/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ import { lstatSync, PathLike } from 'node:fs';
import { opendir } from 'node:fs/promises';
import type { AutoLoadConfig } from '../interfaces/bootstrap-config.js';
import type { BootstrapConfig } from '../interfaces/index.js';
import { CLASS_LOADER, ClassLoader, Constructable, hooksRegistry } from '../plugins/index.js';
import { CLASS_LOADER, ClassLoader, Constructable } from '../plugins/index.js';
import { CREATOR } from '../symbols/index.js';
import { transformAndWait } from '../utils/transform-and-wait.js';
import { isValidRegistrable } from '../utils/validators.js';
import { hooksRegistry } from '../registry/hooks-registry.js';

const defaultMask = /\.(handler|controller)\./;

/**
* Fastify plugin responsible for bootstraping
* fastify-decorators.
*/
export const bootstrap = fp<BootstrapConfig>(
async (fastifyInstance: FastifyInstance, config: BootstrapConfig): Promise<void> => {
// 1. Load all modules
Expand Down Expand Up @@ -44,15 +49,30 @@ export const bootstrap = fp<BootstrapConfig>(
},
);

/**
* Automatically loads modules from filesystem
*/
function autoLoadModules(config: AutoLoadConfig): AsyncIterable<Constructable<unknown>> {
const flags = config.mask instanceof RegExp ? config.mask.flags.replace('g', '') : '';
const mask = config.mask ? new RegExp(config.mask, flags) : defaultMask;

return readModulesRecursively(parsePath(config.directory), mask);
return readModulesRecursively(getBaseDirOf(config.directory), mask);
}

function parsePath(directory: PathLike): URL {
const urlLike = directory.toString('utf8');
/**
* Function accepts anything path-like and transforms
* it to URL object linking to the base directory.
*
* @example
* ```typescript
* parsePath(import.meta.url) // returns URL to directory containing file from which function was called
* parsePath(__filename) // same as above
* parsePath(__dirname) // converts dirname into URL
* parsePath(process.cwd) // converts process working directory into URL
* ```
*/
function getBaseDirOf(pathLike: PathLike): URL {
const urlLike = pathLike.toString('utf8');
const url = urlLike.startsWith('file://') ? new URL(urlLike) : new URL('file://' + urlLike);

if (lstatSync(url).isFile()) url.pathname += './..';
Expand Down
2 changes: 1 addition & 1 deletion lib/decorators/helpers/class-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IErrorHandler, IHandler, IHook } from '../../interfaces/index.js';
import { ERROR_HANDLERS, HANDLERS, HOOKS, METADATA } from '../../symbols/index.js';
import { Container } from './container.js';
import { Container } from '../../registry/container.js';

// TODO: Support for ES Decorators
export function getHandlersContainer<T extends object>(target: T): Container<IHandler> {
Expand Down
25 changes: 0 additions & 25 deletions lib/decorators/helpers/container.ts

This file was deleted.

3 changes: 2 additions & 1 deletion lib/decorators/helpers/request-decorators.factory.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { FastifyInstance, FastifyRequest, RouteShorthandOptions } from 'fastify';
import { HttpMethods, RequestHandler, RouteConfig } from '../../interfaces/index.js';
import { Constructable, getErrorHandlerContainer, getHandlersContainer, getHooksContainer, hooksRegistry, Registrable } from '../../plugins/index.js';
import { Constructable, getErrorHandlerContainer, getHandlersContainer, getHooksContainer, Registrable } from '../../plugins/index.js';
import { CREATOR } from '../../symbols/index.js';
import { transformAndWait } from '../../utils/transform-and-wait.js';
import { getHandlerContainerMetadata } from './class-metadata.js';
import { createErrorsHandler } from './create-errors-handler.js';
import { ensureRegistrable } from './ensure-registrable.js';
import { hooksRegistry } from '../../registry/hooks-registry.js';

type ParsedRouteConfig = { url: string; options: RouteShorthandOptions };

Expand Down
3 changes: 2 additions & 1 deletion lib/decorators/strategies/controller-type.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { FastifyInstance, FastifyReply, FastifyRequest, FastifySchema } from 'fastify';
import { onRequestHookHandler } from 'fastify/types/hooks.js';
import { CLASS_LOADER, ClassLoader, hooksRegistry, Registrable } from '../../plugins/index.js';
import { CLASS_LOADER, ClassLoader, Registrable } from '../../plugins/index.js';
import { ControllerType } from '../../registry/controller-type.js';
import { transformAndWait } from '../../utils/transform-and-wait.js';
import { getErrorHandlerContainer, getHandlersContainer, getHooksContainer } from '../helpers/class-metadata.js';
import { injectTagsIntoSwagger, TagObject } from '../helpers/swagger-helper.js';
import { hooksRegistry } from '../../registry/hooks-registry.js';

const controllersCache = new WeakMap<FastifyRequest, unknown>();

Expand Down
11 changes: 11 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
/**
* Fastify-decorators is a library-wrapper
* around the Fastify, it providers set of
* decorators.
*
* Decorators can be used to declare controllers,
* their requests handlers and life-cycle hooks.
*
* @packageDocumentation
*/

export { bootstrap } from './bootstrap/bootstrap.js';
export { BootstrapConfig } from './interfaces/bootstrap-config.js';
export { RequestHandler } from './interfaces/request-handler.js';
Expand Down
22 changes: 22 additions & 0 deletions lib/plugins/class-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,30 @@ declare module 'fastify' {
}
}

/**
* Symbol for getting class-loader from
* FastifyInstance.
*/
export const CLASS_LOADER = Symbol.for('fastify-decorators.class-loader');

/**
* Dependency scope, used to determine in
* which context class isntance was requested.
* By default all class instances are requested
* in FastifyInstance context and thus they
* are singletons, with one exception - when using
* per-request controller strategy.
*/
export type Scope = FastifyInstance | FastifyRequest;

/**
* ClassLoader is the function which fastify-decorators
* uses under the hood to get class instance for given
* scope.
* Currently only 2 scopes are supported:
* - FastifyInstance - classes instantiated in this
* scope are singletons
* - FastifyRequest - classes instantiated in this
* scope destroyed when request finished
*/
export type ClassLoader = <C>(constructor: Constructable<C>, scope: Scope) => C;
12 changes: 11 additions & 1 deletion lib/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
/**
* Entrypoint for plugin development and integration.
* Provides utilities to interact with library
* life-cycle events, can be used in order to
* customize controller creation, app startup
* and teardown behavior.
*
* @packageDocumentation
*/

export * from './class-loader.js';
export * from './life-cycle.js';
export { Constructable, Registrable } from './shared-interfaces.js';
Expand All @@ -6,5 +16,5 @@ export { getHandlersContainer, getHooksContainer, getErrorHandlerContainer } fro
export { IHook, IHandler, IErrorHandler } from '../interfaces/controller.js';
export { CREATOR } from '../symbols/index.js';

export { Container } from '../decorators/helpers/container.js';
export { Container } from '../registry/container.js';
export { CLASS_LOADER } from './class-loader.js';
45 changes: 29 additions & 16 deletions lib/plugins/life-cycle.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,41 @@
import { FastifyInstance } from 'fastify';
import { Registrable } from './shared-interfaces.js';
import { HooksRegistry, hooksRegistry } from '../registry/hooks-registry.js';

/**
* Hook that executed when Fastify starts
* fastify-decorators loading
*/
export type AppInitHook = (fastifyInstance: FastifyInstance) => unknown | Promise<unknown>;

/**
* Hooks that executed before each controller
* class instantiation.
*/
export type BeforeControllerCreationHook = (fastifyInstance: FastifyInstance, target: Registrable) => unknown | Promise<unknown>;

/**
* Hooks that executed after each controller
* class were instantiated.
*/
export type AfterControllerCreationHook = (fastifyInstance: FastifyInstance, target: Registrable, instance: unknown) => unknown | Promise<unknown>;
export type AppReadyHook = (fastifyInstance: FastifyInstance) => unknown | Promise<unknown>;
export type AppDestroyHook = (fastifyInstance: FastifyInstance) => unknown | Promise<unknown>;

export interface HooksRegistry {
appInit: AppInitHook[];
beforeControllerCreation: BeforeControllerCreationHook[];
afterControllerCreation: AfterControllerCreationHook[];
appReady: AppReadyHook[];
appDestroy: AppDestroyHook[];
}
/**
* Hooks that executed when all controllers
* are instantiated.
*/
export type AppReadyHook = (fastifyInstance: FastifyInstance) => unknown | Promise<unknown>;

export const hooksRegistry: HooksRegistry = {
appInit: [],
beforeControllerCreation: [],
afterControllerCreation: [],
appReady: [],
appDestroy: [],
};
/**
* Hooks that executed when fastify instance
* is going to close
*/
export type AppDestroyHook = (fastifyInstance: FastifyInstance) => unknown | Promise<unknown>;

/**
* Helper function for hooking fastify-decorators,
* see overloads and hooks description.
*/
export function createInitializationHook<T extends 'appInit'>(stage: T, hookFn: AppInitHook): void;
export function createInitializationHook<T extends 'beforeControllerCreation'>(stage: T, hookFn: BeforeControllerCreationHook): void;
export function createInitializationHook<T extends 'afterControllerCreation'>(stage: T, hookFn: AfterControllerCreationHook): void;
Expand Down
4 changes: 4 additions & 0 deletions lib/plugins/shared-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import type { CREATOR } from '../symbols/index.js';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Constructable<T = unknown> = new (...args: any) => T;

/**
* Registrable is special classes that
* could be instantiated with class-loader
*/
export interface Registrable<T = unknown> extends Constructable<T> {
[CREATOR]: {
register(instance: FastifyInstance, prefix?: string): Promise<void> | void;
Expand Down
File renamed without changes.
42 changes: 42 additions & 0 deletions lib/registry/container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Container for storing objects
* Can handle hierarchy as well
*
* @experimental
*/
export class Container<T, K = T> implements Iterable<T> {
private readonly _values = new Map<K, T>();

constructor(private _parent?: Container<T, K>) {}

public *[Symbol.iterator](): IterableIterator<T> {
if (this._parent) yield* this._parent;
yield* this._values.values();
}

public setParent(parent: Container<T, K>): void {
this._parent = parent;
}

public set(key: K, value: T): void {
this._values.set(key, value);
}

public get(key: K): T | null {
if (this._values.has(key)) {
return this._values.get(key) ?? null;
}
return this._parent?.get(key) ?? null;
}

public push(...items: T[]): void {
items.forEach((item) => this._values.set(item as unknown as K, item));
}

public get length(): number {
let length = 0;
if (this._parent) length += this._parent.length;
length += this._values.size;
return length;
}
}
17 changes: 17 additions & 0 deletions lib/registry/hooks-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { AfterControllerCreationHook, AppDestroyHook, AppInitHook, AppReadyHook, BeforeControllerCreationHook } from '../plugins/life-cycle.js';

export interface HooksRegistry {
appInit: AppInitHook[];
beforeControllerCreation: BeforeControllerCreationHook[];
afterControllerCreation: AfterControllerCreationHook[];
appReady: AppReadyHook[];
appDestroy: AppDestroyHook[];
}

export const hooksRegistry: HooksRegistry = {
appInit: [],
beforeControllerCreation: [],
afterControllerCreation: [],
appReady: [],
appDestroy: [],
};
42 changes: 19 additions & 23 deletions plugins/typedi/src/use-container.spec.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,30 @@
import { FastifyInstance } from 'fastify';
import { hooksRegistry, Registrable } from 'fastify-decorators/plugins';
import { Container } from 'typedi';
import { fastify } from 'fastify';
import { bootstrap, Controller } from 'fastify-decorators';
import { Container, Service } from 'typedi';
import { useContainer } from './index.js';
import { CLASS_LOADER } from 'fastify-decorators/plugins';

describe('Use container', () => {
beforeEach(() => {
hooksRegistry.beforeControllerCreation = [];
hooksRegistry.appInit = [];
Container.reset();
});

it('should register before controller creation hook', () => {
beforeAll(() => {
useContainer(Container);

expect(hooksRegistry.beforeControllerCreation).toHaveLength(1);
});

it('should register controller in container on beforeControllerCreation', () => {
useContainer(Container);

class Test {}
it('should create controller with injected dependency', async () => {
const instance = fastify();
instance.register(bootstrap, { controllers: [SampleController] });

hooksRegistry.beforeControllerCreation[0]({} as FastifyInstance, Test as Registrable);
await instance.ready();

expect(Container.get(Test)).toBeInstanceOf(Test);
expect(Container.has(SampleController)).toBeTruthy();
expect(instance[CLASS_LOADER](SampleController, instance)).toBeInstanceOf(SampleController);
expect(instance[CLASS_LOADER](SampleController, instance).dependency).toBeInstanceOf(Dependency);
});
});

it('should register after controller creation hook', () => {
useContainer(Container);
@Service()
class Dependency {}

expect(hooksRegistry.appInit).toHaveLength(1);
});
});
@Controller()
class SampleController {
constructor(public dependency: Dependency) {}
}
2 changes: 1 addition & 1 deletion plugins/typedi/src/use-container.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CLASS_LOADER, createInitializationHook } from 'fastify-decorators/plugins';
import { Constructable } from 'fastify-decorators/plugins/index.js';
import { Constructable } from 'fastify-decorators/plugins';
import type { Container as TypeDIContainer, ServiceOptions } from 'typedi';

export function useContainer(Container: typeof TypeDIContainer) {
Expand Down

0 comments on commit eb16859

Please sign in to comment.