Skip to content

Commit

Permalink
feat: support inject named export (#180)
Browse files Browse the repository at this point in the history
* feat: support named export

* fix test

* fix test

* add test

* fix

* fix: add type

* fix

* fix

* fix
  • Loading branch information
hyj1991 authored Aug 25, 2022
1 parent f3e5a2f commit bd3f8d1
Show file tree
Hide file tree
Showing 34 changed files with 248 additions and 82 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
],
"scripts": {
"build": "tsc -p ./tsconfig.build.json",
"test": "jest --coverage --detectOpenHandles",
"test": "jest --coverage --detectOpenHandles --testTimeout=15000",
"ci": "npm run lint && npm run test",
"lint:fix": "eslint . --ext .ts --fix",
"lint": "eslint . --ext .ts"
Expand Down
14 changes: 10 additions & 4 deletions src/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,22 @@ export enum ArtusInjectEnum {
Trigger = 'artus#trigger',
}

export const ARTUS_EXCEPTION_DEFAULT_LOCALE = 'en';

export const ARTUS_SERVER_ENV = 'ARTUS_SERVER_ENV';

export enum ARTUS_DEFAULT_CONFIG_ENV {
DEV = 'development',
PROD = 'production',
DEFAULT = 'default',
}

export enum ScanPolicy {
NamedExport = 'named_export',
DefaultExport = 'default_export',
All = "all",
}

export const ARTUS_EXCEPTION_DEFAULT_LOCALE = 'en';

export const ARTUS_SERVER_ENV = 'ARTUS_SERVER_ENV';

export const HOOK_NAME_META_PREFIX = 'hookName:';
export const HOOK_FILE_LOADER = 'appHook:fileLoader';
export const HOOK_CONFIG_HANDLE = 'appHook:configHandle::';
Expand Down
66 changes: 45 additions & 21 deletions src/loader/factory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as path from 'path';
import { isInjectable, Container } from '@artus/injection';
import { ArtusInjectEnum, DEFAULT_LOADER, HOOK_FILE_LOADER, LOADER_NAME_META } from '../constant';
import { ArtusInjectEnum, DEFAULT_LOADER, HOOK_FILE_LOADER, LOADER_NAME_META, ScanPolicy } from '../constant';
import {
Manifest,
ManifestItem,
Expand Down Expand Up @@ -92,8 +92,9 @@ export class LoaderFactory {
return loader.load(item);
}

async findLoader(opts: LoaderFindOptions): Promise<LoaderFindResult|null> {
const loaderName = await this.findLoaderName(opts);
async findLoader(opts: LoaderFindOptions): Promise<LoaderFindResult | null> {
const { loader: loaderName, exportNames } = await this.findLoaderName(opts);

if (!loaderName) {
return null;
}
Expand All @@ -104,40 +105,63 @@ export class LoaderFactory {
}
const result: LoaderFindResult = {
loaderName,
loaderState: { exportNames },
};
if (loaderClazz.onFind) {
result.loaderState = await loaderClazz.onFind(opts);
}
return result;
}

async findLoaderName(opts: LoaderFindOptions): Promise<string|null> {
async findLoaderName(opts: LoaderFindOptions): Promise<{ loader: string | null, exportNames: string[] }> {
for (const [loaderName, LoaderClazz] of LoaderFactory.loaderClazzMap.entries()) {
if (await LoaderClazz.is?.(opts)) {
return loaderName;
return { loader: loaderName, exportNames: [] };
}
}
const { root, filename } = opts;
const { root, filename, policy = ScanPolicy.All } = opts;

// require file for find loader
const targetClazz = await compatibleRequire(path.join(root, filename));
if (!isClass(targetClazz)) {
// The file is not export with default class
return null;
}
const allExport = await compatibleRequire(path.join(root, filename), true);
const exportNames: string[] = [];

let loaders = Object.entries(allExport)
.map(([name, targetClazz]) => {
if (!isClass(targetClazz)) {
// The file is not export with default class
return null;
}

// get loader from reflect metadata
const loaderMd = Reflect.getMetadata(HOOK_FILE_LOADER, targetClazz);
if (loaderMd?.loader) {
return loaderMd.loader;
}
if (policy === ScanPolicy.NamedExport && name === 'default') {
return null;
}

if (policy === ScanPolicy.DefaultExport && name !== 'default') {
return null;
}

// get loader from reflect metadata
const loaderMd = Reflect.getMetadata(HOOK_FILE_LOADER, targetClazz);
if (loaderMd?.loader) {
exportNames.push(name);
return loaderMd.loader;
}

// default loder with @Injectable
const injectableMd = isInjectable(targetClazz);
if (injectableMd) {
exportNames.push(name);
return DEFAULT_LOADER;
}
})
.filter(v => v);

loaders = Array.from(new Set(loaders));

// default loder with @Injectable
const injectableMd = isInjectable(targetClazz);
if (injectableMd) {
return DEFAULT_LOADER;
if (loaders.length > 1) {
throw new Error(`Not support multiple loaders for ${path.join(root, filename)}`);
}

return null;
return { loader: loaders[0] ?? null, exportNames };
}
}
41 changes: 26 additions & 15 deletions src/loader/impl/module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Container, InjectableDefinition, ScopeEnum } from '@artus/injection';
import { Constructable, Container, InjectableDefinition, ScopeEnum } from '@artus/injection';
import { DefineLoader } from '../decorator';
import { ManifestItem, Loader } from '../types';
import compatibleRequire from '../../utils/compatible_require';
Expand All @@ -12,23 +12,34 @@ class ModuleLoader implements Loader {
this.container = container;
}

async load(item: ManifestItem) {
const moduleClazz = await compatibleRequire(item.path);
const opts: Partial<InjectableDefinition> = {
path: item.path,
type: moduleClazz,
scope: ScopeEnum.EXECUTION, // The class used with @artus/core will have default scope EXECUTION, can be overwritten by Injectable decorator
};
if (item.id) {
opts.id = item.id;
}
async load(item: ManifestItem): Promise<Constructable[]> {
const origin = await compatibleRequire(item.path, true);
item._loaderState = Object.assign({ exportNames: ['default'] }, item._loaderState);
const { _loaderState: state } = item as { _loaderState: { exportNames: string[] } };

const modules: Constructable[] = [];

for (const name of state.exportNames) {
const moduleClazz = origin[name];
const opts: Partial<InjectableDefinition> = {
path: item.path,
type: moduleClazz,
scope: ScopeEnum.EXECUTION, // The class used with @artus/core will have default scope EXECUTION, can be overwritten by Injectable decorator
};
if (item.id) {
opts.id = item.id;
}

const shouldOverwriteValue = Reflect.getMetadata(SHOULD_OVERWRITE_VALUE, moduleClazz);
const shouldOverwriteValue = Reflect.getMetadata(SHOULD_OVERWRITE_VALUE, moduleClazz);

if (shouldOverwriteValue || !this.container.hasValue(opts)) {
this.container.set(opts);
if (shouldOverwriteValue || !this.container.hasValue(opts)) {
this.container.set(opts);
}

modules.push(moduleClazz);
}
return moduleClazz;

return modules;
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/loader/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Container } from '@artus/injection';
import { ScanPolicy } from '../constant';

interface Manifest {
items: ManifestItem[];
Expand All @@ -20,6 +21,7 @@ interface LoaderFindOptions {
root: string;
baseDir: string;
configDir: string;
policy?: ScanPolicy;
}

interface LoaderFindResult {
Expand Down
6 changes: 5 additions & 1 deletion src/scanner/scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
DEFAULT_EXCLUDES,
DEFAULT_LOADER_LIST_WITH_ORDER,
LOADER_NAME_META,
ScanPolicy,
} from '../constant';
import { LoaderFactory, Manifest, ManifestItem } from '../loader';
import { ScannerOptions, WalkOptions } from './types';
Expand Down Expand Up @@ -37,6 +38,7 @@ export class Scanner {
useRelativePath: true,
configDir: DEFAULT_CONFIG_DIR,
loaderListGenerator: (defaultLoaderList: string[]) => defaultLoaderList,
policy: ScanPolicy.All,
...options,
exclude: DEFAULT_EXCLUDES.concat(options.exclude ?? []),
extensions: [...new Set(this.moduleExtensions.concat(options.extensions ?? []))],
Expand Down Expand Up @@ -169,11 +171,12 @@ export class Scanner {
if (ScanUtils.isExclude(filename, extname, this.options.exclude, this.options.extensions)) {
return null;
}
let loader = await loaderFactory.findLoaderName({
let { loader } = await loaderFactory.findLoaderName({
filename,
baseDir,
root,
configDir,
policy: this.options.policy,
});
if (loader === 'framework-config') {
// SEEME: framework-config is a special loader, cannot be used when scan, need refactor later
Expand Down Expand Up @@ -242,6 +245,7 @@ export class Scanner {
extensions: this.options.extensions,
exclude: this.options.exclude,
configDir: this.options.configDir,
policy: this.options.policy,
};

if (source === 'plugin') {
Expand Down
3 changes: 3 additions & 0 deletions src/scanner/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { BaseLoader, ManifestItem } from "../loader";
import { FrameworkConfig } from "../framework";
import { PluginConfigItem } from "../plugin/types";
import { Application } from "../types";
import { ScanPolicy } from "../constant";

export interface ScannerOptions {
appName: string;
Expand All @@ -10,6 +11,7 @@ export interface ScannerOptions {
useRelativePath: boolean;
exclude: string[];
configDir: string;
policy: ScanPolicy;
envs?: string[];
framework?: FrameworkConfig;
plugin?: Record<string, Partial<PluginConfigItem>>;
Expand All @@ -21,6 +23,7 @@ export interface WalkOptions {
source: string;
baseDir: string;
configDir: string;
policy: ScanPolicy;
extensions: string[];
exclude: string[];
itemMap: Map<string, ManifestItem[]>;
Expand Down
1 change: 1 addition & 0 deletions src/scanner/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export class ScanUtils {
root,
baseDir,
configDir,
policy: options.policy,
});
if (!loaderFindResult) {
continue;
Expand Down
4 changes: 2 additions & 2 deletions src/utils/compatible_require.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import assert from 'assert';
* compatible esModule require
* @param path
*/
export default async function compatibleRequire(path: string): Promise<any> {
export default async function compatibleRequire(path: string, origin = false): Promise<any> {
const requiredModule = await import(path);

assert(requiredModule, `module '${path}' exports is undefined`);

return requiredModule.default || requiredModule;
return origin ? requiredModule : (requiredModule.default || requiredModule);
}
8 changes: 5 additions & 3 deletions test/fixtures/app_koa_with_ts/src/koa_app.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Injectable } from '@artus/injection';
import { Injectable, ScopeEnum } from '@artus/injection';
import Koa from 'koa';

@Injectable()
export default class KoaApplication extends Koa {}
@Injectable({
scope: ScopeEnum.SINGLETON,
})
export default class KoaApplication extends Koa { }
6 changes: 4 additions & 2 deletions test/fixtures/app_with_lifecycle/lifecyclelist.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Injectable } from '../../../src';
import { Injectable, ScopeEnum } from '../../../src';

@Injectable()
@Injectable({
scope: ScopeEnum.SINGLETON,
})
export default class LifecycleList {
lifecycleList: string[] = [];

Expand Down
6 changes: 4 additions & 2 deletions test/fixtures/application_specific/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import path from 'path';
import { Manifest, ArtusApplication, ArtusInjectEnum } from "../../../../src";
import { AbstractBar } from '../../frameworks/bar/src';
import { Inject, Injectable } from "@artus/injection";
import { Inject, Injectable, ScopeEnum } from "@artus/injection";

@Injectable()
@Injectable({
scope: ScopeEnum.SINGLETON,
})
export default class MyArtusApplication {
@Inject('ABSTRACT_BAR')
private bar: AbstractBar;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Injectable } from "@artus/injection";
import { Injectable, ScopeEnum } from "@artus/injection";

export interface MysqlConfig {
clientName: string
}

@Injectable({
id: 'ARTUS_MYSQL',
scope: ScopeEnum.SINGLETON,
})
export default class Client {
private clientName = '';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Injectable } from "@artus/injection";
import { Injectable, ScopeEnum } from "@artus/injection";

export interface MysqlConfig {
clientName: string
}

@Injectable({
id: 'ARTUS_MYSQL',
scope: ScopeEnum.SINGLETON,
})
export default class Client {
private clientName = '';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Injectable } from "@artus/injection";
import { Injectable, ScopeEnum } from "@artus/injection";

export interface RedisConfig {
clientName: string
}

@Injectable({
id: 'ARTUS_REDIS',
scope: ScopeEnum.SINGLETON,
})
export default class Client {
private clientName = '';
Expand Down
6 changes: 4 additions & 2 deletions test/fixtures/artus_application/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import path from 'path';
import { Manifest, ArtusApplication, ArtusInjectEnum } from "../../../../src";
import { AbstractBar } from '../../frameworks/bar/src';
import { Inject, Injectable } from "@artus/injection";
import { Inject, Injectable, ScopeEnum } from "@artus/injection";

@Injectable()
@Injectable({
scope: ScopeEnum.SINGLETON,
})
export default class MyArtusApplication {
@Inject('ABSTRACT_BAR')
private bar: AbstractBar;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Injectable } from "@artus/injection";
import { Injectable, ScopeEnum } from "@artus/injection";

export interface MysqlConfig {
clientName: string
}

@Injectable({
id: 'ARTUS_MYSQL',
scope: ScopeEnum.SINGLETON,
})
export default class Client {
private clientName = '';
Expand Down
Loading

0 comments on commit bd3f8d1

Please sign in to comment.