Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

release: v0.3.1 #37

Merged
merged 31 commits into from
Aug 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3edace0
add: exports in module
player31-kks Aug 15, 2023
5d855db
Merge pull request #30 from tiny-nestjs/feature/provider-export
Nhahan Aug 15, 2023
0fd57b3
feat: defined auto-alias decorator
Nhahan Aug 15, 2023
1d8ea6f
Merge branch 'dev' into feat/auto-alias
Nhahan Aug 15, 2023
d10e8dc
refactor: merge `import-sync.ts` into `import.ts`
Nhahan Aug 15, 2023
d634041
fix: `index.ts` import statements.
Nhahan Aug 15, 2023
13e36d8
fix: `@AutoAlias` proper parameter
Nhahan Aug 15, 2023
26f31a5
feat: `@AutoAlias`. And fix to use more native `Nest` types.
Nhahan Aug 15, 2023
d624574
fix: parameter of `@AutoAlias()` must be defined
Nhahan Aug 15, 2023
7e75eb7
revert: remove export from type ClassType
Nhahan Aug 16, 2023
a56b858
update: `@AutoAlias()` related features, and more.
Nhahan Aug 16, 2023
feb1792
fix: importer exports
Nhahan Aug 16, 2023
8c64292
Merge pull request #31 from tiny-nestjs/feat/auto-alias
Nhahan Aug 16, 2023
2b959bf
Merge pull request #32 from tiny-nestjs/update/README
player31-kks Aug 16, 2023
c4399f6
[Fix] getRootGlobPath -> getFolderPath
player31-kks Aug 16, 2023
3e63cc5
fix: get right folderPath
Nhahan Aug 16, 2023
82f4844
fix: matchGlob also resolve path with parameter `require.main.path`
Nhahan Aug 16, 2023
bc32713
fix: `matchGlob()` to return right variables
Nhahan Aug 16, 2023
02ea943
Merge pull request #33 from tiny-nestjs/feature/component-scan-path
player31-kks Aug 16, 2023
caf8a49
fix: dynamic `@ComponentScan()` parameter
Nhahan Aug 16, 2023
a1e725b
Merge pull request #34 from tiny-nestjs/fix/component-scan-parameter
player31-kks Aug 16, 2023
5fa5357
chore: allow `Function` type
Nhahan Aug 17, 2023
09b54fe
add: component-scan symbol
Nhahan Aug 17, 2023
6ebf16b
add: define reflection watermark on `ComponentScan`
Nhahan Aug 17, 2023
160bd80
feat: throw `Nest` native look Error when `@ComponentScan()` scopes a…
Nhahan Aug 17, 2023
0e5bbbb
docs: npm version to `0.3.1`
Nhahan Aug 17, 2023
2807f44
docs: enhance `catchDuplicateScanScope()` ts-doc
Nhahan Aug 17, 2023
21d16ee
chore: duplicate -> overlapped, wrong word usage
Aug 18, 2023
76e105e
refactor: `getForRootPath()`
Nhahan Aug 18, 2023
865b152
Merge pull request #36 from tiny-nestjs/feat/component-scan-no-duplic…
Nhahan Aug 18, 2023
7d08ce4
fix: `ComponentScan()` scope overlap error more looking like `Nest`
Nhahan Aug 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,15 @@ module.exports = {
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
Expand Down
98 changes: 89 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
</p>

<div align="center">
<img src="https://img.shields.io/badge/npm-v0.3.0-blue" alt="npm version">
<img src="https://img.shields.io/badge/npm-v0.3.1-blue" alt="npm version">
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
</div>

Expand All @@ -17,9 +17,10 @@ With this library, you can inject dependencies into classes without the need for

## Features

- `@ComponentScan()` decorator enables automatic scanning and injection of classes within a module.
- `@AutoInjectable()` decorator allows classes to be automatically injectable for DI.
- `@AutoController()` decorator automatically registers controllers.
- `@ComponentScan()` enables automatic scanning and injection of classes within a module.
- `@AutoInjectable()` allows classes to be automatically injectable for DI.
- `@AutoController()` automatically registers controllers.
- `@AutoAlias()` defines an alias for the @AutoInjectable() class.

## Installation

Expand All @@ -33,7 +34,7 @@ yarn add @tiny-nestjs/auto-injectable

## Usage

**1. `@ComponentScan()`**
**1. `@ComponentScan()` basic usage**

```ts
import { Module } from '@nestjs/common';
Expand All @@ -49,7 +50,7 @@ export class AppModule {
}
```

By applying the `@ComponentScan()` decorator to the AppModule class, Nest will automatically scan for classes and
By applying the `@ComponentScan()` decorator to the `AppModule` class, Nest will automatically scan for classes and
inject necessary dependencies.

**2. `@AutoInjectable()`**
Expand All @@ -63,8 +64,9 @@ export class CatService {
}
```

In this case, by applying the `@AutoInjectable()` decorator to the CatService class, the class has become injectable,
allowing it to be injected into other modules without the need for module definitions.
In this case, by applying the `@AutoInjectable()` decorator to the `CatService` class, the class has become injectable,
allowing it to be injected into other modules without the need for module definitions. (The parameter
of `@AutoInjectable()` is the same as `@Injectable()`)

**3. `@AutoController()` and dependency injection**

Expand All @@ -84,11 +86,89 @@ export class CatController {
```

The class with the `@AutoInjectable()` decorator has been successfully injected and `/cats` api can be accessed by
applying `@AutoController()` on `CatController` service.
applying `@AutoController()` on `CatController` service. (The parameter of `@AutoController()` is the same
as `@Controller()`)

| You can see actual [project example](https://github.com/tiny-nestjs/auto-injectable-example) here. |
|----------------------------------------------------------------------------------------------------|

<br>

---

<br>

- _Below are advanced usage techniques of the library. In most cases, utilizing the methods above will suffice._

<br>

**4. `@AutoAlias()`**

```ts
import { AutoAlias } from '@tiny-nestjs/auto-injectable';
import { AutoInjectable } from '@tiny-nestjs/auto-injectable';

@AutoAlias('kitty')
@AutoInjectable()
export class CatService {
// ...
}
```

```ts
import { Inject } from '@nestjs/common';
import { AutoController } from '@tiny-nestjs/auto-injectable';

@AutoController()
export class CatController {
constructor(@Inject('kitty') private readonly catService: CatService) {
}
}
```

`@AutoAlias()` is a decorator used to specify an alias. In the constructor of the `CatService` class, `@Inject('kitty')`
is used to configure the injection of a `CatService` instance with the alias 'kitty'.
As the library is fully compatible with the `Nest` core, you can use `Nest`'s built-in `@Inject()` decorator.

**5. Define DI scope with the `@ComponentScan()`**

```ts
import { Module } from '@nestjs/common';
import { ComponentScan } from '@tiny-nestjs/auto-injectable';

@ComponentScan()
@Module({})
export class AnimalModule {
}
```

The library recommends using `@ComponentScan()` in the AppModule. However, to enable seamless DI within the desired
scope, you can also specify `@ComponentScan()` in other modules.

**6. `@ComponentScan()` parameters**

```bash
# normal case
- /cat
- cat.module.ts
- ...
```

```bash
# special case
- /animal
- /cat
- /module
- cat.module.ts
- /service
- cat.service.ts
```

In most cases, the module is positioned at the top-level directory of its domain. However, in some cases, you can
specify the exact directory path as an array parameter, such as `@ComponentScan(['animal/cat'])`.

<br>

## Contribution

To contribute to this library, fork the [GitHub repository](https://github.com/tiny-nestjs/auto-injectable), make your
Expand Down
15 changes: 2 additions & 13 deletions lib/auto.module.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,10 @@
import { DynamicModule, Module } from '@nestjs/common';
import { Importer, ImporterSync } from './core';
import { Importer } from './core';

@Module({})
export class AutoModule {
static async forRootAsync(patterns: string[]): Promise<DynamicModule> {
const classes = await Importer.load(patterns);

return {
module: AutoModule,
controllers: [...classes.controllers],
providers: [...classes.providers],
exports: [...classes.providers],
};
}

static forRoot(patterns: string[]): DynamicModule {
const classes = ImporterSync.load(patterns);
const classes = Importer.load(patterns);

return {
module: AutoModule,
Expand Down
59 changes: 0 additions & 59 deletions lib/core/import-sync.ts

This file was deleted.

99 changes: 69 additions & 30 deletions lib/core/importer.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,93 @@
import { glob } from 'glob';
/* eslint-disable @typescript-eslint/no-var-requires */
import { globSync } from 'glob';
import { resolve } from 'path';
import { AUTO_CONTROLLER_WATERMARK, AUTO_INJECTABLE_WATERMARK } from '../interfaces';
import 'reflect-metadata';
import {
AUTO_ALIAS_WATERMARK,
AUTO_CONTROLLER_WATERMARK,
AUTO_INJECTABLE_WATERMARK,
COMPONENT_SCAN_WATERMARK,
} from '../interfaces';
import { Logger, Provider } from '@nestjs/common';
import { Type } from '@nestjs/common/interfaces/type.interface';
import { DynamicModule } from '@nestjs/common/interfaces/modules/dynamic-module.interface';
import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface';
import { Abstract } from '@nestjs/common/interfaces/abstract.interface';
import { locate } from 'func-loc';

type ClassType = new (...args: any[]) => any;

interface AutoClasses {
providers: ClassType[];
controllers: ClassType[];
providers: Provider[];
controllers: Type[];
exports: Array<
DynamicModule | Promise<DynamicModule> | string | symbol | Provider | ForwardReference | Abstract<any> | Function
>;
}

export class Importer {
private static instance: Importer;
private rootPath = '';

static getInstance(): Importer {
if (!Importer.instance) {
Importer.instance = new Importer();
}
return Importer.instance;
}

static async load(patterns: string[]): Promise<AutoClasses> {
const importer = Importer.getInstance();
const pathNames = await importer.matchGlob(patterns);
const foundClasses = await Promise.all(pathNames.map((pathName) => importer.scan(pathName)));
static load(patterns: string[]): AutoClasses {
const importer = new Importer();
const pathNames = importer.matchGlob(patterns);
const foundClasses = pathNames.map((pathName) => importer.scan(pathName));
return foundClasses.reduce(
(merged, found) => ({
providers: [...merged.providers, ...found.providers],
controllers: [...merged.controllers, ...found.controllers],
exports: [...merged.exports, ...found.providers],
}),
{ providers: [], controllers: [] } as AutoClasses,
{ providers: [], controllers: [], exports: [] },
);
}

private async scan(pathName: string): Promise<AutoClasses> {
const exports: Record<string, unknown> = await import(pathName);
const autoClasses = Object.values(exports).filter((value) => typeof value === 'function') as ClassType[];
private scan(pathName: string): AutoClasses {
const exports: Record<string, unknown> = require(pathName);
return (Object.values(exports) as ClassType[]).reduce(
(classes: AutoClasses, value: ClassType) => {
if (typeof value === 'function') {
Reflect.hasMetadata(COMPONENT_SCAN_WATERMARK, value) && this.catchOverlappedScanScope(value, pathName);

return autoClasses.reduce(
(result: AutoClasses, value: ClassType) => {
Reflect.hasMetadata(AUTO_INJECTABLE_WATERMARK, value) && result.providers.push(value);
Reflect.hasMetadata(AUTO_CONTROLLER_WATERMARK, value) && result.controllers.push(value);
return result;
if (Reflect.hasMetadata(AUTO_ALIAS_WATERMARK, value)) {
classes.providers.push({
provide: Reflect.getMetadata(AUTO_ALIAS_WATERMARK, value),
useClass: value,
});
} else if (Reflect.hasMetadata(AUTO_INJECTABLE_WATERMARK, value)) {
classes.providers.push(value);
} else if (Reflect.hasMetadata(AUTO_CONTROLLER_WATERMARK, value)) {
classes.controllers.push(value);
}
}
return classes;
},
{ providers: [], controllers: [] },
{ providers: [], controllers: [], exports: [] },
);
}

private matchGlob(patterns: string[]) {
const globs = patterns.map((pattern) =>
globSync(resolve(require.main?.path || process.cwd(), pattern), {
ignore: ['**/node_modules/**'],
}),
);
return globs.flat();
}

private async matchGlob(patterns: string[]) {
const globs = patterns.map((pattern) => glob(resolve(process.cwd(), pattern)));
return (await Promise.all(globs)).flat();
/**
* This code is intentionally structured to execute the callback function registered with `.then()`
* and proceed through the event loop only after the asynchronous task of the `locate` function is completed.
* Designed to handle potential errors after the scan is completed.
*/
private catchOverlappedScanScope(value: { new (...args: any[]): any; name: string }, pathName: string) {
locate(value as any).then(({ path }: { path: string }) => {
if (!this.rootPath) this.rootPath = pathName;
if (this.rootPath !== path) {
new Logger('ExceptionHandler').error(
`ComponentScan() module scope cannot be overlapped.\n\nPotential causes:\n- A overlapped dependecy between modules.\n- Please check the module in '${this.rootPath}' and '${path}'\n\nScope [${value.name}]`,
);
process.exit(1);
}
});
}
}
1 change: 0 additions & 1 deletion lib/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from './importer';
export * from './import-sync';
7 changes: 7 additions & 0 deletions lib/decorators/auto-alias.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { AUTO_ALIAS_WATERMARK } from '../interfaces';

export function AutoAlias(alias: string) {
return (target: object) => {
Reflect.defineMetadata(AUTO_ALIAS_WATERMARK, alias, target);
};
}
Loading