├── .eslintignore ├── .eslintrc ├── .github └── workflows │ ├── nodejs.yml │ └── npm-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── exception.json ├── jest.config.js ├── package.json ├── src ├── application.ts ├── configuration │ └── index.ts ├── constant.ts ├── decorator.ts ├── exception │ ├── constant.ts │ ├── decorator.ts │ ├── impl.ts │ ├── index.ts │ ├── types.ts │ └── utils.ts ├── index.ts ├── injection.ts ├── lifecycle │ └── index.ts ├── loader │ ├── base.ts │ ├── decorator.ts │ ├── factory.ts │ ├── helper.ts │ ├── impl │ │ ├── config.ts │ │ ├── exception.ts │ │ ├── exception_filter.ts │ │ ├── index.ts │ │ ├── lifecycle.ts │ │ ├── module.ts │ │ └── plugin_meta.ts │ ├── index.ts │ ├── loader_event.ts │ ├── types.ts │ └── utils │ │ ├── config_file_meta.ts │ │ └── merge.ts ├── logger │ ├── impl.ts │ ├── index.ts │ ├── level.ts │ └── types.ts ├── plugin │ ├── common.ts │ ├── factory.ts │ ├── impl.ts │ ├── index.ts │ └── types.ts ├── scanner │ ├── impl.ts │ ├── index.ts │ ├── task.ts │ ├── types.ts │ └── utils.ts ├── types.ts └── utils │ ├── compatible_require.ts │ ├── fs.ts │ ├── index.ts │ ├── is.ts │ └── load_meta_file.ts ├── test ├── __snapshots__ │ └── scanner.test.ts.snap ├── app.test.ts ├── config.test.ts ├── exception.test.ts ├── exception_filter.test.ts ├── fixtures │ ├── app_empty │ │ └── .gitkeep │ ├── app_koa_with_ts │ │ ├── package.json │ │ ├── src │ │ │ ├── bootstrap.ts │ │ │ ├── config │ │ │ │ ├── config.default.ts │ │ │ │ ├── config.dev.ts │ │ │ │ ├── plugin.d.ts │ │ │ │ ├── plugin.default.ts │ │ │ │ └── plugin.dev.ts │ │ │ ├── controllers │ │ │ │ └── hello.ts │ │ │ ├── exception.json │ │ │ ├── filter │ │ │ │ └── default.ts │ │ │ ├── koa_app.ts │ │ │ ├── lifecycle.ts │ │ │ ├── mysql_plugin │ │ │ │ ├── app.ts │ │ │ │ └── meta.json │ │ │ ├── no_ext_file │ │ │ ├── redis_plugin │ │ │ │ ├── app.ts │ │ │ │ ├── meta.json │ │ │ │ ├── not_to_be_scanned_dir │ │ │ │ │ └── not_to_be_scanned_file.ts │ │ │ │ └── not_to_be_scanned_file.ts │ │ │ ├── services │ │ │ │ ├── hello.ts │ │ │ │ ├── no_default.ts │ │ │ │ └── no_injectable.ts │ │ │ └── test_duplicate_plugin │ │ │ │ └── meta.json │ │ └── tsconfig.json │ ├── app_with_config │ │ ├── app.ts │ │ ├── bootstrap.ts │ │ ├── config │ │ │ ├── config.default.ts │ │ │ └── config.production.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── app_with_lifecycle │ │ ├── bootstrap_duplicated.ts │ │ ├── bootstrap_load.ts │ │ ├── bootstrap_ready.ts │ │ ├── config │ │ │ ├── config.default.ts │ │ │ └── plugin.ts │ │ ├── lifecyclelist.ts │ │ ├── plugins │ │ │ ├── plugin-a │ │ │ │ ├── bootstrap.ts │ │ │ │ └── meta.json │ │ │ └── plugin-b │ │ │ │ ├── bootstrap.ts │ │ │ │ └── meta.json │ │ └── test │ │ │ └── throw.ts │ ├── app_with_manifest │ │ ├── bootstrap.ts │ │ ├── config │ │ │ └── config.default.ts │ │ ├── controller │ │ │ └── home.ts │ │ ├── lifecycle.ts │ │ └── manifest.json │ ├── app_with_plugin_version_check │ │ ├── config │ │ │ └── plugin.default.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── app_with_preset_b │ │ ├── config │ │ │ └── plugin.default.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── app_without_config │ │ ├── app.ts │ │ ├── lifecyclelist.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── custom_instance │ │ ├── bootstrap.ts │ │ ├── config │ │ │ └── config.default.ts │ │ └── custom.ts │ ├── exception_filter │ │ ├── bootstrap.ts │ │ ├── error.ts │ │ ├── exception.json │ │ ├── filter.ts │ │ ├── package.json │ │ ├── service.ts │ │ └── tsconfig.json │ ├── exception_invalid_filter │ │ ├── bootstrap.ts │ │ ├── filter.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── exception_with_ts_yaml │ │ ├── app.ts │ │ ├── bootstrap.ts │ │ ├── exception.json │ │ ├── package.json │ │ └── tsconfig.json │ ├── logger │ │ ├── package.json │ │ ├── src │ │ │ ├── custom_logger.ts │ │ │ ├── index.ts │ │ │ ├── test_clazz.ts │ │ │ └── test_custom_clazz.ts │ │ └── tsconfig.json │ ├── module_with_custom_loader │ │ ├── package.json │ │ ├── src │ │ │ ├── index.ts │ │ │ ├── loader │ │ │ │ └── test_custom_loader.ts │ │ │ └── test_clazz.ts │ │ └── tsconfig.json │ ├── module_with_js │ │ ├── package.json │ │ └── src │ │ │ ├── index.js │ │ │ ├── test_service_a.js │ │ │ └── test_service_b.js │ ├── module_with_ts │ │ ├── package.json │ │ ├── src │ │ │ ├── index.ts │ │ │ ├── test_service_a.ts │ │ │ └── test_service_b.ts │ │ └── tsconfig.json │ ├── named_export │ │ ├── package.json │ │ └── src │ │ │ ├── config │ │ │ └── config.default.ts │ │ │ ├── index.ts │ │ │ ├── mysql.ts │ │ │ └── redis.ts │ └── plugins │ │ ├── plugin_a │ │ ├── meta.json │ │ └── package.json │ │ ├── plugin_a_other_ver │ │ ├── meta.json │ │ └── package.json │ │ ├── plugin_a_same_ver │ │ ├── meta.json │ │ └── package.json │ │ ├── plugin_b │ │ ├── meta.json │ │ └── package.json │ │ ├── plugin_c │ │ ├── meta.json │ │ └── package.json │ │ ├── plugin_d │ │ ├── meta.json │ │ └── package.json │ │ ├── plugin_with_entry_a │ │ ├── mock_lib │ │ │ ├── index.js │ │ │ └── meta.json │ │ └── package.json │ │ ├── plugin_with_entry_b │ │ ├── mock_lib │ │ │ ├── index.js │ │ │ └── meta.json │ │ └── package.json │ │ ├── plugin_with_entry_c │ │ ├── mock_lib │ │ │ ├── index.js │ │ │ └── meta.json │ │ └── package.json │ │ ├── plugin_with_entry_wrong │ │ ├── mock_lib │ │ │ ├── index.js │ │ │ └── meta.json │ │ └── package.json │ │ ├── plugin_wrong_a │ │ ├── meta.json │ │ └── package.json │ │ ├── plugin_wrong_b │ │ ├── meta.json │ │ └── package.json │ │ ├── preset_a │ │ ├── config │ │ │ └── plugin.default.ts │ │ ├── meta.json │ │ ├── package.json │ │ └── tsconfig.json │ │ ├── preset_b │ │ ├── config │ │ │ └── plugin.default.ts │ │ ├── meta.json │ │ ├── package.json │ │ └── tsconfig.json │ │ └── preset_c │ │ ├── config │ │ └── plugin.default.ts │ │ ├── meta.json │ │ ├── package.json │ │ └── tsconfig.json ├── lifecycle.test.ts ├── loader.test.ts ├── logger.test.ts ├── plugin.test.ts ├── scanner.test.ts └── utils │ └── index.ts ├── tsconfig.build.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | coverage 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@artus/eslint-config-artus/typescript", 4 | "plugin:import/recommended", 5 | "plugin:import/typescript" 6 | ], 7 | "parserOptions": { 8 | "project": "./tsconfig.json" 9 | }, 10 | "rules": { 11 | "@typescript-eslint/ban-types": "off", 12 | "no-unused-vars": "off", 13 | "@typescript-eslint/no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }] 14 | }, 15 | "overrides": [ 16 | { 17 | "files": [ 18 | "test/**/*" 19 | ], 20 | "rules": { 21 | "@typescript-eslint/no-var-requires": "off" 22 | } 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | - master 11 | pull_request: 12 | branches: 13 | - main 14 | - master 15 | schedule: 16 | - cron: '0 2 * * *' 17 | 18 | jobs: 19 | build: 20 | runs-on: ${{ matrix.os }} 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | node-version: [16, 18] 26 | os: [ubuntu-latest, windows-latest, macos-latest] 27 | 28 | steps: 29 | - name: Checkout Git Source 30 | uses: actions/checkout@v2 31 | 32 | - name: Use Node.js ${{ matrix.node-version }} 33 | uses: actions/setup-node@v3 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | 37 | - name: Install Dependencies 38 | run: npm i 39 | 40 | - name: Continuous Integration 41 | run: npm run ci 42 | 43 | - name: Code Coverage 44 | uses: codecov/codecov-action@v3 45 | with: 46 | token: ${{ secrets.CODECOV_TOKEN }} 47 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: NPM Publish 2 | 3 | on: 4 | release: 5 | # This specifies that the build will be triggered when we publish a release 6 | types: 7 | - published 8 | 9 | jobs: 10 | publish: 11 | name: Publish to NPM & Create Pull Request of Version Update 12 | # Run on latest version of ubuntu 13 | runs-on: ubuntu-latest 14 | permissions: 15 | id-token: write 16 | contents: write 17 | pull-requests: write 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | with: 22 | # "ref" specifies the branch to check out. 23 | # "github.event.release.target_commitish" is a global variable and specifies the branch the release targeted 24 | ref: ${{ github.event.release.target_commitish }} 25 | # install Node.js 26 | - name: Use Node.js 16 27 | uses: actions/setup-node@v1 28 | with: 29 | node-version: 18 30 | # Specifies the registry, this field is required! 31 | registry-url: https://registry.npmjs.org/ 32 | # clean install of your projects' deps. We use "npm ci" to avoid package lock changes 33 | - run: npm install -g npm@latest 34 | - run: npm install 35 | # set up git since we will later push to the repo 36 | - run: git config --global user.name "Artus Version Bot" 37 | - run: git config --global user.email "artusjs-version-bot@example.org" 38 | - run: git status 39 | - run: git diff 40 | # upgrade npm version in package.json to the tag used in the release. 41 | # upgrade npm version in package.json to the tag used in the release. 42 | - run: npm version ${{ github.event.release.tag_name }} --allow-same-version 43 | # build the project 44 | - run: npm run build 45 | # run tests just in case 46 | - run: npm test 47 | # publish to NPM -> there is one caveat, continue reading for the fix 48 | - run: npm publish --provenance --access public 49 | if: "!github.event.release.prerelease" 50 | env: 51 | # Use a token to publish to NPM. See below for how to set it up 52 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 53 | - run: npm publish --provenance --access public --tag beta 54 | if: "github.event.release.prerelease" 55 | env: 56 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 57 | - name: Create Pull Request 58 | uses: peter-evans/create-pull-request@v4 59 | with: 60 | title: "[Version] ${{ github.event.release.tag_name }}" 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Dependency Lock File 107 | package-lock.json 108 | yarn.lock 109 | pnpm-lock.yaml 110 | 111 | # Visual Studio Code Workspace Configuration 112 | .vscode/ 113 | 114 | lib 115 | 116 | # OSX 117 | .DS_Store 118 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-present Artus.js Working Group 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @artus/core 2 | 3 | [![Node.js CI](https://github.com/artusjs/core/actions/workflows/nodejs.yml/badge.svg)](https://github.com/artusjs/core/actions/workflows/nodejs.yml) 4 | [![codecov](https://codecov.io/gh/artusjs/core/branch/main/graph/badge.svg)](https://codecov.io/gh/artusjs/core) 5 | 6 | Core package of Artus 7 | 8 | ## Build 9 | 10 | `@artus/core` write with TypeScript, so we need build source code to Node.js Module. 11 | 12 | ```bash 13 | npm run build 14 | # Or 15 | yarn build 16 | # Or 17 | pnpm run build 18 | ``` 19 | 20 | ## Run test case 21 | 22 | `@artus/core` use `jest` for unit-test case. 23 | 24 | ```bash 25 | npm run test 26 | # Or 27 | yarn test 28 | # Or 29 | pnpm run test 30 | ``` 31 | -------------------------------------------------------------------------------- /exception.json: -------------------------------------------------------------------------------- 1 | { 2 | "ARTUS:GLOBAL_TEST": { 3 | "desc": "全局测试错误,仅用于单元测试", 4 | "detailUrl": "https://github.com/artusjs/spec" 5 | }, 6 | "ARTUS:GLOBAL_TEST_I18N": { 7 | "desc": { 8 | "zh": "全局测试错误,仅用于单元测试", 9 | "en": "This is a test exception, only valid in unit-test" 10 | }, 11 | "detailUrl": "https://github.com/artusjs/spec" 12 | } 13 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | transform: { 4 | '^.+\\.tsx?$': 'ts-jest', 5 | }, 6 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 7 | testEnvironment: 'node', 8 | testPathIgnorePatterns: [ 9 | '/test/fixtures/app-koa-with-ts/src/controllers' 10 | ], 11 | coverageReporters: ['json', 'json-summary', 'lcov', 'text', 'clover', 'text-summary', 'cobertura'], 12 | collectCoverageFrom: ['/src/**/*.ts'], 13 | transformIgnorePatterns: ["/node_modules/(?!(artus_plugin_hbase)/)", "\\.pnp\\.[^\\\/]+$"] 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@artus/core", 3 | "version": "2.2.3", 4 | "description": "Core package of Artus", 5 | "main": "./lib/index.js", 6 | "types": "./lib/index.d.ts", 7 | "exports": { 8 | ".": { 9 | "types": "./lib/index.d.ts", 10 | "default": "./lib/index.js" 11 | }, 12 | "./injection": { 13 | "types": "./lib/injection.d.ts", 14 | "default": "./lib/injection.js" 15 | }, 16 | "./utils/*": { 17 | "types": "./lib/utils/*.d.ts", 18 | "default": "./lib/utils/*.js" 19 | }, 20 | "./package.json": "./package.json" 21 | }, 22 | "files": [ 23 | "lib" 24 | ], 25 | "scripts": { 26 | "build": "tsc -p ./tsconfig.build.json", 27 | "test": "jest --detectOpenHandles --testTimeout=15000", 28 | "cov": "jest --coverage --detectOpenHandles --testTimeout=15000", 29 | "ci": "npm run lint && npm run cov", 30 | "lint:fix": "eslint . --ext .ts --fix", 31 | "lint": "eslint . --ext .ts" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/artusjs/core.git" 36 | }, 37 | "author": "Artus Working Group", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/artusjs/core/issues" 41 | }, 42 | "homepage": "https://github.com/artusjs/core#readme", 43 | "devDependencies": { 44 | "@artus/eslint-config-artus": "0.0.1", 45 | "@artus/tsconfig": "1.0.1", 46 | "@babel/core": "^7.18.6", 47 | "@types/jest": "^29.5.11", 48 | "@types/js-yaml": "^4.0.5", 49 | "@types/koa": "^2.13.4", 50 | "@types/minimatch": "^3.0.5", 51 | "@types/node": "^20.10.5", 52 | "axios": "^0.26.1", 53 | "babel-jest": "^29.7.0", 54 | "egg-ci": "^2.1.0", 55 | "eslint": "^8.19.0", 56 | "eslint-plugin-import": "^2.26.0", 57 | "jest": "^29.7.0", 58 | "koa": "^2.13.4", 59 | "reflect-metadata": "^0.1.13", 60 | "ts-jest": "^29.1.1", 61 | "typescript": "^4.8.0" 62 | }, 63 | "dependencies": { 64 | "@artus/injection": "^0.5.1", 65 | "deepmerge": "^4.2.2", 66 | "minimatch": "^5.0.1", 67 | "tslib": "^2.6.1" 68 | }, 69 | "peerDependencies": { 70 | "reflect-metadata": "^0.1.13" 71 | }, 72 | "ci": { 73 | "version": "16, 18" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/application.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Container } from '@artus/injection'; 3 | import { ArtusInjectEnum } from './constant'; 4 | import { ArtusStdError } from './exception'; 5 | import { HookFunction, LifecycleManager } from './lifecycle'; 6 | import { LoaderFactory, Manifest } from './loader'; 7 | import { mergeConfig } from './loader/utils/merge'; 8 | import { Application, ApplicationInitOptions } from './types'; 9 | import ConfigurationHandler from './configuration'; 10 | import { Logger, LoggerType } from './logger'; 11 | 12 | export class ArtusApplication implements Application { 13 | public manifest?: Manifest; 14 | public container: Container; 15 | 16 | constructor(opts?: ApplicationInitOptions) { 17 | this.container = new Container(opts?.containerName ?? ArtusInjectEnum.DefaultContainerName); 18 | 19 | if (opts?.env) { 20 | const envList = [].concat(opts.env); 21 | this.container.set({ id: ArtusInjectEnum.EnvList, value: envList }); 22 | } 23 | this.loadDefaultClass(); 24 | this.addLoaderListener(); 25 | 26 | process.on('SIGINT', () => this.close(true)); 27 | process.on('SIGTERM', () => this.close(true)); 28 | } 29 | 30 | get config(): Record { 31 | return this.container.get(ArtusInjectEnum.Config); 32 | } 33 | 34 | get configurationHandler(): ConfigurationHandler { 35 | return this.container.get(ConfigurationHandler); 36 | } 37 | 38 | get lifecycleManager(): LifecycleManager { 39 | return this.container.get(LifecycleManager); 40 | } 41 | 42 | get loaderFactory(): LoaderFactory { 43 | return this.container.get(LoaderFactory); 44 | } 45 | 46 | get logger(): LoggerType { 47 | return this.container.get(Logger); 48 | } 49 | 50 | loadDefaultClass() { 51 | // load Artus default clazz 52 | this.container.set({ id: Container, value: this.container }); 53 | this.container.set({ id: ArtusInjectEnum.Application, value: this }); 54 | this.container.set({ id: ArtusInjectEnum.Config, value: {} }); 55 | 56 | this.container.set({ type: ConfigurationHandler }); 57 | this.container.set({ type: LoaderFactory }); 58 | this.container.set({ type: LifecycleManager }); 59 | this.container.set({ type: Logger }); 60 | } 61 | 62 | async load(manifest: Manifest, root: string = process.cwd()) { 63 | // Load user manifest 64 | this.manifest = manifest; 65 | 66 | await this.loaderFactory.loadManifest(manifest, root); 67 | 68 | await this.lifecycleManager.emitHook('didLoad'); 69 | 70 | return this; 71 | } 72 | 73 | async run() { 74 | await this.lifecycleManager.emitHook('willReady'); // 通知协议实现层启动服务器 75 | await this.lifecycleManager.emitHook('didReady'); 76 | } 77 | 78 | registerHook(hookName: string, hookFn: HookFunction) { 79 | this.lifecycleManager.registerHook(hookName, hookFn); 80 | } 81 | 82 | async close(exit = false) { 83 | try { 84 | // reverse emitHook to avoid plugin closed before app hook 85 | await this.lifecycleManager.emitHook('beforeClose', null, true); 86 | } catch (e) { 87 | throw new Error(e); 88 | } 89 | if (exit) { 90 | process.exit(0); 91 | } 92 | } 93 | 94 | throwException(code: string): void { 95 | throw new ArtusStdError(code); 96 | } 97 | 98 | createException(code: string): ArtusStdError { 99 | return new ArtusStdError(code); 100 | } 101 | 102 | protected addLoaderListener() { 103 | this.loaderFactory 104 | .addLoaderListener('config', { 105 | before: () => this.lifecycleManager.emitHook('configWillLoad'), 106 | after: () => { 107 | this.updateConfig(); 108 | return this.lifecycleManager.emitHook('configDidLoad'); 109 | }, 110 | }); 111 | } 112 | 113 | protected updateConfig() { 114 | const oldConfig = this.container.get(ArtusInjectEnum.Config, { noThrow: true }) ?? {}; 115 | const newConfig = this.configurationHandler.getMergedConfig() ?? {}; 116 | this.container.set({ 117 | id: ArtusInjectEnum.Config, 118 | value: mergeConfig(oldConfig, newConfig), 119 | }); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/configuration/index.ts: -------------------------------------------------------------------------------- 1 | import { Container, Inject, Injectable } from '@artus/injection'; 2 | import { ArtusInjectEnum, ARTUS_DEFAULT_CONFIG_ENV, ARTUS_SERVER_ENV } from '../constant'; 3 | import { ManifestItem } from '../loader'; 4 | import { mergeConfig } from '../loader/utils/merge'; 5 | import compatibleRequire from '../utils/compatible_require'; 6 | 7 | export type ConfigObject = Record; 8 | export type PackageObject = ConfigObject; 9 | 10 | @Injectable() 11 | export default class ConfigurationHandler { 12 | static getEnvFromFilename(filename: string): string { 13 | let [_, env, extname] = filename.split('.'); 14 | if (!extname) { 15 | env = ARTUS_DEFAULT_CONFIG_ENV.DEFAULT; 16 | } 17 | return env; 18 | } 19 | 20 | public configStore: Record = {}; 21 | 22 | @Inject() 23 | private container: Container; 24 | 25 | getMergedConfig(): ConfigObject { 26 | return this.mergeConfigByStore(this.configStore); 27 | } 28 | 29 | mergeConfigByStore(store: Record): ConfigObject { 30 | let envList: string[] = this.container.get(ArtusInjectEnum.EnvList, { noThrow: true }); 31 | if (!envList) { 32 | envList = process.env[ARTUS_SERVER_ENV] ? [process.env[ARTUS_SERVER_ENV]] : [ARTUS_DEFAULT_CONFIG_ENV.DEV]; 33 | } 34 | const defaultConfig = store[ARTUS_DEFAULT_CONFIG_ENV.DEFAULT] ?? {}; 35 | const envConfigList = envList.map(currentEnv => (store[currentEnv] ?? {})); 36 | return mergeConfig(defaultConfig, ...envConfigList); 37 | } 38 | 39 | clearStore(): void { 40 | this.configStore = {}; 41 | } 42 | 43 | setConfig(env: string, config: ConfigObject) { 44 | const storedConfig = this.configStore[env] ?? {}; 45 | this.configStore[env] = mergeConfig(storedConfig, config); 46 | } 47 | 48 | async setConfigByFile(fileItem: ManifestItem) { 49 | const configContent: ConfigObject = await compatibleRequire(fileItem.path + fileItem.extname); 50 | if (configContent) { 51 | const env = ConfigurationHandler.getEnvFromFilename(fileItem.filename); 52 | this.setConfig(env, configContent); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/constant.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_LOADER = 'module'; 2 | export const LOADER_NAME_META = 'loader:name'; 3 | 4 | export const ArtusInjectPrefix = 'artus#'; 5 | 6 | export enum ArtusInjectEnum { 7 | Application = 'artus#application', 8 | Config = 'artus#config', 9 | DefaultContainerName = 'artus#default_container', 10 | EnvList = 'artus#env_list', 11 | } 12 | 13 | export enum ARTUS_DEFAULT_CONFIG_ENV { 14 | DEV = 'development', 15 | PROD = 'production', 16 | DEFAULT = 'default', 17 | } 18 | 19 | export enum ScanPolicy { 20 | NamedExport = 'named_export', 21 | DefaultExport = 'default_export', 22 | All = 'all', 23 | } 24 | 25 | export const ARTUS_EXCEPTION_DEFAULT_LOCALE = 'en'; 26 | 27 | export const ARTUS_SERVER_ENV = 'ARTUS_SERVER_ENV'; 28 | 29 | export const HOOK_NAME_META_PREFIX = 'hookName:'; 30 | export const HOOK_FILE_LOADER = 'appHook:fileLoader'; 31 | 32 | export const DEFAULT_EXCLUDES = [ 33 | 'test', 34 | 'node_modules', 35 | '.*', 36 | 'tsconfig*.json', 37 | '*.d.ts', 38 | 'jest.config.*', 39 | 'meta.json', 40 | 'LICENSE', 41 | 'pnpm-lock.yaml', 42 | ]; 43 | export const DEFAULT_MODULE_EXTENSIONS = ['.js', '.json', '.node']; 44 | export const DEFAULT_APP_REF = '_app'; 45 | 46 | export const FRAMEWORK_PATTERN = 'framework.*'; 47 | export const PLUGIN_CONFIG_PATTERN = 'plugin.*'; 48 | export const PACKAGE_JSON = 'package.json'; 49 | export const PLUGIN_META_FILENAME = 'meta.json'; 50 | export const EXCEPTION_FILENAME = 'exception.json'; 51 | 52 | export const DEFAULT_LOADER_LIST_WITH_ORDER = [ 53 | 'exception', 54 | 'exception-filter', 55 | 'plugin-meta', 56 | 'package-json', 57 | 'module', 58 | 'lifecycle-hook-unit', 59 | 'config', 60 | ]; 61 | 62 | export const DEFAULT_CONFIG_DIR = 'src/config'; 63 | export const DEFAULT_MANIFEST_FILENAME = 'manifest.json'; 64 | 65 | export const SHOULD_OVERWRITE_VALUE = 'shouldOverwrite'; 66 | -------------------------------------------------------------------------------- /src/decorator.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@artus/injection'; 2 | import { 3 | HOOK_NAME_META_PREFIX, 4 | HOOK_FILE_LOADER, 5 | } from './constant'; 6 | 7 | export function LifecycleHookUnit(): ClassDecorator { 8 | return (target: any) => { 9 | Reflect.defineMetadata(HOOK_FILE_LOADER, { loader: 'lifecycle-hook-unit' }, target); 10 | Injectable({ lazy: true })(target); 11 | }; 12 | } 13 | 14 | export function LifecycleHook(hookName?: string): PropertyDecorator { 15 | return (target: any, propertyKey: string | symbol) => { 16 | if (typeof propertyKey === 'symbol') { 17 | throw new Error(`hookName is not support symbol [${propertyKey.description}]`); 18 | } 19 | Reflect.defineMetadata(`${HOOK_NAME_META_PREFIX}${propertyKey}`, hookName ?? propertyKey, target.constructor); 20 | }; 21 | } 22 | 23 | export * from './loader/decorator'; 24 | -------------------------------------------------------------------------------- /src/exception/constant.ts: -------------------------------------------------------------------------------- 1 | export const EXCEPTION_FILTER_METADATA_KEY = 'exception_filter_meta'; 2 | export const EXCEPTION_FILTER_MAP_INJECT_ID = Symbol.for('exception_filter_map'); 3 | export const EXCEPTION_FILTER_DEFAULT_SYMBOL = Symbol.for('exception_filter_default'); 4 | -------------------------------------------------------------------------------- /src/exception/decorator.ts: -------------------------------------------------------------------------------- 1 | import { Constructable, Injectable } from '@artus/injection'; 2 | import { HOOK_FILE_LOADER } from '../constant'; 3 | import { EXCEPTION_FILTER_DEFAULT_SYMBOL, EXCEPTION_FILTER_METADATA_KEY } from './constant'; 4 | 5 | export const Catch = (targetErr?: string|Constructable): ClassDecorator => { 6 | return (target: Function) => { 7 | Reflect.defineMetadata(EXCEPTION_FILTER_METADATA_KEY, { 8 | targetErr: targetErr ?? EXCEPTION_FILTER_DEFAULT_SYMBOL, 9 | }, target); 10 | Reflect.defineMetadata(HOOK_FILE_LOADER, { loader: 'exception-filter' }, target); 11 | Injectable()(target); 12 | }; 13 | }; -------------------------------------------------------------------------------- /src/exception/impl.ts: -------------------------------------------------------------------------------- 1 | import { ARTUS_EXCEPTION_DEFAULT_LOCALE } from '../constant'; 2 | import { ExceptionItem } from './types'; 3 | 4 | export class ArtusStdError extends Error { 5 | name = 'ArtusStdError'; 6 | private _code: string; 7 | private static codeMap: Map = new Map(); 8 | private static currentLocale: string = process.env.ARTUS_ERROR_LOCALE || ARTUS_EXCEPTION_DEFAULT_LOCALE; 9 | 10 | public static registerCode(code: string, item: ExceptionItem) { 11 | this.codeMap.set(code, item); 12 | } 13 | 14 | public static setCurrentLocale(locale: string) { 15 | this.currentLocale = locale; 16 | } 17 | 18 | constructor (code: string) { 19 | super(`[${code}] This is Artus standard error, Please check on https://github.com/artusjs/spec/blob/master/documentation/core/6.exception.md`); // default message 20 | this._code = code; 21 | this.message = this.getFormatedMessage(); 22 | } 23 | 24 | private getFormatedMessage(): string { 25 | const { code, desc, detailUrl } = this; 26 | return `[${code}] ${desc}${detailUrl ? ', Please check on ' + detailUrl : ''}`; 27 | } 28 | 29 | public get code(): string { 30 | return this._code; 31 | } 32 | 33 | public get desc(): string { 34 | const { codeMap, currentLocale } = ArtusStdError; 35 | const exceptionItem = codeMap.get(this._code); 36 | if (!exceptionItem) { 37 | return 'Unknown Error'; 38 | } 39 | if (typeof exceptionItem.desc === 'string') { 40 | return exceptionItem.desc; 41 | } 42 | return exceptionItem.desc[currentLocale]; 43 | } 44 | 45 | public get detailUrl(): string|undefined { 46 | return ArtusStdError.codeMap.get(this._code)?.detailUrl; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/exception/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constant'; 2 | export * from './decorator'; 3 | export * from './impl'; 4 | export * from './types'; 5 | export * from './utils'; 6 | -------------------------------------------------------------------------------- /src/exception/types.ts: -------------------------------------------------------------------------------- 1 | import { Constructable } from '@artus/injection'; 2 | import { ArtusStdError } from './impl'; 3 | 4 | export interface ExceptionItem { 5 | desc: string | Record; 6 | detailUrl?: string; 7 | } 8 | 9 | export type ExceptionIdentifier = string|symbol|Constructable; 10 | export type ExceptionFilterMapType = Map>; 11 | 12 | export interface ExceptionFilterType { 13 | catch(err: Error|ArtusStdError): void | Promise; 14 | } 15 | -------------------------------------------------------------------------------- /src/exception/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Constructable, Container } from '@artus/injection'; 3 | import { EXCEPTION_FILTER_DEFAULT_SYMBOL, EXCEPTION_FILTER_MAP_INJECT_ID } from './constant'; 4 | import { ArtusStdError } from './impl'; 5 | import { ExceptionFilterMapType, ExceptionFilterType } from './types'; 6 | import { isClass } from '../utils/is'; 7 | 8 | 9 | export const matchExceptionFilterClazz = (err: Error, container: Container): Constructable | null => { 10 | const filterMap: ExceptionFilterMapType = container.get(EXCEPTION_FILTER_MAP_INJECT_ID, { 11 | noThrow: true, 12 | }); 13 | if (!filterMap) { 14 | return null; 15 | } 16 | 17 | // handle ArtusStdError with code simply 18 | if (err instanceof ArtusStdError && filterMap.has(err.code)) { 19 | return filterMap.get(err.code); 20 | } 21 | 22 | // handle CustomErrorClazz, loop inherit class 23 | let errConstructor: Function = err['constructor']; 24 | while(isClass(errConstructor)) { // until parent/self is not class 25 | if (filterMap.has(errConstructor as Constructable)) { 26 | return filterMap.get(errConstructor as Constructable); 27 | } 28 | errConstructor = Object.getPrototypeOf(errConstructor); // get parent clazz by prototype 29 | } 30 | 31 | // handle default ExceptionFilter 32 | if (filterMap.has(EXCEPTION_FILTER_DEFAULT_SYMBOL)) { 33 | return filterMap.get(EXCEPTION_FILTER_DEFAULT_SYMBOL); 34 | } 35 | 36 | return null; 37 | }; 38 | 39 | export const matchExceptionFilter = (err: Error, container: Container): ExceptionFilterType | null => { 40 | const filterClazz = matchExceptionFilterClazz(err, container); 41 | 42 | // return the instance of exception filter 43 | return filterClazz ? container.get(filterClazz) : null; 44 | }; 45 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@artus/injection'; 2 | 3 | export * from './loader'; 4 | export * from './logger'; 5 | export * from './lifecycle'; 6 | export * from './exception'; 7 | export * from './plugin'; 8 | export * from './application'; 9 | export * from './scanner'; 10 | export * from './decorator'; 11 | export * from './types'; 12 | export * from './constant'; 13 | 14 | import ConfigurationHandler from './configuration'; 15 | export { ConfigurationHandler }; 16 | 17 | -------------------------------------------------------------------------------- /src/injection.ts: -------------------------------------------------------------------------------- 1 | export * from '@artus/injection'; -------------------------------------------------------------------------------- /src/lifecycle/index.ts: -------------------------------------------------------------------------------- 1 | import { Constructable, Container, Inject, Injectable, ScopeEnum } from '@artus/injection'; 2 | import { Application, ApplicationLifecycle } from '../types'; 3 | import { 4 | ArtusInjectEnum, 5 | HOOK_NAME_META_PREFIX, 6 | } from '../constant'; 7 | 8 | export type HookFunction = (hookProps: { 9 | app: Application, 10 | lifecycleManager: LifecycleManager, 11 | payload?: T 12 | }) => void | Promise; 13 | 14 | @Injectable({ 15 | scope: ScopeEnum.SINGLETON, 16 | }) 17 | export class LifecycleManager { 18 | public enable = true; // Enabled default, will NOT emit when enable is false 19 | 20 | private hookList: string[] = [ 21 | 'configWillLoad', // 配置文件即将加载,这是最后动态修改配置的时机 22 | 'configDidLoad', // 配置文件加载完成 23 | 'didLoad', // 文件加载完成 24 | 'willReady', // 插件启动完毕 25 | 'didReady', // 应用启动完成 26 | 'beforeClose', // 应用即将关闭 27 | ]; 28 | private hookFnMap: Map = new Map(); 29 | private hookUnitSet: Set> = new Set(); 30 | 31 | @Inject(ArtusInjectEnum.Application) 32 | private app: Application; 33 | 34 | @Inject() 35 | private container: Container; 36 | 37 | insertHook(existHookName: string, newHookName: string) { 38 | const startIndex = this.hookList.findIndex(val => val === existHookName); 39 | this.hookList.splice(startIndex, 0, newHookName); 40 | } 41 | 42 | appendHook(newHookName: string) { 43 | this.hookList.push(newHookName); 44 | } 45 | 46 | registerHook(hookName: string, hookFn: HookFunction) { 47 | if (this.hookFnMap.has(hookName)) { 48 | this.hookFnMap.get(hookName)?.push(hookFn); 49 | } else { 50 | this.hookFnMap.set(hookName, [ 51 | hookFn, 52 | ]); 53 | } 54 | } 55 | 56 | registerHookUnit(extClazz: Constructable) { 57 | if (this.hookUnitSet.has(extClazz)) { 58 | return; 59 | } 60 | this.hookUnitSet.add(extClazz); 61 | const fnMetaKeys = Reflect.getMetadataKeys(extClazz); 62 | const extClazzInstance = this.container.get(extClazz); 63 | for (const fnMetaKey of fnMetaKeys) { 64 | if (typeof fnMetaKey !== 'string' || !fnMetaKey.startsWith(HOOK_NAME_META_PREFIX)) { 65 | continue; 66 | } 67 | const hookName = Reflect.getMetadata(fnMetaKey, extClazz); 68 | const propertyKey = fnMetaKey.slice(HOOK_NAME_META_PREFIX.length); 69 | this.registerHook(hookName, extClazzInstance[propertyKey].bind(extClazzInstance)); 70 | } 71 | } 72 | 73 | async emitHook(hookName: string, payload?: T, reverse = false) { 74 | if (!this.enable) { 75 | return; 76 | } 77 | if (!this.hookFnMap.has(hookName)) { 78 | return; 79 | } 80 | const fnList = this.hookFnMap.get(hookName); 81 | if (!Array.isArray(fnList)) { 82 | return; 83 | } 84 | // lifecycle hook should only trigger one time 85 | this.hookFnMap.delete(hookName); 86 | if (reverse) { 87 | fnList.reverse(); 88 | } 89 | for (const hookFn of fnList) { 90 | await hookFn({ 91 | app: this.app, 92 | lifecycleManager: this, 93 | payload, 94 | }); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/loader/base.ts: -------------------------------------------------------------------------------- 1 | import { Loader, ManifestItem } from './types'; 2 | 3 | export default class BaseLoader implements Loader { 4 | async load(_item: ManifestItem) { 5 | throw new Error('Not implemented'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/loader/decorator.ts: -------------------------------------------------------------------------------- 1 | import { LOADER_NAME_META } from '../constant'; 2 | 3 | export const DefineLoader = (loaderName: string): ClassDecorator => 4 | (target: Function) => { 5 | Reflect.defineMetadata(LOADER_NAME_META, loaderName, target); 6 | } 7 | ; 8 | -------------------------------------------------------------------------------- /src/loader/factory.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { Container, Injectable, Inject, ScopeEnum } from '@artus/injection'; 3 | import { DEFAULT_LOADER, LOADER_NAME_META, DEFAULT_LOADER_LIST_WITH_ORDER, DEFAULT_APP_REF, ARTUS_DEFAULT_CONFIG_ENV } from '../constant'; 4 | import { 5 | Manifest, 6 | ManifestItem, 7 | LoaderConstructor, 8 | Loader, 9 | } from './types'; 10 | import ConfigurationHandler from '../configuration'; 11 | import { LifecycleManager } from '../lifecycle'; 12 | import LoaderEventEmitter, { LoaderEventListener } from './loader_event'; 13 | import { PluginConfig, PluginFactory } from '../plugin'; 14 | import { Logger, LoggerType } from '../logger'; 15 | import { mergeConfig } from './utils/merge'; 16 | 17 | @Injectable({ 18 | scope: ScopeEnum.SINGLETON, 19 | }) 20 | export class LoaderFactory { 21 | public static loaderClazzMap: Map = new Map(); 22 | 23 | static register(clazz: LoaderConstructor) { 24 | const loaderName = Reflect.getMetadata(LOADER_NAME_META, clazz); 25 | this.loaderClazzMap.set(loaderName, clazz); 26 | } 27 | 28 | @Inject() 29 | private container: Container; 30 | 31 | private loaderEmitter: LoaderEventEmitter = new LoaderEventEmitter(); 32 | 33 | get lifecycleManager(): LifecycleManager { 34 | return this.container.get(LifecycleManager); 35 | } 36 | 37 | get configurationHandler(): ConfigurationHandler { 38 | return this.container.get(ConfigurationHandler); 39 | } 40 | 41 | get logger(): LoggerType { 42 | return this.container.get(Logger); 43 | } 44 | 45 | addLoaderListener(eventName: string, listener: LoaderEventListener) { 46 | this.loaderEmitter.addListener(eventName, listener); 47 | return this; 48 | } 49 | 50 | removeLoaderListener(eventName: string, stage?: 'before' | 'after') { 51 | this.loaderEmitter.removeListener(eventName, stage); 52 | return this; 53 | } 54 | 55 | getLoader(loaderName: string): Loader { 56 | const LoaderClazz = LoaderFactory.loaderClazzMap.get(loaderName); 57 | if (!LoaderClazz) { 58 | throw new Error(`Cannot find loader '${loaderName}'`); 59 | } 60 | return new LoaderClazz(this.container); 61 | } 62 | 63 | async loadManifest( 64 | manifest: Manifest, 65 | root: string = process.cwd(), 66 | ): Promise { 67 | if (!('version' in manifest) || manifest.version !== '2') { 68 | throw new Error(`invalid manifest, @artus/core@2.x only support manifest version 2.`); 69 | } 70 | // Manifest Version 2 is supported mainly 71 | 72 | // Merge plugin config with ref 73 | const mergeRef = (refName: string) => { 74 | if (!refName || !manifest.refMap?.[refName]) { 75 | return {}; 76 | } 77 | const pluginConfig = this.configurationHandler.mergeConfigByStore(manifest.refMap[refName].pluginConfig ?? {}); 78 | return mergeConfig(...Object.values(pluginConfig).map(({ refName }) => mergeRef(refName)).concat(pluginConfig)); 79 | }; 80 | const mergedPluginConfig = mergeConfig(manifest.extraPluginConfig ?? {}, ...[ 81 | ...Object.values(manifest.extraPluginConfig ?? {}).map(({ refName }) => refName), 82 | DEFAULT_APP_REF, 83 | ].map(mergeRef)) as PluginConfig; 84 | this.configurationHandler.setConfig(ARTUS_DEFAULT_CONFIG_ENV.DEFAULT, { 85 | plugin: mergedPluginConfig, 86 | }); // For compatible 87 | for (const [pluginName, pluginConfigItem] of Object.entries(mergedPluginConfig)) { 88 | const refItem = manifest.refMap[pluginConfigItem.refName]; 89 | if (!refItem) { 90 | continue; 91 | } 92 | mergedPluginConfig[pluginName] = { 93 | ...pluginConfigItem, 94 | metadata: refItem.pluginMetadata, 95 | }; 96 | } 97 | 98 | // sort ref(plugin) firstly 99 | const sortedPluginList = await PluginFactory.createFromConfig(mergedPluginConfig, { 100 | logger: this.logger, 101 | }); 102 | 103 | // Merge itemList 104 | let itemList: ManifestItem[] = []; 105 | const sortedRefNameList: (string | null)[] = sortedPluginList 106 | .map(plugin => ((plugin.enable && mergedPluginConfig[plugin.name]?.refName) || null)) 107 | .concat([DEFAULT_APP_REF]); 108 | for (const refName of sortedRefNameList) { 109 | const refItem = manifest.refMap[refName]; 110 | itemList = itemList.concat(refItem.items); 111 | } 112 | 113 | // Load final item list(non-ordered) 114 | await this.loadItemList(itemList, root); 115 | } 116 | 117 | async loadItemList(itemList: ManifestItem[] = [], root?: string): Promise { 118 | const itemMap = new Map(DEFAULT_LOADER_LIST_WITH_ORDER.map(loaderName => [loaderName, []])); 119 | 120 | // group by loader names 121 | for (const item of itemList) { 122 | if (!itemMap.has(item.loader)) { 123 | // compatible for custom loader 124 | itemMap.set(item.loader, []); 125 | } 126 | let resolvedPath = item.path; 127 | if (root && !path.isAbsolute(resolvedPath)) { 128 | resolvedPath = path.resolve(root, resolvedPath); 129 | } 130 | itemMap.get(item.loader)!.push({ 131 | ...item, 132 | path: resolvedPath, 133 | loader: item.loader ?? DEFAULT_LOADER, 134 | }); 135 | } 136 | 137 | // trigger loader 138 | for (const [loaderName, itemList] of itemMap) { 139 | await this.loaderEmitter.emitBefore(loaderName); 140 | 141 | for (const item of itemList) { 142 | const curLoader = item.loader; 143 | await this.loaderEmitter.emitBeforeEach(curLoader, item); 144 | const result = await this.loadItem(item); 145 | await this.loaderEmitter.emitAfterEach(curLoader, item, result); 146 | } 147 | 148 | await this.loaderEmitter.emitAfter(loaderName); 149 | } 150 | } 151 | 152 | loadItem(item: ManifestItem): Promise { 153 | const loaderName = item.loader || DEFAULT_LOADER; 154 | const loader = this.getLoader(loaderName); 155 | loader.state = item.loaderState; 156 | return loader.load(item); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/loader/helper.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { isInjectable } from '@artus/injection'; 3 | import { LoaderFactory } from './factory'; 4 | import { 5 | LoaderFindOptions, 6 | LoaderFindResult, 7 | } from './types'; 8 | import { ScanPolicy, HOOK_FILE_LOADER, DEFAULT_LOADER } from '../constant'; 9 | import compatibleRequire from '../utils/compatible_require'; 10 | import { isClass } from '../utils/is'; 11 | 12 | export const findLoaderName = async (opts: LoaderFindOptions): Promise<{ loader: string | null, exportNames: string[] }> => { 13 | // Use Loader.is to find loader 14 | for (const [loaderName, LoaderClazz] of LoaderFactory.loaderClazzMap.entries()) { 15 | if (await LoaderClazz.is?.(opts)) { 16 | return { loader: loaderName, exportNames: [] }; 17 | } 18 | } 19 | const { root, filename, policy = ScanPolicy.All } = opts; 20 | 21 | // require file for find loader 22 | const allExport = await compatibleRequire(path.join(root, filename), true); 23 | const exportNames: string[] = []; 24 | 25 | let loaders = Object.entries(allExport) 26 | .map(([name, targetClazz]) => { 27 | if (!isClass(targetClazz)) { 28 | // The file is not export with default class 29 | return null; 30 | } 31 | 32 | if (policy === ScanPolicy.NamedExport && name === 'default') { 33 | return null; 34 | } 35 | 36 | if (policy === ScanPolicy.DefaultExport && name !== 'default') { 37 | return null; 38 | } 39 | 40 | // get loader from reflect metadata 41 | const loaderMd = Reflect.getMetadata(HOOK_FILE_LOADER, targetClazz); 42 | if (loaderMd?.loader) { 43 | exportNames.push(name); 44 | return loaderMd.loader; 45 | } 46 | 47 | // default loder with @Injectable 48 | const injectableMd = isInjectable(targetClazz); 49 | if (injectableMd) { 50 | exportNames.push(name); 51 | return DEFAULT_LOADER; 52 | } 53 | }) 54 | .filter(v => v); 55 | 56 | loaders = Array.from(new Set(loaders)); 57 | 58 | if (loaders.length > 1) { 59 | throw new Error(`Not support multiple loaders for ${path.join(root, filename)}`); 60 | } 61 | 62 | return { loader: loaders[0] ?? null, exportNames }; 63 | }; 64 | 65 | export const findLoader = async (opts: LoaderFindOptions): Promise => { 66 | const { loader: loaderName, exportNames } = await findLoaderName(opts); 67 | 68 | if (!loaderName) { 69 | return null; 70 | } 71 | 72 | const loaderClazz = LoaderFactory.loaderClazzMap.get(loaderName); 73 | if (!loaderClazz) { 74 | throw new Error(`Cannot find loader '${loaderName}'`); 75 | } 76 | const result: LoaderFindResult = { 77 | loaderName, 78 | loaderState: { exportNames }, 79 | }; 80 | if (loaderClazz.onFind) { 81 | result.loaderState = await loaderClazz.onFind(opts); 82 | } 83 | return result; 84 | }; 85 | 86 | -------------------------------------------------------------------------------- /src/loader/impl/config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { Container } from '@artus/injection'; 3 | import ConfigurationHandler from '../../configuration'; 4 | import { ArtusInjectEnum, FRAMEWORK_PATTERN } from '../../constant'; 5 | import { DefineLoader } from '../decorator'; 6 | import { ManifestItem, Loader, LoaderFindOptions } from '../types'; 7 | import compatibleRequire from '../../utils/compatible_require'; 8 | import { isMatch } from '../../utils'; 9 | import { Application } from '../../types'; 10 | import { getConfigMetaFromFilename } from '../utils/config_file_meta'; 11 | // import { PluginFactory } from '../../plugin'; 12 | 13 | @DefineLoader('config') 14 | class ConfigLoader implements Loader { 15 | protected container: Container; 16 | 17 | constructor(container) { 18 | this.container = container; 19 | } 20 | 21 | protected get app(): Application { 22 | return this.container.get(ArtusInjectEnum.Application); 23 | } 24 | 25 | protected get configurationHandler(): ConfigurationHandler { 26 | return this.container.get(ConfigurationHandler); 27 | } 28 | 29 | static async is(opts: LoaderFindOptions): Promise { 30 | return ( 31 | this.isConfigDir(opts) && 32 | !isMatch(opts.filename, FRAMEWORK_PATTERN) 33 | ); 34 | } 35 | 36 | protected static isConfigDir(opts: LoaderFindOptions): boolean { 37 | const { configDir, baseDir, root } = opts; 38 | return path.join(baseDir, configDir) === root; 39 | } 40 | 41 | async load(item: ManifestItem) { 42 | const { namespace, env } = getConfigMetaFromFilename(item.filename); 43 | let configObj = await this.loadConfigFile(item); 44 | if (namespace) { 45 | configObj = { 46 | [namespace]: configObj, 47 | }; 48 | } 49 | this.configurationHandler.setConfig(env, configObj); 50 | return configObj; 51 | } 52 | 53 | protected async loadConfigFile(item: ManifestItem): Promise> { 54 | const originConfigObj = await compatibleRequire(item.path + item.extname); 55 | let configObj = originConfigObj; 56 | if (typeof originConfigObj === 'function') { 57 | const app = this.container.get(ArtusInjectEnum.Application); 58 | configObj = originConfigObj(app); 59 | } 60 | return configObj; 61 | } 62 | } 63 | 64 | export default ConfigLoader; 65 | -------------------------------------------------------------------------------- /src/loader/impl/exception.ts: -------------------------------------------------------------------------------- 1 | import { DefineLoader } from '../decorator'; 2 | import { ManifestItem, Loader, LoaderFindOptions } from '../types'; 3 | import { ExceptionItem } from '../../exception/types'; 4 | import { loadMetaFile } from '../../utils/load_meta_file'; 5 | import { EXCEPTION_FILENAME } from '../../constant'; 6 | import { isMatch } from '../../utils'; 7 | import { ArtusStdError } from '../../exception'; 8 | 9 | @DefineLoader('exception') 10 | class ExceptionLoader implements Loader { 11 | static async is(opts: LoaderFindOptions) { 12 | return isMatch(opts.filename, EXCEPTION_FILENAME); 13 | } 14 | 15 | async load(item: ManifestItem) { 16 | try { 17 | const codeMap: Record = await loadMetaFile< 18 | Record 19 | >(item.path); 20 | for (const [errCode, exceptionItem] of Object.entries(codeMap)) { 21 | ArtusStdError.registerCode(errCode, exceptionItem); 22 | } 23 | return codeMap; 24 | } catch (error) { 25 | console.warn(`[Artus-Exception] Parse CodeMap ${item.path} failed: ${error.message}`); 26 | return void 0; 27 | } 28 | } 29 | } 30 | 31 | export default ExceptionLoader; 32 | -------------------------------------------------------------------------------- /src/loader/impl/exception_filter.ts: -------------------------------------------------------------------------------- 1 | import { DefineLoader } from '../decorator'; 2 | import { ManifestItem } from '../types'; 3 | import ModuleLoader from './module'; 4 | import { ArtusStdError, EXCEPTION_FILTER_DEFAULT_SYMBOL, EXCEPTION_FILTER_MAP_INJECT_ID, EXCEPTION_FILTER_METADATA_KEY } from '../../exception'; 5 | import { Constructable } from '@artus/injection'; 6 | import { ExceptionFilterMapType, ExceptionFilterType, ExceptionIdentifier } from '../../exception/types'; 7 | 8 | @DefineLoader('exception-filter') 9 | class ExceptionFilterLoader extends ModuleLoader { 10 | async load(item: ManifestItem) { 11 | // Get or Init Exception Filter Map 12 | let filterMap: ExceptionFilterMapType = this.container.get(EXCEPTION_FILTER_MAP_INJECT_ID, { 13 | noThrow: true, 14 | }); 15 | if (!filterMap) { 16 | filterMap = new Map(); 17 | this.container.set({ 18 | id: EXCEPTION_FILTER_MAP_INJECT_ID, 19 | value: filterMap, 20 | }); 21 | } 22 | 23 | const clazzList = await super.load(item) as Constructable[]; 24 | for (let i = 0; i < clazzList.length; i++) { 25 | const filterClazz = clazzList[i]; 26 | const filterMeta: { 27 | targetErr: ExceptionIdentifier 28 | } = Reflect.getOwnMetadata(EXCEPTION_FILTER_METADATA_KEY, filterClazz); 29 | 30 | if (!filterMeta) { 31 | throw new Error(`invalid ExceptionFilter ${filterClazz.name}`); 32 | } 33 | 34 | let { targetErr } = filterMeta; 35 | if (filterMap.has(targetErr)) { 36 | // check duplicated 37 | if (targetErr === EXCEPTION_FILTER_DEFAULT_SYMBOL) { 38 | throw new Error('the Default ExceptionFilter is duplicated'); 39 | } 40 | let targetErrName = targetErr; 41 | if (typeof targetErr !== 'string' && typeof targetErr !== 'symbol') { 42 | targetErrName = targetErr.name || targetErr; 43 | } 44 | throw new Error(`the ExceptionFilter for ${String(targetErrName)} is duplicated`); 45 | } 46 | 47 | if ( 48 | typeof targetErr !== 'string' && typeof targetErr !== 'symbol' && // Decorate with a error class 49 | Object.prototype.isPrototypeOf.call(ArtusStdError.prototype, targetErr.prototype) && // the class extends ArtusStdError 50 | typeof targetErr['code'] === 'string' // Have static property `code` defined by string 51 | ) { 52 | // Custom Exception Class use Error Code for simplied match 53 | targetErr = targetErr['code'] as string; 54 | } 55 | 56 | filterMap.set(targetErr, filterClazz); 57 | } 58 | return clazzList; 59 | } 60 | } 61 | 62 | export default ExceptionFilterLoader; 63 | -------------------------------------------------------------------------------- /src/loader/impl/index.ts: -------------------------------------------------------------------------------- 1 | import ModuleLoader from './module'; 2 | import ConfigLoader from './config'; 3 | import ExceptionLoader from './exception'; 4 | import ExceptionFilterLoader from './exception_filter'; 5 | import LifecycleLoader from './lifecycle'; 6 | import PluginMetaLoader from './plugin_meta'; 7 | 8 | export { 9 | ModuleLoader, 10 | ConfigLoader, 11 | ExceptionLoader, 12 | ExceptionFilterLoader, 13 | LifecycleLoader, 14 | PluginMetaLoader, 15 | }; 16 | -------------------------------------------------------------------------------- /src/loader/impl/lifecycle.ts: -------------------------------------------------------------------------------- 1 | import { Constructable, Container } from '@artus/injection'; 2 | import { LifecycleManager } from '../../lifecycle'; 3 | import { ApplicationLifecycle } from '../../types'; 4 | import { DefineLoader } from '../decorator'; 5 | import { ManifestItem, Loader } from '../types'; 6 | import compatibleRequire from '../../utils/compatible_require'; 7 | 8 | @DefineLoader('lifecycle-hook-unit') 9 | class LifecycleLoader implements Loader { 10 | private container: Container; 11 | 12 | constructor(container) { 13 | this.container = container; 14 | } 15 | 16 | get lifecycleManager(): LifecycleManager { 17 | return this.container.get(LifecycleManager); 18 | } 19 | 20 | async load(item: ManifestItem) { 21 | const origin: Constructable[] = await compatibleRequire(item.path + item.extname, true); 22 | item.loaderState = Object.assign({ exportNames: ['default'] }, item.loaderState); 23 | const { loaderState: state } = item as { loaderState: { exportNames: string[] } }; 24 | 25 | const lifecycleClazzList = []; 26 | 27 | for (const name of state.exportNames) { 28 | const clazz = origin[name]; 29 | this.container.set({ type: clazz }); 30 | this.lifecycleManager.registerHookUnit(clazz); 31 | } 32 | 33 | return lifecycleClazzList; 34 | } 35 | } 36 | 37 | export default LifecycleLoader; 38 | -------------------------------------------------------------------------------- /src/loader/impl/module.ts: -------------------------------------------------------------------------------- 1 | import { Constructable, Container, InjectableDefinition, ScopeEnum } from '@artus/injection'; 2 | import { DefineLoader } from '../decorator'; 3 | import { ManifestItem, Loader } from '../types'; 4 | import compatibleRequire from '../../utils/compatible_require'; 5 | import { SHOULD_OVERWRITE_VALUE } from '../../constant'; 6 | 7 | @DefineLoader('module') 8 | class ModuleLoader implements Loader { 9 | protected container: Container; 10 | 11 | constructor(container) { 12 | this.container = container; 13 | } 14 | 15 | async load(item: ManifestItem): Promise { 16 | const origin = await compatibleRequire(item.path + item.extname, true); 17 | item.loaderState = Object.assign({ exportNames: ['default'] }, item.loaderState); 18 | const { loaderState: state } = item as { loaderState: { exportNames: string[] } }; 19 | 20 | const modules: Constructable[] = []; 21 | 22 | for (const name of state.exportNames) { 23 | const moduleClazz = origin[name]; 24 | if (typeof moduleClazz !== 'function') { 25 | continue; 26 | } 27 | const opts: Partial = { 28 | type: moduleClazz, 29 | scope: ScopeEnum.EXECUTION, // The class used with @artus/core will have default scope EXECUTION, can be overwritten by Injectable decorator 30 | }; 31 | if (item.id) { 32 | opts.id = item.id; 33 | } 34 | 35 | const shouldOverwriteValue = Reflect.getMetadata(SHOULD_OVERWRITE_VALUE, moduleClazz); 36 | 37 | if (shouldOverwriteValue || !this.container.hasValue(opts)) { 38 | this.container.set(opts); 39 | } 40 | 41 | modules.push(moduleClazz); 42 | } 43 | 44 | return modules; 45 | } 46 | } 47 | 48 | export default ModuleLoader; 49 | -------------------------------------------------------------------------------- /src/loader/impl/plugin_meta.ts: -------------------------------------------------------------------------------- 1 | import { Container } from '@artus/injection'; 2 | import { DefineLoader } from '../decorator'; 3 | import { ManifestItem, Loader, LoaderFindOptions } from '../types'; 4 | import { loadMetaFile } from '../../utils/load_meta_file'; 5 | import { PluginMetadata } from '../../plugin/types'; 6 | import { PLUGIN_META_FILENAME } from '../../constant'; 7 | import { isMatch } from '../../utils'; 8 | 9 | @DefineLoader('plugin-meta') 10 | class PluginMetaLoader implements Loader { 11 | private container: Container; 12 | 13 | constructor(container) { 14 | this.container = container; 15 | } 16 | 17 | static async is(opts: LoaderFindOptions): Promise { 18 | return isMatch(opts.filename, PLUGIN_META_FILENAME); 19 | } 20 | 21 | async load(item: ManifestItem) { 22 | const pluginMeta: PluginMetadata = await loadMetaFile(item.path); 23 | this.container.set({ 24 | id: `pluginMeta_${pluginMeta.name}`, 25 | value: pluginMeta, 26 | }); 27 | return pluginMeta; 28 | } 29 | } 30 | 31 | export default PluginMetaLoader; 32 | -------------------------------------------------------------------------------- /src/loader/index.ts: -------------------------------------------------------------------------------- 1 | import { LoaderFactory } from './factory'; 2 | import BaseLoader from './base'; 3 | 4 | // Import inner impls 5 | import * as LoaderImpls from './impl'; 6 | 7 | // Register inner impls 8 | for (const [_, impl] of Object.entries(LoaderImpls)) { 9 | LoaderFactory.register(impl); 10 | } 11 | 12 | export * from './helper'; 13 | export * from './types'; 14 | 15 | export { 16 | // Class 17 | LoaderFactory, 18 | BaseLoader, 19 | }; 20 | -------------------------------------------------------------------------------- /src/loader/loader_event.ts: -------------------------------------------------------------------------------- 1 | import { ManifestItem } from './types'; 2 | 3 | export interface LoaderEventListener { 4 | before?: CallableFunction; 5 | after?: CallableFunction; 6 | 7 | beforeEach?: (item: ManifestItem) => void; 8 | 9 | afterEach?: (item: ManifestItem, loadContent: any) => void; 10 | } 11 | 12 | export default class LoaderEventEmitter { 13 | private listeners: Record> = {}; 14 | 15 | addListener(eventName, listener: LoaderEventListener) { 16 | if (!this.listeners[eventName]) { 17 | this.listeners[eventName] = { before: [], after: [], beforeEach: [], afterEach: [] }; 18 | } 19 | 20 | if (listener.before) { 21 | this.listeners[eventName].before.push(listener.before); 22 | } 23 | 24 | if (listener.after) { 25 | this.listeners[eventName].after.push(listener.after); 26 | } 27 | 28 | if (listener.beforeEach) { 29 | this.listeners[eventName].beforeEach.push(listener.beforeEach); 30 | } 31 | 32 | if (listener.afterEach) { 33 | this.listeners[eventName].afterEach.push(listener.afterEach); 34 | } 35 | } 36 | 37 | removeListener(eventName: string, stage?: keyof LoaderEventListener) { 38 | if (!this.listeners[eventName]) { 39 | return; 40 | } 41 | if (stage) { 42 | this.listeners[eventName][stage] = []; 43 | return; 44 | } 45 | 46 | delete this.listeners[eventName]; 47 | } 48 | 49 | async emitBefore(eventName, ...args) { 50 | await this.emit(eventName, 'before', ...args); 51 | } 52 | 53 | async emitAfter(eventName, ...args) { 54 | await this.emit(eventName, 'after', ...args); 55 | } 56 | 57 | async emitBeforeEach(eventName, ...args) { 58 | await this.emit(eventName, 'beforeEach', ...args); 59 | } 60 | 61 | async emitAfterEach(eventName, ...args) { 62 | await this.emit(eventName, 'afterEach', ...args); 63 | } 64 | 65 | async emit(eventName: string, stage: string, ...args) { 66 | const stages = (this.listeners[eventName] ?? {})[stage]; 67 | if (!stages || stages.length === 0) { 68 | return; 69 | } 70 | 71 | for (const listener of stages) { 72 | await listener(...args); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/loader/types.ts: -------------------------------------------------------------------------------- 1 | import { Container } from '@artus/injection'; 2 | import { ScanPolicy } from '../constant'; 3 | import { PluginConfig, PluginMetadata } from '../plugin/types'; 4 | 5 | // Key: Env => PluginName => Value: PluginConfigItem 6 | export type PluginConfigEnvMap = Record; 7 | 8 | export interface RefMapItem { 9 | relativedPath?: string; 10 | packageVersion?: string; 11 | pluginMetadata?: PluginMetadata; 12 | pluginConfig: PluginConfigEnvMap; 13 | items: ManifestItem[]; 14 | } 15 | // Key: RefName => RefMapItem 16 | export type RefMap = Record; 17 | 18 | export interface Manifest { 19 | version: '2'; 20 | refMap: RefMap; 21 | extraPluginConfig?: PluginConfig; 22 | } 23 | 24 | export interface ManifestItem extends Record { 25 | path: string; 26 | extname: string; 27 | filename: string; 28 | loader?: string; 29 | source?: string; 30 | unitName?: string; 31 | loaderState?: LoaderState; 32 | } 33 | 34 | export interface LoaderFindOptions { 35 | filename: string; 36 | root: string; 37 | baseDir: string; 38 | configDir: string; 39 | policy?: ScanPolicy; 40 | } 41 | 42 | export interface LoaderFindResult { 43 | loaderName: string; 44 | loaderState?: unknown; 45 | } 46 | 47 | export interface LoaderHookUnit { 48 | before?: Function; 49 | after?: Function; 50 | } 51 | 52 | export interface LoaderConstructor { 53 | new(container: Container): Loader; 54 | is?(opts: LoaderFindOptions): Promise; 55 | onFind?(opts: LoaderFindOptions): Promise; 56 | } 57 | export interface Loader { 58 | state?: any; 59 | load(item: ManifestItem): Promise; 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/loader/utils/config_file_meta.ts: -------------------------------------------------------------------------------- 1 | import { ARTUS_DEFAULT_CONFIG_ENV } from '../../constant'; 2 | 3 | export interface ConfigFileMeta { 4 | env: string; 5 | namespace?: string; 6 | } 7 | 8 | export const getConfigMetaFromFilename = (filename: string): ConfigFileMeta => { 9 | let [namespace, env, extname] = filename.split('.'); 10 | if (!extname) { 11 | // No env flag, set to Default 12 | env = ARTUS_DEFAULT_CONFIG_ENV.DEFAULT; 13 | } 14 | const meta: ConfigFileMeta = { 15 | env, 16 | }; 17 | if (namespace !== 'config') { 18 | meta.namespace = namespace; 19 | } 20 | return meta; 21 | }; 22 | -------------------------------------------------------------------------------- /src/loader/utils/merge.ts: -------------------------------------------------------------------------------- 1 | import deepmerge from 'deepmerge'; 2 | import { isPlainObject } from '../../utils/is'; 3 | 4 | export function mergeConfig(...args) { 5 | /* istanbul ignore next */ 6 | return deepmerge.all(args, { 7 | arrayMerge: (_, src) => src, 8 | clone: false, 9 | isMergeableObject: isPlainObject, 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/logger/impl.ts: -------------------------------------------------------------------------------- 1 | import { Container, Inject, Injectable, ScopeEnum } from '@artus/injection'; 2 | import { ArtusInjectEnum } from '../constant'; 3 | import { LoggerLevel, LOGGER_LEVEL_MAP } from './level'; 4 | import { LoggerOptions, LoggerType, LogOptions } from './types'; 5 | 6 | @Injectable({ 7 | scope: ScopeEnum.SINGLETON, 8 | }) 9 | export default class Logger implements LoggerType { 10 | @Inject() 11 | protected container!: Container; 12 | 13 | protected get loggerOpts(): LoggerOptions { 14 | let appConfig: Record = {}; 15 | try { 16 | appConfig = this.container.get(ArtusInjectEnum.Config); 17 | } catch(e) { 18 | // do nothing 19 | } 20 | return appConfig?.logger ?? {}; 21 | } 22 | 23 | protected checkLoggerLevel(level: LoggerLevel) { 24 | const targetLevel = this.loggerOpts.level ?? LoggerLevel.INFO; 25 | if (LOGGER_LEVEL_MAP[level] < LOGGER_LEVEL_MAP[targetLevel]) { 26 | return false; 27 | } 28 | return true; 29 | } 30 | 31 | public trace(message: string, ...args: any[]) { 32 | if (!this.checkLoggerLevel(LoggerLevel.TRACE)) { 33 | return; 34 | } 35 | console.trace(message, ...args); 36 | } 37 | 38 | public debug(message: string, ...args: any[]) { 39 | if (!this.checkLoggerLevel(LoggerLevel.DEBUG)) { 40 | return; 41 | } 42 | console.debug(message, ...args); 43 | } 44 | 45 | public info(message: string, ...args: any[]) { 46 | if (!this.checkLoggerLevel(LoggerLevel.INFO)) { 47 | return; 48 | } 49 | console.info(message, ...args); 50 | } 51 | 52 | public warn(message: string, ...args: any[]) { 53 | if (!this.checkLoggerLevel(LoggerLevel.WARN)) { 54 | return; 55 | } 56 | console.warn(message, ...args); 57 | } 58 | 59 | public error(message: string | Error, ...args: any[]) { 60 | if (!this.checkLoggerLevel(LoggerLevel.ERROR)) { 61 | return; 62 | } 63 | console.error(message, ...args); 64 | } 65 | 66 | public fatal(message: string | Error, ...args: any[]) { 67 | if (!this.checkLoggerLevel(LoggerLevel.FATAL)) { 68 | return; 69 | } 70 | console.error(message, ...args); 71 | } 72 | 73 | public log({ 74 | level, 75 | message, 76 | splat = [], 77 | }: LogOptions) { 78 | if (message instanceof Error) { 79 | if (level === LoggerLevel.ERROR) { 80 | return this.error(message, ...splat); 81 | } 82 | message = message.stack ?? message.message; 83 | } 84 | switch (level) { 85 | case LoggerLevel.TRACE: 86 | this.trace(message, ...splat); 87 | break; 88 | case LoggerLevel.DEBUG: 89 | this.debug(message, ...splat); 90 | break; 91 | case LoggerLevel.INFO: 92 | this.info(message, ...splat); 93 | break; 94 | case LoggerLevel.WARN: 95 | this.warn(message, ...splat); 96 | break; 97 | case LoggerLevel.ERROR: 98 | this.error(message, ...splat); 99 | break; 100 | case LoggerLevel.FATAL: 101 | this.fatal(message, ...splat); 102 | break; 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/logger/index.ts: -------------------------------------------------------------------------------- 1 | import Logger from './impl'; 2 | 3 | export { Logger }; 4 | export * from './level'; 5 | export * from './types'; 6 | -------------------------------------------------------------------------------- /src/logger/level.ts: -------------------------------------------------------------------------------- 1 | export enum LoggerLevel { 2 | TRACE = 'trace', 3 | DEBUG = 'debug', 4 | INFO = 'info', 5 | WARN = 'warn', 6 | ERROR = 'error', 7 | FATAL = 'fatal', 8 | } 9 | 10 | export const LOGGER_LEVEL_MAP = { 11 | [LoggerLevel.TRACE]: 0, 12 | [LoggerLevel.DEBUG]: 1, 13 | [LoggerLevel.INFO]: 2, 14 | [LoggerLevel.WARN]: 3, 15 | [LoggerLevel.ERROR]: 4, 16 | [LoggerLevel.FATAL]: 5, 17 | }; 18 | -------------------------------------------------------------------------------- /src/logger/types.ts: -------------------------------------------------------------------------------- 1 | import { LoggerLevel } from './level'; 2 | 3 | export interface LoggerOptions { 4 | level?: LoggerLevel; 5 | } 6 | 7 | export interface LogOptions { 8 | level: LoggerLevel; 9 | message: string|Error; 10 | splat?: any[]; 11 | } 12 | 13 | export interface LoggerType { 14 | trace(message: string, ...args: any[]): void; 15 | debug(message: string, ...args: any[]): void; 16 | info(message: string, ...args: any[]): void; 17 | warn(message: string, ...args: any[]): void; 18 | error(message: string | Error, ...args: any[]): void; 19 | fatal(message: string | Error, ...args: any[]): void; 20 | 21 | log(opts: LogOptions): void; 22 | } 23 | -------------------------------------------------------------------------------- /src/plugin/common.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import compatibleRequire from "../utils/compatible_require"; 3 | import { PluginType } from "./types"; 4 | import { LoggerType } from "../logger"; 5 | 6 | export function sortPlugins( 7 | pluginInstanceMap: Map, 8 | logger: LoggerType, 9 | ): PluginType[] { 10 | const sortedPlugins: PluginType[] = []; 11 | const visited: Record = {}; 12 | 13 | const visit = (pluginName: string, depChain: string[] = []) => { 14 | if (depChain.includes(pluginName)) { 15 | throw new Error( 16 | `Circular dependency found in plugins: ${depChain.join(", ")}`, 17 | ); 18 | } 19 | 20 | if (visited[pluginName]) return; 21 | 22 | visited[pluginName] = true; 23 | 24 | const plugin = pluginInstanceMap.get(pluginName); 25 | if (plugin) { 26 | for (const dep of plugin.metadata.dependencies ?? []) { 27 | const depPlugin = pluginInstanceMap.get(dep.name); 28 | if (!depPlugin || !depPlugin.enable) { 29 | if (dep.optional) { 30 | logger?.warn( 31 | `Plugin ${plugin.name} need have optional dependency: ${dep.name}.`, 32 | ); 33 | } else { 34 | throw new Error( 35 | `Plugin ${plugin.name} need have dependency: ${dep.name}.`, 36 | ); 37 | } 38 | } else { 39 | // Plugin exist and enabled, need visit 40 | visit(dep.name, depChain.concat(pluginName)); 41 | } 42 | } 43 | sortedPlugins.push(plugin); 44 | } 45 | }; 46 | 47 | for (const pluginName of pluginInstanceMap.keys()) { 48 | visit(pluginName); 49 | } 50 | 51 | return sortedPlugins; 52 | } 53 | 54 | // A util function of get package path for plugin 55 | export function getPackagePath( 56 | packageName: string, 57 | paths: string[] = [], 58 | ): string { 59 | const opts = { 60 | paths: paths.concat(__dirname), 61 | }; 62 | return path.dirname(require.resolve(packageName, opts)); 63 | } 64 | 65 | export async function getInlinePackageEntryPath( 66 | packagePath: string, 67 | ): Promise { 68 | const pkgJson = await compatibleRequire(`${packagePath}/package.json`); 69 | let entryFilePath = ""; 70 | if (pkgJson.exports) { 71 | if (Array.isArray(pkgJson.exports)) { 72 | throw new Error(`inline package multi exports is not supported`); 73 | } else if (typeof pkgJson.exports === "string") { 74 | entryFilePath = pkgJson.exports; 75 | } else if (pkgJson.exports?.["."]) { 76 | entryFilePath = pkgJson.exports["."].require; 77 | } 78 | } 79 | if (!entryFilePath && pkgJson.main) { 80 | entryFilePath = pkgJson.main; 81 | } 82 | // will use package root path if no entry file found 83 | return entryFilePath 84 | ? path.resolve(packagePath, entryFilePath, "..") 85 | : packagePath; 86 | } 87 | -------------------------------------------------------------------------------- /src/plugin/factory.ts: -------------------------------------------------------------------------------- 1 | import { sortPlugins } from './common'; 2 | import { Plugin } from './impl'; 3 | import { PluginConfigItem, PluginCreateOptions, PluginMap, PluginType } from './types'; 4 | 5 | export class PluginFactory { 6 | static async createFromConfig(config: Record, opts?: PluginCreateOptions): Promise { 7 | const pluginInstanceMap: PluginMap = new Map(); 8 | for (const [name, item] of Object.entries(config)) { 9 | if (item.enable) { 10 | const pluginInstance = new Plugin(name, item); 11 | await pluginInstance.init(); 12 | pluginInstanceMap.set(name, pluginInstance); 13 | } 14 | } 15 | return sortPlugins(pluginInstanceMap, opts?.logger); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/plugin/impl.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { loadMetaFile } from '../utils/load_meta_file'; 3 | import { exists } from '../utils/fs'; 4 | import { PLUGIN_META_FILENAME } from '../constant'; 5 | import { PluginConfigItem, PluginMetadata, PluginType } from './types'; 6 | import { getPackagePath } from './common'; 7 | 8 | export class Plugin implements PluginType { 9 | public name: string; 10 | public enable: boolean; 11 | public importPath = ''; 12 | public metadata: Partial; 13 | public metaFilePath = ''; 14 | 15 | constructor(name: string, configItem: PluginConfigItem) { 16 | this.name = name; 17 | this.enable = configItem.enable ?? false; 18 | if (this.enable) { 19 | let importPath = configItem.path ?? ''; 20 | if (configItem.package) { 21 | if (importPath) { 22 | throw new Error(`plugin ${name} config error, package and path can't be set at the same time.`); 23 | } 24 | importPath = getPackagePath(configItem.package); 25 | } 26 | if (!importPath && !configItem.refName) { 27 | throw new Error(`Plugin ${name} need have path or package field`); 28 | } 29 | this.importPath = importPath; 30 | } 31 | if (configItem.metadata) { 32 | this.metadata = configItem.metadata; 33 | } 34 | } 35 | 36 | public async init() { 37 | if (!this.enable) { 38 | return; 39 | } 40 | await this.checkAndLoadMetadata(); 41 | if (!this.metadata) { 42 | throw new Error(`${this.name} is not have metadata.`); 43 | } 44 | if (this.metadata.name !== this.name) { 45 | throw new Error(`${this.name} metadata invalid, name is ${this.metadata.name}`); 46 | } 47 | } 48 | 49 | private async checkAndLoadMetadata() { 50 | // check metadata from configItem 51 | if (this.metadata) { 52 | return; 53 | } 54 | // check import path 55 | if (!await exists(this.importPath)) { 56 | throw new Error(`load plugin <${this.name}> import path ${this.importPath} is not exists.`); 57 | } 58 | const metaFilePath = path.resolve(this.importPath, PLUGIN_META_FILENAME); 59 | try { 60 | if (!await exists(metaFilePath)) { 61 | throw new Error(`load plugin <${this.name}> import path ${this.importPath} can't find meta file.`); 62 | } 63 | this.metadata = await loadMetaFile(metaFilePath); 64 | this.metaFilePath = metaFilePath; 65 | } catch (e) { 66 | throw new Error(`load plugin <${this.name}> failed, err: ${e}`); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/plugin/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './impl'; 3 | export * from './factory'; 4 | -------------------------------------------------------------------------------- /src/plugin/types.ts: -------------------------------------------------------------------------------- 1 | import { LoggerType } from '../logger'; 2 | 3 | export interface PluginCreateOptions { 4 | logger?: LoggerType; 5 | } 6 | 7 | export interface PluginMetadata { 8 | name: string; 9 | dependencies?: PluginDependencyItem[]; 10 | type?: 'simple' | 'module' | string; 11 | configDir?: string 12 | exclude?: string[]; 13 | } 14 | 15 | export interface PluginDependencyItem { 16 | name: string; 17 | optional?: boolean; 18 | 19 | // Only exist on runtime, cannot config in meta.json 20 | _enabled?: boolean; 21 | } 22 | 23 | export interface PluginConfigItem { 24 | enable?: boolean; 25 | path?: string; 26 | package?: string; 27 | refName?: string; 28 | metadata?: PluginMetadata; 29 | } 30 | 31 | export type PluginConfig = Record; 32 | export type PluginMap = Map; 33 | 34 | export interface PluginType { 35 | name: string; 36 | enable: boolean; 37 | importPath: string; 38 | metadata: Partial; 39 | metaFilePath: string; 40 | 41 | init(): Promise; 42 | } 43 | -------------------------------------------------------------------------------- /src/scanner/impl.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { writeFile } from 'fs/promises'; 3 | import { DEFAULT_CONFIG_DIR, DEFAULT_EXCLUDES, DEFAULT_MANIFEST_FILENAME, DEFAULT_MODULE_EXTENSIONS, ScanPolicy } from '../constant'; 4 | import { Manifest } from '../loader'; 5 | import { ScannerOptions, ScannerType } from './types'; 6 | import { ScanTaskRunner } from './task'; 7 | 8 | export class ArtusScanner implements ScannerType { 9 | private options: ScannerOptions; 10 | 11 | constructor(options: Partial = {}) { 12 | this.options = { 13 | needWriteFile: true, 14 | manifestFilePath: DEFAULT_MANIFEST_FILENAME, 15 | useRelativePath: true, 16 | configDir: DEFAULT_CONFIG_DIR, 17 | policy: ScanPolicy.All, 18 | ...options, 19 | exclude: DEFAULT_EXCLUDES.concat(options.exclude ?? []), 20 | extensions: DEFAULT_MODULE_EXTENSIONS.concat(options.extensions ?? []), 21 | }; 22 | } 23 | 24 | /** 25 | * The entrance of Scanner 26 | */ 27 | async scan(root: string): Promise { 28 | // make sure the root path is absolute 29 | if (!path.isAbsolute(root)) { 30 | root = path.resolve(root); 31 | } 32 | 33 | // Init scan-task scanner 34 | const taskRunner = new ScanTaskRunner( 35 | root, 36 | this.options, 37 | ); 38 | 39 | // Start scan 40 | await taskRunner.runAll(); 41 | 42 | // Dump manifest 43 | const manifestResult: Manifest = taskRunner.dump(); 44 | if (this.options.needWriteFile) { 45 | let { manifestFilePath } = this.options; 46 | if (!path.isAbsolute(manifestFilePath)) { 47 | manifestFilePath = path.resolve(root, manifestFilePath); 48 | } 49 | await writeFile( 50 | manifestFilePath, 51 | JSON.stringify(manifestResult, null, 2), 52 | ); 53 | } 54 | 55 | return manifestResult; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/scanner/index.ts: -------------------------------------------------------------------------------- 1 | export * from './impl'; 2 | export * from './types'; 3 | 4 | -------------------------------------------------------------------------------- /src/scanner/task.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import * as fs from 'fs/promises'; 3 | import { ScannerOptions, ScanTaskItem, WalkOptions } from './types'; 4 | import { existsAsync, getPackageVersion, isExclude, isPluginAsync, loadConfigItemList, resolvePluginConfigItemRef } from './utils'; 5 | import { findLoader, Manifest, ManifestItem, RefMap, RefMapItem } from '../loader'; 6 | import { PluginConfig, PluginMetadata } from '../plugin'; 7 | import { loadMetaFile } from '../utils/load_meta_file'; 8 | import { DEFAULT_APP_REF, PLUGIN_META_FILENAME } from '../constant'; 9 | import { Application } from '../types'; 10 | import { ArtusApplication } from '../application'; 11 | 12 | export class ScanTaskRunner { 13 | private waitingTaskMap: Map = new Map(); // Key is pluginName, waiting to detect enabled 14 | private enabledPluginSet: Set = new Set(); // Key is pluginName 15 | private extraPluginConfig: PluginConfig = {}; 16 | private refMap: RefMap = {}; 17 | private taskQueue: ScanTaskItem[] = []; 18 | private app: Application; 19 | 20 | constructor( 21 | private root: string, 22 | private options: ScannerOptions, 23 | ) { 24 | this.app = options.app ?? new ArtusApplication(); 25 | } 26 | 27 | /* 28 | * Handler for walk directories and files recursively 29 | */ 30 | private async walk(curPath: string, options: WalkOptions) { 31 | const { baseDir, configDir } = options; 32 | if (!(await existsAsync(curPath))) { 33 | this.app.logger.warn(`[scan->walk] ${curPath} is not exists.`); 34 | return []; 35 | } 36 | 37 | const stat = await fs.stat(curPath); 38 | if (!stat.isDirectory()) { 39 | return []; 40 | } 41 | 42 | const items = await fs.readdir(curPath); 43 | const itemWalkResult = await Promise.all(items.map(async (item): Promise => { 44 | const realPath = path.resolve(curPath, item); 45 | const extname = path.extname(realPath); 46 | const relativePath = path.relative(baseDir, realPath); 47 | if (isExclude(relativePath, options.exclude, options.extensions)) { 48 | return []; 49 | } 50 | const itemStat = await fs.stat(realPath); 51 | if (itemStat.isDirectory()) { 52 | // ignore plugin dir 53 | if (await isPluginAsync(realPath)) { 54 | return []; 55 | } 56 | return this.walk(realPath, options); 57 | } else if (itemStat.isFile()) { 58 | if (!extname) { 59 | // Exclude file without extname 60 | return []; 61 | } 62 | const filename = path.basename(realPath); 63 | const filenameWithoutExt = path.basename(realPath, extname); 64 | const loaderFindResult = await findLoader({ 65 | filename, 66 | root: curPath, 67 | baseDir, 68 | configDir, 69 | policy: options.policy, 70 | }); 71 | if (!loaderFindResult) { 72 | return []; 73 | } 74 | const { loaderName, loaderState } = loaderFindResult; 75 | const item: ManifestItem = { 76 | path: path.resolve(curPath, filenameWithoutExt), 77 | extname, 78 | filename, 79 | loader: loaderName, 80 | source: options.source, 81 | unitName: options.unitName, 82 | }; 83 | if (loaderState) { 84 | item.loaderState = loaderState; 85 | } 86 | return [item]; 87 | } else { 88 | return []; 89 | } 90 | })); 91 | return itemWalkResult.reduce((itemList, result) => itemList.concat(result), []); 92 | } 93 | 94 | /* 95 | * Handler for pluginConfig object 96 | * Will push new task for plugin, and merge config by env 97 | */ 98 | public async handlePluginConfig( 99 | pluginConfig: PluginConfig, 100 | basePath: string, 101 | ): Promise { 102 | const res: PluginConfig = {}; 103 | for (const [pluginName, pluginConfigItem] of Object.entries(pluginConfig)) { 104 | // Set temp pluginConfig in manifest 105 | res[pluginName] = {}; 106 | if (pluginConfigItem.enable !== undefined) { 107 | res[pluginName].enable = pluginConfigItem.enable; 108 | } 109 | if (pluginConfigItem.enable) { 110 | this.enabledPluginSet.add(pluginName); 111 | } 112 | 113 | // Resolve ref and set 114 | const isPluginEnabled = this.enabledPluginSet.has(pluginName); 115 | const ref = await resolvePluginConfigItemRef(pluginConfigItem, basePath, this.root); 116 | if (!ref?.name) { 117 | continue; 118 | } 119 | res[pluginName].refName = ref.name; 120 | // Generate and push scan task 121 | const curRefTask: ScanTaskItem = { 122 | curPath: ref.path, 123 | refName: ref.name, 124 | isPackage: ref.isPackage, 125 | }; 126 | const waitingTaskList = this.waitingTaskMap.get(pluginName) ?? []; 127 | // Use unshift to make the items later in the list have higher priority 128 | if (isPluginEnabled) { 129 | // Ref need scan immediately and add all waiting task to queue 130 | this.taskQueue.unshift(curRefTask); 131 | for (const waitingTask of waitingTaskList) { 132 | this.taskQueue.unshift(waitingTask); 133 | } 134 | this.waitingTaskMap.delete(pluginName); 135 | } else { 136 | // Need waiting to detect enabled, push refTask to waitingList 137 | waitingTaskList.unshift(curRefTask); 138 | this.waitingTaskMap.set(pluginName, waitingTaskList); 139 | } 140 | } 141 | return res; 142 | } 143 | 144 | 145 | /** 146 | * Handler of single scan task(only a ref) 147 | */ 148 | public async run(taskItem: ScanTaskItem): Promise { 149 | const { curPath = '', refName, isPackage } = taskItem; 150 | let basePath = curPath; 151 | if (!path.isAbsolute(basePath)) { 152 | // basePath must be absolute path 153 | basePath = path.resolve(this.root, curPath); 154 | } 155 | 156 | // pre-scan check for multi-version package 157 | const relativedPath = path.relative(this.root, basePath); 158 | const packageVersion = await getPackageVersion( 159 | (refName === DEFAULT_APP_REF || !isPackage) 160 | ? basePath 161 | : refName, 162 | ); 163 | 164 | if (this.refMap[refName]) { 165 | // Already scanned 166 | if (refName === DEFAULT_APP_REF) { 167 | // No need to check app level 168 | return; 169 | } 170 | const refItem = this.refMap[refName]; 171 | if (refItem.packageVersion && packageVersion && packageVersion !== refItem.packageVersion) { 172 | // Do NOT allow multi-version of plugin package by different version number 173 | throw new Error(`${refName} has multi version of ${packageVersion}, ${refItem.packageVersion}`); 174 | } 175 | if (refItem.relativedPath && relativedPath && relativedPath !== refItem.relativedPath) { 176 | // Do NOT allow multi-version of plugin package by different path 177 | throw new Error(`${refName} has multi path with same version in ${relativedPath} and ${refItem.relativedPath}`); 178 | } 179 | return; 180 | } 181 | 182 | const walkOpts: WalkOptions = { 183 | baseDir: basePath, 184 | configDir: this.options.configDir, 185 | exclude: this.options.exclude, 186 | extensions: this.options.extensions, 187 | policy: this.options.policy, 188 | source: refName === DEFAULT_APP_REF ? 'app' : 'plugin', 189 | unitName: refName, 190 | }; 191 | const refItem: RefMapItem = { 192 | relativedPath, 193 | packageVersion, 194 | pluginConfig: {}, 195 | items: [], 196 | }; 197 | 198 | if (await isPluginAsync(basePath)) { 199 | const metaFilePath = path.resolve(basePath, PLUGIN_META_FILENAME); 200 | const pluginMeta: PluginMetadata = await loadMetaFile(metaFilePath); 201 | walkOpts.configDir = pluginMeta.configDir || walkOpts.configDir; 202 | walkOpts.exclude = walkOpts.exclude.concat(pluginMeta.exclude ?? []); 203 | walkOpts.unitName = pluginMeta.name; 204 | refItem.pluginMetadata = pluginMeta; 205 | } 206 | 207 | refItem.items = await this.walk(basePath, walkOpts); 208 | const configItemList = refItem.items.filter(item => item.loader === 'config'); 209 | const pluginConfigEnvMap = await loadConfigItemList<{ 210 | plugin: PluginConfig; 211 | }>(configItemList, this.app); 212 | for (const [env, configObj] of Object.entries(pluginConfigEnvMap)) { 213 | const pluginConfig = configObj?.plugin; 214 | if (!pluginConfig) { 215 | continue; 216 | } 217 | refItem.pluginConfig[env] = await this.handlePluginConfig(pluginConfig, basePath); 218 | } 219 | 220 | if (this.options.useRelativePath) { 221 | refItem.items = refItem.items.map(item => ({ 222 | ...item, 223 | path: path.relative(this.root, item.path), 224 | })); 225 | } 226 | 227 | this.refMap[refName] = refItem; 228 | } 229 | 230 | public async runAll(): Promise { 231 | // Add Task of options.plugin 232 | if (this.options.plugin) { 233 | this.extraPluginConfig = await this.handlePluginConfig(this.options.plugin, this.root); 234 | } 235 | 236 | // Add Root Task(make it as top/start) 237 | this.taskQueue.unshift({ 238 | curPath: '.', 239 | refName: DEFAULT_APP_REF, 240 | isPackage: false, 241 | }); 242 | 243 | // Run task queue 244 | while (this.taskQueue.length > 0) { 245 | const taskItem = this.taskQueue.shift(); 246 | await this.run(taskItem); 247 | } 248 | } 249 | 250 | public dump(): Manifest { 251 | return { 252 | version: '2', 253 | refMap: this.refMap, 254 | extraPluginConfig: this.extraPluginConfig, 255 | }; 256 | } 257 | } 258 | 259 | -------------------------------------------------------------------------------- /src/scanner/types.ts: -------------------------------------------------------------------------------- 1 | import { Manifest } from '../loader'; 2 | import { PluginConfig } from '../plugin/types'; 3 | import { Application } from '../types'; 4 | import { ScanPolicy } from '../constant'; 5 | 6 | 7 | export interface ScannerOptions { 8 | extensions: string[]; 9 | needWriteFile: boolean; 10 | manifestFilePath?: string; 11 | useRelativePath: boolean; 12 | exclude: string[]; 13 | configDir: string; 14 | policy: ScanPolicy; 15 | envs?: string[]; 16 | plugin?: PluginConfig; 17 | app?: Application; 18 | } 19 | 20 | export interface WalkOptions { 21 | baseDir: string; 22 | configDir: string; 23 | policy: ScanPolicy; 24 | extensions: string[]; 25 | exclude: string[]; 26 | source?: string; 27 | unitName?: string; 28 | } 29 | 30 | export interface LoaderOptions { 31 | root: string; 32 | baseDir: string; 33 | } 34 | 35 | export interface ScannerConstructor { 36 | new(opts?: Partial): ScannerType; 37 | } 38 | 39 | export interface ScannerType { 40 | scan(root: string): Promise; 41 | } 42 | 43 | export interface ScanTaskItem { 44 | curPath: string; 45 | refName: string; 46 | isPackage: boolean; 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/scanner/utils.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs/promises'; 4 | import { isMatch } from '../utils'; 5 | import compatibleRequire from '../utils/compatible_require'; 6 | import { PLUGIN_META_FILENAME } from '../constant'; 7 | import { PluginConfigItem } from '../plugin'; 8 | import { getInlinePackageEntryPath, getPackagePath } from '../plugin/common'; 9 | import { Application } from '../types'; 10 | import { ManifestItem } from '../loader'; 11 | import { ConfigObject } from '../configuration'; 12 | 13 | export const getPackageVersion = async (basePath: string): Promise => { 14 | try { 15 | const packageJsonPath = path.resolve(basePath, 'package.json'); 16 | const packageJson = await compatibleRequire(packageJsonPath); 17 | return packageJson?.version; 18 | } catch (error) { 19 | return undefined; 20 | } 21 | }; 22 | 23 | export const existsAsync = async (filePath: string): Promise => { 24 | try { 25 | await fs.access(filePath); 26 | return true; 27 | } catch (error) { 28 | return false; 29 | } 30 | }; 31 | 32 | export const isExclude = (targetPath: string, exclude: string[], extensions: string[]): boolean => { 33 | let result = false; 34 | if (!result && exclude) { 35 | result = isMatch(targetPath, exclude, true); 36 | } 37 | 38 | const extname = path.extname(targetPath); 39 | if (!result && extname) { 40 | result = !extensions.includes(extname); 41 | } 42 | return result; 43 | }; 44 | 45 | export const isPluginAsync = (basePath: string): Promise => { 46 | return existsAsync(path.resolve(basePath, PLUGIN_META_FILENAME)); 47 | }; 48 | 49 | export const loadConfigItemList = async (configItemList: ManifestItem[], app: Application): Promise> => { 50 | if (!configItemList.length) { 51 | return {}; 52 | } 53 | 54 | // Use temp Map to store config 55 | const configEnvMap: Record = {}; 56 | const stashedConfigStore = app.configurationHandler.configStore; 57 | app.configurationHandler.configStore = configEnvMap; 58 | 59 | // Load all config items without hook 60 | const enabledLifecycleManager = app.lifecycleManager.enable; 61 | app.lifecycleManager.enable = false; 62 | await app.loaderFactory.loadItemList(configItemList); 63 | app.lifecycleManager.enable = enabledLifecycleManager; 64 | 65 | // Restore config store 66 | app.configurationHandler.configStore = stashedConfigStore; 67 | 68 | return configEnvMap; 69 | }; 70 | 71 | export const resolvePluginConfigItemRef = async ( 72 | pluginConfigItem: PluginConfigItem, 73 | baseDir: string, 74 | root: string, 75 | ): Promise<{ 76 | name: string; 77 | path: string; 78 | isPackage: boolean; 79 | } | null> => { 80 | if (pluginConfigItem.refName) { 81 | // For Unit-test 82 | return { 83 | name: pluginConfigItem.refName, 84 | path: pluginConfigItem.path ?? pluginConfigItem.refName, 85 | isPackage: !!pluginConfigItem.package, 86 | }; 87 | } 88 | if (pluginConfigItem.package) { 89 | const refPath = getPackagePath(pluginConfigItem.package, [baseDir]); 90 | return { 91 | name: pluginConfigItem.package, 92 | path: refPath, 93 | isPackage: true, 94 | }; 95 | } else if (pluginConfigItem.path) { 96 | const refName = path.isAbsolute(pluginConfigItem.path) ? path.relative(root, pluginConfigItem.path) : pluginConfigItem.path; 97 | let refPath = refName; 98 | 99 | const packageJsonPath = path.resolve(pluginConfigItem.path, 'package.json'); 100 | if (await existsAsync(packageJsonPath)) { 101 | refPath = await getInlinePackageEntryPath(pluginConfigItem.path); 102 | } 103 | return { 104 | name: refName, 105 | path: refPath, 106 | isPackage: false, 107 | }; 108 | } 109 | return null; 110 | }; 111 | 112 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Container } from '@artus/injection'; 2 | import ConfigurationHandler, { ConfigObject } from './configuration'; 3 | import { HookFunction, LifecycleManager } from './lifecycle'; 4 | import { LoaderFactory, Manifest } from './loader'; 5 | import { LoggerType } from './logger'; 6 | 7 | export interface ApplicationLifecycle { 8 | configWillLoad?: HookFunction; 9 | configDidLoad?: HookFunction; 10 | didLoad?: HookFunction; 11 | willReady?: HookFunction; 12 | didReady?: HookFunction; 13 | beforeClose?: HookFunction; 14 | } 15 | 16 | export interface ApplicationInitOptions { 17 | containerName?: string; 18 | env: string | string[]; 19 | } 20 | 21 | export interface Application { 22 | container: Container; 23 | 24 | manifest?: Manifest; 25 | 26 | // getter 27 | config: ConfigObject; 28 | configurationHandler: ConfigurationHandler; 29 | lifecycleManager: LifecycleManager; 30 | loaderFactory: LoaderFactory; 31 | logger: LoggerType 32 | 33 | load(manifest: Manifest): Promise; 34 | run(): Promise; 35 | registerHook(hookName: string, hookFn: HookFunction): void; 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/utils/compatible_require.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import * as tslib from 'tslib'; 3 | 4 | /** 5 | * compatible esModule require 6 | * @param path 7 | */ 8 | export default async function compatibleRequire(path: string, origin = false): Promise { 9 | if (path.endsWith('.json')) { 10 | return require(path); 11 | } 12 | 13 | let requiredModule; 14 | try { 15 | /* eslint-disable-next-line @typescript-eslint/no-var-requires */ 16 | requiredModule = tslib.__importStar(require(path)); 17 | assert(requiredModule, `module '${path}' exports is undefined`); 18 | } catch (err) { 19 | if (err.code === 'ERR_REQUIRE_ESM') { 20 | requiredModule = await import(path); 21 | assert(requiredModule, `module '${path}' exports is undefined`); 22 | requiredModule = requiredModule.__esModule ? requiredModule.default ?? requiredModule : requiredModule; 23 | } else { 24 | throw err; 25 | } 26 | } 27 | 28 | return origin ? requiredModule : (requiredModule.default || requiredModule); 29 | } -------------------------------------------------------------------------------- /src/utils/fs.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | 3 | export async function exists(path: string): Promise { 4 | try { 5 | await fs.access(path); 6 | } catch { 7 | return false; 8 | } 9 | return true; 10 | } -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import minimatch from 'minimatch'; 2 | 3 | export function getDefaultExtensions() { 4 | return Object.keys(require.extensions); 5 | } 6 | 7 | export function isMatch(filename: string, patterns: string | string[], matchBase = false) { 8 | if (!Array.isArray(patterns)) { 9 | patterns = [patterns]; 10 | } 11 | return patterns.some(pattern => minimatch(filename, pattern, { matchBase })); 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | export function isString(arg: any): boolean { 2 | return typeof arg === 'string'; 3 | } 4 | 5 | export function isFunction(arg: any): boolean { 6 | return typeof arg === 'function'; 7 | } 8 | 9 | export function isPromise(arg: any): boolean { 10 | return arg && 'function' === typeof arg.then; 11 | } 12 | 13 | export function isClass(arg: any): boolean { 14 | if (typeof arg !== 'function') { 15 | return false; 16 | } 17 | 18 | const fnStr = Function.prototype.toString.call(arg); 19 | 20 | return ( 21 | fnStr.substring(0, 5) === 'class' || 22 | Boolean(~fnStr.indexOf('classCallCheck(')) || 23 | Boolean( 24 | ~fnStr.indexOf('TypeError("Cannot call a class as a function")'), 25 | ) 26 | ); 27 | } 28 | 29 | export function isObject(arg: any): boolean { 30 | return arg !== null && typeof arg === 'object'; 31 | } 32 | 33 | export function isObjectObject(o) { 34 | return ( 35 | isObject(o) && 36 | Object.prototype.toString.call(o) === '[object Object]' 37 | ); 38 | } 39 | 40 | export function isPlainObject(o) { 41 | if (!isObjectObject(o)) { 42 | return false; 43 | } 44 | 45 | // If has modified constructor 46 | const ctor = o.constructor; 47 | if (typeof ctor !== 'function') { 48 | return false; 49 | } 50 | 51 | // If has modified prototype 52 | const prot = ctor.prototype; 53 | if (!isObjectObject(prot)) { 54 | return false; 55 | } 56 | 57 | // If constructor does not have an Object-specific method 58 | if (!prot.hasOwnProperty('isPrototypeOf')) { 59 | return false; 60 | } 61 | 62 | // Most likely a plain Object 63 | return true; 64 | } 65 | -------------------------------------------------------------------------------- /src/utils/load_meta_file.ts: -------------------------------------------------------------------------------- 1 | import compatibleRequire from './compatible_require'; 2 | 3 | export const loadMetaFile = async >(path: string): Promise => { 4 | const metaObject = await compatibleRequire(path); 5 | if (!metaObject || typeof metaObject !== 'object') { 6 | throw new Error(`[loadMetaFile] ${path} is not a valid json file.`); 7 | } 8 | return metaObject; 9 | }; 10 | -------------------------------------------------------------------------------- /test/__snapshots__/scanner.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`test/scanner.test.ts should be scan application 1`] = ` 4 | { 5 | "extraPluginConfig": {}, 6 | "refMap": { 7 | "@artus/injection": { 8 | "items": [], 9 | "packageVersion": undefined, 10 | "pluginConfig": {}, 11 | "relativedPath": "../../../node_modules/@artus/injection/lib", 12 | }, 13 | "_app": { 14 | "items": [ 15 | { 16 | "extname": ".ts", 17 | "filename": "config.default.ts", 18 | "loader": "config", 19 | "loaderState": { 20 | "exportNames": [], 21 | }, 22 | "path": "src/config/config.default", 23 | "source": "app", 24 | "unitName": "_app", 25 | }, 26 | { 27 | "extname": ".ts", 28 | "filename": "config.dev.ts", 29 | "loader": "config", 30 | "loaderState": { 31 | "exportNames": [], 32 | }, 33 | "path": "src/config/config.dev", 34 | "source": "app", 35 | "unitName": "_app", 36 | }, 37 | { 38 | "extname": ".ts", 39 | "filename": "plugin.default.ts", 40 | "loader": "config", 41 | "loaderState": { 42 | "exportNames": [], 43 | }, 44 | "path": "src/config/plugin.default", 45 | "source": "app", 46 | "unitName": "_app", 47 | }, 48 | { 49 | "extname": ".ts", 50 | "filename": "plugin.dev.ts", 51 | "loader": "config", 52 | "loaderState": { 53 | "exportNames": [], 54 | }, 55 | "path": "src/config/plugin.dev", 56 | "source": "app", 57 | "unitName": "_app", 58 | }, 59 | { 60 | "extname": ".ts", 61 | "filename": "hello.ts", 62 | "loader": "module", 63 | "loaderState": { 64 | "exportNames": [ 65 | "default", 66 | ], 67 | }, 68 | "path": "src/controllers/hello", 69 | "source": "app", 70 | "unitName": "_app", 71 | }, 72 | { 73 | "extname": ".json", 74 | "filename": "exception.json", 75 | "loader": "exception", 76 | "loaderState": { 77 | "exportNames": [], 78 | }, 79 | "path": "src/exception", 80 | "source": "app", 81 | "unitName": "_app", 82 | }, 83 | { 84 | "extname": ".ts", 85 | "filename": "default.ts", 86 | "loader": "exception-filter", 87 | "loaderState": { 88 | "exportNames": [ 89 | "MockExceptionFilter", 90 | ], 91 | }, 92 | "path": "src/filter/default", 93 | "source": "app", 94 | "unitName": "_app", 95 | }, 96 | { 97 | "extname": ".ts", 98 | "filename": "koa_app.ts", 99 | "loader": "module", 100 | "loaderState": { 101 | "exportNames": [ 102 | "default", 103 | ], 104 | }, 105 | "path": "src/koa_app", 106 | "source": "app", 107 | "unitName": "_app", 108 | }, 109 | { 110 | "extname": ".ts", 111 | "filename": "lifecycle.ts", 112 | "loader": "lifecycle-hook-unit", 113 | "loaderState": { 114 | "exportNames": [ 115 | "default", 116 | ], 117 | }, 118 | "path": "src/lifecycle", 119 | "source": "app", 120 | "unitName": "_app", 121 | }, 122 | { 123 | "extname": ".ts", 124 | "filename": "hello.ts", 125 | "loader": "module", 126 | "loaderState": { 127 | "exportNames": [ 128 | "default", 129 | ], 130 | }, 131 | "path": "src/services/hello", 132 | "source": "app", 133 | "unitName": "_app", 134 | }, 135 | ], 136 | "packageVersion": undefined, 137 | "pluginConfig": { 138 | "default": { 139 | "mysql": { 140 | "enable": false, 141 | "refName": "src/mysql_plugin", 142 | }, 143 | "redis": { 144 | "enable": true, 145 | "refName": "src/redis_plugin", 146 | }, 147 | "testDuplicate": { 148 | "enable": false, 149 | "refName": "@artus/injection", 150 | }, 151 | }, 152 | "dev": { 153 | "testDuplicate": { 154 | "enable": true, 155 | "refName": "src/test_duplicate_plugin", 156 | }, 157 | }, 158 | }, 159 | "relativedPath": "", 160 | }, 161 | "src/redis_plugin": { 162 | "items": [ 163 | { 164 | "extname": ".ts", 165 | "filename": "app.ts", 166 | "loader": "lifecycle-hook-unit", 167 | "loaderState": { 168 | "exportNames": [ 169 | "default", 170 | ], 171 | }, 172 | "path": "src/redis_plugin/app", 173 | "source": "plugin", 174 | "unitName": "redis", 175 | }, 176 | ], 177 | "packageVersion": undefined, 178 | "pluginConfig": {}, 179 | "pluginMetadata": { 180 | "exclude": [ 181 | "not_to_be_scanned_dir", 182 | "not_to_be_scanned_file.ts", 183 | ], 184 | "name": "redis", 185 | }, 186 | "relativedPath": "src/redis_plugin", 187 | }, 188 | "src/test_duplicate_plugin": { 189 | "items": [], 190 | "packageVersion": undefined, 191 | "pluginConfig": {}, 192 | "pluginMetadata": { 193 | "name": "testDuplicate", 194 | }, 195 | "relativedPath": "src/test_duplicate_plugin", 196 | }, 197 | }, 198 | "version": "2", 199 | } 200 | `; 201 | 202 | exports[`test/scanner.test.ts should scan application with nesting preset a which defined in options 1`] = ` 203 | { 204 | "extraPluginConfig": { 205 | "preset_a": { 206 | "enable": true, 207 | "refName": "../plugins/preset_a", 208 | }, 209 | }, 210 | "refMap": { 211 | "../plugins/plugin_a": { 212 | "items": [], 213 | "packageVersion": "0.0.1", 214 | "pluginConfig": {}, 215 | "pluginMetadata": { 216 | "dependencies": [ 217 | { 218 | "name": "plugin-b", 219 | }, 220 | { 221 | "name": "plugin-c", 222 | "optional": true, 223 | }, 224 | ], 225 | "name": "plugin-a", 226 | }, 227 | "relativedPath": "../plugins/plugin_a", 228 | }, 229 | "../plugins/plugin_b": { 230 | "items": [], 231 | "packageVersion": undefined, 232 | "pluginConfig": {}, 233 | "pluginMetadata": { 234 | "dependencies": [ 235 | { 236 | "name": "plugin-c", 237 | }, 238 | ], 239 | "name": "plugin-b", 240 | }, 241 | "relativedPath": "../plugins/plugin_b", 242 | }, 243 | "../plugins/plugin_d": { 244 | "items": [], 245 | "packageVersion": undefined, 246 | "pluginConfig": {}, 247 | "pluginMetadata": { 248 | "dependencies": [ 249 | { 250 | "name": "plugin-c", 251 | "optional": true, 252 | }, 253 | ], 254 | "name": "plugin-d", 255 | }, 256 | "relativedPath": "../plugins/plugin_d", 257 | }, 258 | "../plugins/plugin_with_entry_a": { 259 | "items": [], 260 | "packageVersion": undefined, 261 | "pluginConfig": {}, 262 | "pluginMetadata": { 263 | "name": "plugin-with-entry-a", 264 | }, 265 | "relativedPath": "../plugins/plugin_with_entry_a/mock_lib", 266 | }, 267 | "../plugins/plugin_with_entry_b": { 268 | "items": [], 269 | "packageVersion": undefined, 270 | "pluginConfig": {}, 271 | "pluginMetadata": { 272 | "name": "plugin-with-entry-b", 273 | }, 274 | "relativedPath": "../plugins/plugin_with_entry_b/mock_lib", 275 | }, 276 | "../plugins/preset_a": { 277 | "items": [ 278 | { 279 | "extname": ".ts", 280 | "filename": "plugin.default.ts", 281 | "loader": "config", 282 | "loaderState": { 283 | "exportNames": [], 284 | }, 285 | "path": "../plugins/preset_a/config/plugin.default", 286 | "source": "plugin", 287 | "unitName": "preset_a", 288 | }, 289 | ], 290 | "packageVersion": undefined, 291 | "pluginConfig": { 292 | "default": { 293 | "a": { 294 | "enable": false, 295 | }, 296 | "plugin-with-entry-a": { 297 | "enable": true, 298 | "refName": "../plugins/plugin_with_entry_a", 299 | }, 300 | "preset_b": { 301 | "enable": true, 302 | "refName": "../plugins/preset_b", 303 | }, 304 | "preset_c": { 305 | "enable": true, 306 | "refName": "../plugins/preset_c", 307 | }, 308 | }, 309 | }, 310 | "pluginMetadata": { 311 | "name": "preset_a", 312 | }, 313 | "relativedPath": "../plugins/preset_a", 314 | }, 315 | "../plugins/preset_b": { 316 | "items": [ 317 | { 318 | "extname": ".ts", 319 | "filename": "plugin.default.ts", 320 | "loader": "config", 321 | "loaderState": { 322 | "exportNames": [], 323 | }, 324 | "path": "../plugins/preset_b/config/plugin.default", 325 | "source": "plugin", 326 | "unitName": "preset_b", 327 | }, 328 | ], 329 | "packageVersion": undefined, 330 | "pluginConfig": { 331 | "default": { 332 | "a": { 333 | "enable": true, 334 | "refName": "../plugins/plugin_a", 335 | }, 336 | "b": { 337 | "enable": true, 338 | "refName": "../plugins/plugin_b", 339 | }, 340 | "plugin-with-entry-b": { 341 | "enable": true, 342 | "refName": "../plugins/plugin_with_entry_b", 343 | }, 344 | }, 345 | }, 346 | "pluginMetadata": { 347 | "name": "preset_b", 348 | }, 349 | "relativedPath": "../plugins/preset_b", 350 | }, 351 | "../plugins/preset_c": { 352 | "items": [ 353 | { 354 | "extname": ".ts", 355 | "filename": "plugin.default.ts", 356 | "loader": "config", 357 | "loaderState": { 358 | "exportNames": [], 359 | }, 360 | "path": "../plugins/preset_c/config/plugin.default", 361 | "source": "plugin", 362 | "unitName": "preset_c", 363 | }, 364 | ], 365 | "packageVersion": undefined, 366 | "pluginConfig": { 367 | "default": { 368 | "b": { 369 | "enable": false, 370 | "refName": "../plugins/plugin_b", 371 | }, 372 | "c": { 373 | "enable": false, 374 | "refName": "../plugins/plugin_c", 375 | }, 376 | "d": { 377 | "enable": true, 378 | "refName": "../plugins/plugin_d", 379 | }, 380 | "plugin-with-entry-c": { 381 | "enable": false, 382 | "refName": "../plugins/plugin_with_entry_c", 383 | }, 384 | }, 385 | }, 386 | "pluginMetadata": { 387 | "name": "preset_c", 388 | }, 389 | "relativedPath": "../plugins/preset_c", 390 | }, 391 | "_app": { 392 | "items": [], 393 | "packageVersion": undefined, 394 | "pluginConfig": {}, 395 | "relativedPath": "", 396 | }, 397 | }, 398 | "version": "2", 399 | } 400 | `; 401 | 402 | exports[`test/scanner.test.ts should scan application with single preset b which defined in config 1`] = ` 403 | { 404 | "extraPluginConfig": {}, 405 | "refMap": { 406 | "../plugins/plugin_a": { 407 | "items": [], 408 | "packageVersion": "0.0.1", 409 | "pluginConfig": {}, 410 | "pluginMetadata": { 411 | "dependencies": [ 412 | { 413 | "name": "plugin-b", 414 | }, 415 | { 416 | "name": "plugin-c", 417 | "optional": true, 418 | }, 419 | ], 420 | "name": "plugin-a", 421 | }, 422 | "relativedPath": "../plugins/plugin_a", 423 | }, 424 | "../plugins/plugin_b": { 425 | "items": [], 426 | "packageVersion": undefined, 427 | "pluginConfig": {}, 428 | "pluginMetadata": { 429 | "dependencies": [ 430 | { 431 | "name": "plugin-c", 432 | }, 433 | ], 434 | "name": "plugin-b", 435 | }, 436 | "relativedPath": "../plugins/plugin_b", 437 | }, 438 | "../plugins/plugin_with_entry_b": { 439 | "items": [], 440 | "packageVersion": undefined, 441 | "pluginConfig": {}, 442 | "pluginMetadata": { 443 | "name": "plugin-with-entry-b", 444 | }, 445 | "relativedPath": "../plugins/plugin_with_entry_b/mock_lib", 446 | }, 447 | "../plugins/preset_b": { 448 | "items": [ 449 | { 450 | "extname": ".ts", 451 | "filename": "plugin.default.ts", 452 | "loader": "config", 453 | "loaderState": { 454 | "exportNames": [], 455 | }, 456 | "path": "../plugins/preset_b/config/plugin.default", 457 | "source": "plugin", 458 | "unitName": "preset_b", 459 | }, 460 | ], 461 | "packageVersion": undefined, 462 | "pluginConfig": { 463 | "default": { 464 | "a": { 465 | "enable": true, 466 | "refName": "../plugins/plugin_a", 467 | }, 468 | "b": { 469 | "enable": true, 470 | "refName": "../plugins/plugin_b", 471 | }, 472 | "plugin-with-entry-b": { 473 | "enable": true, 474 | "refName": "../plugins/plugin_with_entry_b", 475 | }, 476 | }, 477 | }, 478 | "pluginMetadata": { 479 | "name": "preset_b", 480 | }, 481 | "relativedPath": "../plugins/preset_b", 482 | }, 483 | "_app": { 484 | "items": [ 485 | { 486 | "extname": ".ts", 487 | "filename": "plugin.default.ts", 488 | "loader": "config", 489 | "loaderState": { 490 | "exportNames": [], 491 | }, 492 | "path": "config/plugin.default", 493 | "source": "app", 494 | "unitName": "_app", 495 | }, 496 | ], 497 | "packageVersion": undefined, 498 | "pluginConfig": { 499 | "default": { 500 | "preset_b": { 501 | "enable": true, 502 | "refName": "../plugins/preset_b", 503 | }, 504 | }, 505 | }, 506 | "relativedPath": "", 507 | }, 508 | }, 509 | "version": "2", 510 | } 511 | `; 512 | 513 | exports[`test/scanner.test.ts should scan application with single preset c which defined in options 1`] = ` 514 | { 515 | "extraPluginConfig": { 516 | "preset_c": { 517 | "enable": true, 518 | "refName": "../plugins/preset_c", 519 | }, 520 | }, 521 | "refMap": { 522 | "../plugins/plugin_d": { 523 | "items": [], 524 | "packageVersion": undefined, 525 | "pluginConfig": {}, 526 | "pluginMetadata": { 527 | "dependencies": [ 528 | { 529 | "name": "plugin-c", 530 | "optional": true, 531 | }, 532 | ], 533 | "name": "plugin-d", 534 | }, 535 | "relativedPath": "../plugins/plugin_d", 536 | }, 537 | "../plugins/preset_c": { 538 | "items": [ 539 | { 540 | "extname": ".ts", 541 | "filename": "plugin.default.ts", 542 | "loader": "config", 543 | "loaderState": { 544 | "exportNames": [], 545 | }, 546 | "path": "../plugins/preset_c/config/plugin.default", 547 | "source": "plugin", 548 | "unitName": "preset_c", 549 | }, 550 | ], 551 | "packageVersion": undefined, 552 | "pluginConfig": { 553 | "default": { 554 | "b": { 555 | "enable": false, 556 | "refName": "../plugins/plugin_b", 557 | }, 558 | "c": { 559 | "enable": false, 560 | "refName": "../plugins/plugin_c", 561 | }, 562 | "d": { 563 | "enable": true, 564 | "refName": "../plugins/plugin_d", 565 | }, 566 | "plugin-with-entry-c": { 567 | "enable": false, 568 | "refName": "../plugins/plugin_with_entry_c", 569 | }, 570 | }, 571 | }, 572 | "pluginMetadata": { 573 | "name": "preset_c", 574 | }, 575 | "relativedPath": "../plugins/preset_c", 576 | }, 577 | "_app": { 578 | "items": [], 579 | "packageVersion": undefined, 580 | "pluginConfig": {}, 581 | "relativedPath": "", 582 | }, 583 | }, 584 | "version": "2", 585 | } 586 | `; 587 | -------------------------------------------------------------------------------- /test/app.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import axios from 'axios'; 3 | import assert from 'assert'; 4 | import { ArtusApplication, ArtusInjectEnum, ConfigurationHandler, LifecycleManager, LoaderFactory } from '../src'; 5 | 6 | describe('test/app.test.ts', () => { 7 | describe('app koa with ts', () => { 8 | it('should run app', async () => { 9 | // Skip Controller 10 | const HelloController = require('./fixtures/app_koa_with_ts/src/controllers/hello'); 11 | assert(HelloController); 12 | expect(await new HelloController.default().index()).toStrictEqual({ 13 | content: 'Hello Artus', 14 | status: 200, 15 | headers: {}, 16 | }); 17 | 18 | try { 19 | const { 20 | app, 21 | main, 22 | isListening, 23 | } = require('./fixtures/app_koa_with_ts/src/bootstrap'); 24 | 25 | // Check Artus Default Class Inject to Contianer 26 | expect(app.container.get(ArtusInjectEnum.Application)).toBeInstanceOf(ArtusApplication); 27 | expect(app.container.get(LifecycleManager)).toBeInstanceOf(LifecycleManager); 28 | expect(app.container.get(LoaderFactory)).toBeInstanceOf(LoaderFactory); 29 | expect(app.container.get(ConfigurationHandler)).toBeInstanceOf(ConfigurationHandler); 30 | 31 | await main(); 32 | 33 | const testResponse = await axios.get('http://127.0.0.1:3000', { 34 | headers: { 35 | 'x-hello-artus': 'true', 36 | }, 37 | }); 38 | expect(testResponse.status).toBe(200); 39 | expect(testResponse.data).toBe('Hello Artus'); 40 | expect(testResponse.headers?.['x-hello-artus']).toBe('true'); 41 | await app.close(); 42 | expect(isListening()).toBeFalsy(); 43 | } catch (error) { 44 | throw error; 45 | } 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/config.test.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { ARTUS_SERVER_ENV } from "../src/constant"; 3 | 4 | describe("test/config.test.ts", () => { 5 | describe("app with config", () => { 6 | it("should config load on application", async () => { 7 | process.env[ARTUS_SERVER_ENV] = "production"; 8 | const { main } = require("./fixtures/app_with_config/bootstrap"); 9 | const app = await main(); 10 | expect(app.config).toEqual({ 11 | name: "test-for-config", 12 | plugin: {}, 13 | test: 1, 14 | arr: [4, 5, 6], 15 | }); 16 | process.env[ARTUS_SERVER_ENV] = undefined; 17 | }); 18 | }); 19 | 20 | 21 | describe("app with manifest should load config ok", () => { 22 | it("should load config ok", async () => { 23 | const { main } = require("./fixtures/app_with_manifest/bootstrap"); 24 | const app = await main(); 25 | expect(app.config).toEqual({ httpConfig: { key1: 'value1', port: 3000 }, plugin: {} }); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/exception.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import assert from 'assert'; 3 | import { ArtusStdError } from '../src/exception'; 4 | import { ExceptionItem } from '../src/exception/types'; 5 | 6 | describe('test/app.test.ts', () => { 7 | describe('register error code and throw', () => { 8 | const errorCode = 'ARTUS:TEMP_TEST'; 9 | const exceptionItem: ExceptionItem = { 10 | desc: 'TEST-DESC', 11 | detailUrl: 'http://test.artusjs.org', 12 | }; 13 | ArtusStdError.registerCode(errorCode, exceptionItem); 14 | try { 15 | throw new ArtusStdError(errorCode); 16 | } catch (error) { 17 | assert(error instanceof ArtusStdError); 18 | assert(error.code === errorCode); 19 | assert(error.desc === exceptionItem.desc); 20 | assert(error.detailUrl === exceptionItem.detailUrl); 21 | } 22 | const error = new ArtusStdError(errorCode); 23 | assert(error instanceof ArtusStdError); 24 | assert(error.code === errorCode); 25 | assert(error.desc === exceptionItem.desc); 26 | assert(error.detailUrl === exceptionItem.detailUrl); 27 | 28 | try { 29 | throw new ArtusStdError('UNKNWON_CODE'); 30 | } catch (error) { 31 | assert(error instanceof ArtusStdError); 32 | assert(error.code === 'UNKNWON_CODE'); 33 | assert(error.desc === 'Unknown Error'); 34 | assert(error.detailUrl === undefined); 35 | } 36 | }); 37 | 38 | describe('register error code and throw, with i18n', () => { 39 | const errorCode = 'ARTUS:TEMP_TEST_I18N'; 40 | const exceptionItem: ExceptionItem = { 41 | desc: { 42 | zh: 'TEST-DESC-ZH', 43 | en: 'TEST-DESC-EN', 44 | }, 45 | detailUrl: 'http://test.artusjs.org', 46 | }; 47 | ArtusStdError.registerCode(errorCode, exceptionItem); 48 | [ 49 | undefined, 50 | 'zh', 51 | 'en', 52 | ].forEach(locale => { 53 | if (locale) { 54 | ArtusStdError.setCurrentLocale(locale); 55 | } 56 | const tDesc = exceptionItem.desc[locale || 'en']; 57 | try { 58 | throw new ArtusStdError(errorCode); 59 | } catch (error) { 60 | assert(error instanceof ArtusStdError); 61 | assert(error.code === errorCode); 62 | assert(error.desc === tDesc); 63 | assert(error.detailUrl === exceptionItem.detailUrl); 64 | } 65 | const error = new ArtusStdError(errorCode); 66 | assert(error instanceof ArtusStdError); 67 | assert(error.code === errorCode); 68 | assert(error.desc === tDesc); 69 | assert(error.detailUrl === exceptionItem.detailUrl); 70 | }); 71 | }); 72 | 73 | describe('app test for ts and yaml', () => { 74 | it('should run app', async () => { 75 | try { 76 | const { 77 | main, 78 | } = require('./fixtures/exception_with_ts_yaml/bootstrap'); 79 | const app = await main(); 80 | 81 | try { 82 | app.throwException('ARTUS:GLOBAL_TEST'); 83 | } catch (error) { 84 | expect(error).toBeInstanceOf(ArtusStdError); 85 | expect(error.code).toBe('ARTUS:GLOBAL_TEST'); 86 | expect(error.desc).toBe('全局测试错误,仅用于单元测试'); 87 | expect(error.detailUrl).toBe('https://github.com/artusjs/spec'); 88 | } 89 | try { 90 | app.throwException('APP:TEST_ERROR'); 91 | } catch (error) { 92 | expect(error).toBeInstanceOf(ArtusStdError); 93 | expect(error.code).toBe('APP:TEST_ERROR'); 94 | expect(error.desc).toBe('这是一个测试用的错误'); 95 | expect(error.detailUrl).toBe('https://github.com/artusjs'); 96 | } 97 | try { 98 | process.env.ARTUS_ERROR_LOCALE = 'en'; 99 | app.throwException('ARTUS:GLOBAL_TEST_I18N'); 100 | } catch (error) { 101 | expect(error).toBeInstanceOf(ArtusStdError); 102 | expect(error.code).toBe('ARTUS:GLOBAL_TEST_I18N'); 103 | expect(error.desc).toBe('This is a test exception, only valid in unit-test'); 104 | expect(error.detailUrl).toBe('https://github.com/artusjs/spec'); 105 | } 106 | 107 | const error = app.createException('ARTUS:GLOBAL_TEST'); 108 | expect(error).toBeInstanceOf(ArtusStdError); 109 | expect(error.code).toBe('ARTUS:GLOBAL_TEST'); 110 | expect(error.desc).toBe('全局测试错误,仅用于单元测试'); 111 | expect(error.detailUrl).toBe('https://github.com/artusjs/spec'); 112 | 113 | await app.close(); 114 | } catch (error) { 115 | console.error(error); 116 | throw error; 117 | } 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /test/exception_filter.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { ArtusStdError, matchExceptionFilter } from '../src'; 3 | import MockErrorService from './fixtures/exception_filter/service'; 4 | 5 | describe('test/exception_filter.test.ts', () => { 6 | it('a standard exception catch logic with no filter', async () => { 7 | try { 8 | try { 9 | throw new ArtusStdError('TEST'); 10 | } catch (error) { 11 | expect(error).toBeInstanceOf(ArtusStdError); 12 | } 13 | } catch (error) { 14 | throw error; 15 | } 16 | }); 17 | it('exception should pass their filter', async () => { 18 | try { 19 | const { 20 | main, 21 | } = require('./fixtures/exception_filter/bootstrap'); 22 | 23 | const app = await main(); 24 | const mockSet: Set = app.container.get('mock_exception_set'); 25 | for (const [inputTarget, exceptedVal] of [ 26 | ['default', 'Error'], 27 | ['custom', 'TestCustomError'], 28 | ['wrapped', 'APP:WRAPPED_ERROR'], 29 | ['APP:TEST_ERROR', 'APP:TEST_ERROR'], 30 | ['inherit', 'TestInheritError'], 31 | ['defaultInherit', 'TestDefaultInheritError'], 32 | ]) { 33 | const mockErrorService = app.container.get(MockErrorService); 34 | try { 35 | mockErrorService.throw(inputTarget); 36 | } catch (error) { 37 | const filter = matchExceptionFilter(error, app.container); 38 | await filter.catch(error); 39 | } 40 | expect(mockSet.has(exceptedVal)).toBeTruthy(); 41 | } 42 | } catch (error) { 43 | throw error; 44 | } 45 | }); 46 | it('should throw error then filter is invalid', async () => { 47 | try { 48 | const { 49 | main, 50 | } = require('./fixtures/exception_invalid_filter/bootstrap'); 51 | 52 | expect(() => main()).rejects.toThrow(new Error(`invalid ExceptionFilter TestInvalidFilter`)); 53 | } catch (error) { 54 | throw error; 55 | } 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/fixtures/app_empty/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artusjs/core/b942d5095c8e9a98194a82509120bdda7f63a164/test/fixtures/app_empty/.gitkeep -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-app-repo-koa-ts", 3 | "main": "src/index.ts" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import path from "path"; 3 | import { ArtusApplication } from "../../../../src"; 4 | import { server } from "./lifecycle"; 5 | 6 | export const app: ArtusApplication = new ArtusApplication(); 7 | 8 | async function main() { 9 | await app.load({ 10 | version: '2', 11 | refMap: { 12 | _app: { 13 | pluginConfig: {}, 14 | items: [ 15 | { 16 | path: path.resolve(__dirname, "./lifecycle"), 17 | extname: ".ts", 18 | filename: "lifecycle.ts", 19 | loader: "lifecycle-hook-unit", 20 | source: "app", 21 | }, 22 | { 23 | path: path.resolve(__dirname, "./koa_app"), 24 | extname: ".ts", 25 | filename: "koaApp.ts", 26 | loader: "module", 27 | source: "app", 28 | }, 29 | { 30 | path: path.resolve(__dirname, "./controllers/hello"), 31 | extname: ".ts", 32 | filename: "hello.ts", 33 | loader: "module", 34 | source: "app", 35 | }, 36 | { 37 | path: path.resolve(__dirname, "./services/hello"), 38 | extname: ".ts", 39 | filename: "hello.ts", 40 | loader: "module", 41 | source: "app", 42 | }, 43 | ], 44 | }, 45 | }, 46 | }); 47 | await app.run(); 48 | 49 | return app; 50 | } 51 | 52 | const isListening = () => server.listening; 53 | 54 | export { main, isListening }; 55 | -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/config/config.default.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'artus', 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/config/config.dev.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'artus', 3 | testStr: 'This is dev config', 4 | }; 5 | -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/config/plugin.d.ts: -------------------------------------------------------------------------------- 1 | declare const _default: { 2 | redis: { 3 | enable: boolean; 4 | package: string; 5 | }; 6 | }; 7 | export default _default; 8 | -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/config/plugin.default.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export default { 4 | redis: { 5 | enable: true, 6 | path: path.resolve(__dirname, '../redis_plugin'), 7 | }, 8 | mysql: { 9 | enable: false, 10 | path: path.resolve(__dirname, '../mysql_plugin'), 11 | }, 12 | testDuplicate: { 13 | enable: false, 14 | package: '@artus/injection', // This package is unimportant, will be replaced by path in dev config 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/config/plugin.dev.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export default { 4 | testDuplicate: { 5 | enable: true, 6 | path: path.resolve(__dirname, '../test_duplicate_plugin'), 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/controllers/hello.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Inject, Injectable, ScopeEnum } from '@artus/injection'; 3 | import HelloService from '../services/hello'; 4 | 5 | // TODO: 待实现 Controller/Route 装饰器 6 | @Injectable({ 7 | scope: ScopeEnum.EXECUTION, 8 | }) 9 | export default class HelloController { 10 | @Inject(HelloService) 11 | helloService: HelloService; 12 | 13 | async index () { 14 | return { 15 | status: 200, 16 | content: 'Hello Artus', 17 | headers: { 18 | ...this.helloService?.getTestHeaders(), 19 | }, 20 | }; 21 | } 22 | } -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/exception.json: -------------------------------------------------------------------------------- 1 | { 2 | "ARTUS:GLOBAL_TEST": { 3 | "desc": "全局测试错误,仅用于单元测试", 4 | "detailUrl": "https://github.com/artusjs/spec" 5 | }, 6 | "ARTUS:GLOBAL_TEST_I18N": { 7 | "desc": { 8 | "zh": "全局测试错误,仅用于单元测试", 9 | "en": "This is a test exception, only valid in unit-test" 10 | }, 11 | "detailUrl": "https://github.com/artusjs/spec" 12 | } 13 | } -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/filter/default.ts: -------------------------------------------------------------------------------- 1 | import { Catch, ExceptionFilterType } from '../../../../../src'; 2 | 3 | @Catch() 4 | export class MockExceptionFilter implements ExceptionFilterType { 5 | async catch(_err: Error): Promise { 6 | // Empty filter 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/koa_app.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ScopeEnum } from '@artus/injection'; 2 | import Koa from 'koa'; 3 | 4 | @Injectable({ 5 | scope: ScopeEnum.SINGLETON, 6 | }) 7 | export default class KoaApplication extends Koa { } 8 | -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/lifecycle.ts: -------------------------------------------------------------------------------- 1 | import { DefaultContext } from 'koa'; 2 | import { Server } from 'http'; 3 | import { Container, ExecutionContainer, Inject } from '@artus/injection'; 4 | 5 | import { ArtusApplication, ArtusInjectEnum } from '../../../../src'; 6 | import { LifecycleHookUnit, LifecycleHook } from '../../../../src/decorator'; 7 | import { ApplicationLifecycle } from '../../../../src/types'; 8 | 9 | import KoaApplication from './koa_app'; 10 | import HelloController from './controllers/hello'; 11 | 12 | export let server: Server; 13 | 14 | @LifecycleHookUnit() 15 | export default class MyLifecycle implements ApplicationLifecycle { 16 | @Inject(ArtusInjectEnum.Application) 17 | app: ArtusApplication; 18 | @Inject() 19 | container: Container; 20 | 21 | get koaApp(): KoaApplication { 22 | return this.container.get(KoaApplication); 23 | } 24 | 25 | @LifecycleHook('willReady') 26 | setKoaMiddleware() { 27 | this.koaApp.use(async (koaCtx: DefaultContext) => { 28 | const executionContainer = new ExecutionContainer(null, this.container); 29 | executionContainer.set({ id: 'headers', value: koaCtx.headers }); 30 | 31 | const data = await executionContainer.get(HelloController).index(); 32 | koaCtx.status = data.status || 200; 33 | koaCtx.body = data.content; 34 | for (const [k, v] of Object.entries(data.headers)) { 35 | koaCtx.set(k, v); 36 | } 37 | }); 38 | } 39 | 40 | @LifecycleHook('willReady') 41 | startKoaServer() { 42 | server = this.koaApp.listen(3000); 43 | } 44 | 45 | @LifecycleHook() 46 | beforeClose() { 47 | server?.close(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/mysql_plugin/app.ts: -------------------------------------------------------------------------------- 1 | import { LifecycleHookUnit, LifecycleHook } from '../../../../../src/decorator'; 2 | 3 | @LifecycleHookUnit() 4 | export default class Hook { 5 | @LifecycleHook() 6 | async willReady() { 7 | console.log('MySQL Plugin will ready'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/mysql_plugin/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mysql" 3 | } -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/no_ext_file: -------------------------------------------------------------------------------- 1 | hello:world -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/redis_plugin/app.ts: -------------------------------------------------------------------------------- 1 | import { LifecycleHookUnit, LifecycleHook } from '../../../../../src/decorator'; 2 | 3 | @LifecycleHookUnit() 4 | export default class Hook { 5 | @LifecycleHook() 6 | async willReady() { 7 | console.log('Redis Plugin will ready'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/redis_plugin/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis", 3 | "exclude": [ 4 | "not_to_be_scanned_dir", 5 | "not_to_be_scanned_file.ts" 6 | ] 7 | } -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/redis_plugin/not_to_be_scanned_dir/not_to_be_scanned_file.ts: -------------------------------------------------------------------------------- 1 | export default class DummyClazz {} 2 | -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/redis_plugin/not_to_be_scanned_file.ts: -------------------------------------------------------------------------------- 1 | export default class DummyClazz {} 2 | -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/services/hello.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Inject, Injectable, ScopeEnum } from '@artus/injection'; 3 | 4 | @Injectable({ 5 | scope: ScopeEnum.EXECUTION, 6 | }) 7 | export default class HelloService { 8 | @Inject('headers') 9 | headers: Record; 10 | 11 | public getTestHeaders() { 12 | return { 13 | 'x-hello-artus': this.headers['x-hello-artus'], 14 | }; 15 | } 16 | } -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/services/no_default.ts: -------------------------------------------------------------------------------- 1 | export class Clazz {} 2 | -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/services/no_injectable.ts: -------------------------------------------------------------------------------- 1 | export default class Clazz {} 2 | -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/src/test_duplicate_plugin/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testDuplicate" 3 | } -------------------------------------------------------------------------------- /test/fixtures/app_koa_with_ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "experimentalDecorators": true, 5 | "target": "ES6", 6 | "moduleResolution": "node" 7 | }, 8 | "rootDir": "./src", 9 | "exclude": [] 10 | } -------------------------------------------------------------------------------- /test/fixtures/app_with_config/app.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'http'; 2 | import Koa from 'koa'; 3 | import { LifecycleHookUnit, LifecycleHook } from '../../../src/decorator'; 4 | import { ApplicationLifecycle } from '../../../src/types'; 5 | 6 | let server: Server; 7 | const koaApp = new Koa(); 8 | 9 | @LifecycleHookUnit() 10 | export default class MyLifecycle implements ApplicationLifecycle { 11 | testStr = 'Hello Artus'; 12 | 13 | @LifecycleHook() 14 | willReady() { 15 | koaApp.use(ctx => { 16 | ctx.status = 200; 17 | ctx.body = this.testStr; 18 | }); 19 | server = koaApp.listen(3000); 20 | } 21 | 22 | @LifecycleHook() 23 | beforeClose() { 24 | server?.close(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/fixtures/app_with_config/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { ArtusApplication } from "../../../src"; 3 | 4 | const rootDir = path.resolve(__dirname, "./"); 5 | 6 | async function main() { 7 | const app = new ArtusApplication(); 8 | await app.load({ 9 | version: "2", 10 | refMap: { 11 | _app: { 12 | pluginConfig: {}, 13 | items: [ 14 | { 15 | path: path.resolve(__dirname, "./app"), 16 | extname: ".ts", 17 | filename: "app.ts", 18 | loader: "lifecycle-hook-unit", 19 | source: "app", 20 | }, 21 | { 22 | path: path.resolve(rootDir, "./config/config.default"), 23 | extname: ".ts", 24 | filename: "config.default.ts", 25 | loader: "config", 26 | source: "app", 27 | }, 28 | { 29 | path: path.resolve(rootDir, "./config/config.production"), 30 | extname: ".ts", 31 | filename: "config.production.ts", 32 | loader: "config", 33 | source: "app", 34 | }, 35 | ], 36 | }, 37 | }, 38 | }); 39 | return app; 40 | } 41 | 42 | export { main }; 43 | -------------------------------------------------------------------------------- /test/fixtures/app_with_config/config/config.default.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: "test-for-config", 3 | test: 1, 4 | arr: [1,2,3], 5 | }; -------------------------------------------------------------------------------- /test/fixtures/app_with_config/config/config.production.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: "test-for-config", 3 | test: 1, 4 | arr: [4,5,6], 5 | }; -------------------------------------------------------------------------------- /test/fixtures/app_with_config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-app-repo-ts", 3 | "main": "src/index.ts" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/app_with_config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "experimentalDecorators": true 5 | }, 6 | "rootDir": "./", 7 | "exclude": [] 8 | } -------------------------------------------------------------------------------- /test/fixtures/app_with_lifecycle/bootstrap_duplicated.ts: -------------------------------------------------------------------------------- 1 | export * from './bootstrap_ready'; 2 | -------------------------------------------------------------------------------- /test/fixtures/app_with_lifecycle/bootstrap_load.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArtusApplication, 3 | ApplicationLifecycle, LifecycleHookUnit, LifecycleHook, ArtusInjectEnum, 4 | } from '../../../src/index'; 5 | 6 | import { Container, Inject } from '@artus/injection'; 7 | import LifecycleList from './lifecyclelist'; 8 | export const TEST_LIFE_CYCLE_LIST = 'TEST_LIFE_CYCLE_LIST'; 9 | 10 | @LifecycleHookUnit() 11 | export default class AppLoadLifecycle implements ApplicationLifecycle { 12 | @Inject(ArtusInjectEnum.Application) 13 | app: ArtusApplication; 14 | 15 | @Inject() 16 | container: Container; 17 | 18 | @Inject() 19 | lifecycleList: LifecycleList; 20 | 21 | @LifecycleHook() 22 | async configWillLoad() { 23 | this.container.set({ id: TEST_LIFE_CYCLE_LIST, value: this.lifecycleList }); 24 | await new Promise(resolve => setTimeout(resolve, 100)); 25 | this.lifecycleList.add('app_configWillLoad'); 26 | } 27 | 28 | @LifecycleHook('configDidLoad') 29 | async customNameConfigDidLoad() { 30 | await new Promise(resolve => setTimeout(resolve, 100)); 31 | this.lifecycleList.add('app_configDidLoad'); 32 | } 33 | 34 | @LifecycleHook('didLoad') 35 | async didLoad() { 36 | await new Promise(resolve => setTimeout(resolve, 100)); 37 | this.lifecycleList.add('app_didLoad'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/fixtures/app_with_lifecycle/bootstrap_ready.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArtusApplication, 3 | ApplicationLifecycle, LifecycleHookUnit, LifecycleHook, ArtusInjectEnum, 4 | } from '../../../src/index'; 5 | 6 | import { Container, Inject } from '@artus/injection'; 7 | import LifecycleList from './lifecyclelist'; 8 | 9 | @LifecycleHookUnit() 10 | export class AppReadyLifecycle implements ApplicationLifecycle { 11 | @Inject(ArtusInjectEnum.Application) 12 | app: ArtusApplication; 13 | 14 | @Inject() 15 | container: Container; 16 | 17 | @Inject() 18 | lifecycleList: LifecycleList; 19 | 20 | @LifecycleHook('willReady') 21 | async willReady() { 22 | await new Promise(resolve => setTimeout(resolve, 100)); 23 | this.lifecycleList.add('app_willReady'); 24 | } 25 | 26 | @LifecycleHook('didReady') 27 | async didReady() { 28 | await new Promise(resolve => setTimeout(resolve, 100)); 29 | this.lifecycleList.add('app_didReady'); 30 | } 31 | 32 | @LifecycleHook() 33 | async beforeClose() { 34 | await new Promise(resolve => setTimeout(resolve, 100)); 35 | this.lifecycleList.add('app_beforeClose'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/fixtures/app_with_lifecycle/config/config.default.ts: -------------------------------------------------------------------------------- 1 | export default {}; -------------------------------------------------------------------------------- /test/fixtures/app_with_lifecycle/config/plugin.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export default { 4 | pluginA: { 5 | enable: true, 6 | path: path.resolve(__dirname, '../plugins/plugin-a'), 7 | }, 8 | 9 | pluginB: { 10 | enable: true, 11 | path: path.resolve(__dirname, '../plugins/plugin-b'), 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /test/fixtures/app_with_lifecycle/lifecyclelist.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ScopeEnum } from '../../../src'; 2 | 3 | @Injectable({ 4 | scope: ScopeEnum.SINGLETON, 5 | }) 6 | export default class LifecycleList { 7 | lifecycleList: string[] = []; 8 | 9 | async add(name: string) { 10 | this.lifecycleList.push(name); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/app_with_lifecycle/plugins/plugin-a/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArtusApplication, 3 | ApplicationLifecycle, LifecycleHookUnit, LifecycleHook, ArtusInjectEnum, 4 | } from '../../../../../src'; 5 | 6 | import { Container, Inject } from '@artus/injection'; 7 | import LifecycleList from '../../lifecyclelist'; 8 | 9 | @LifecycleHookUnit() 10 | export default class AppLifecycle implements ApplicationLifecycle { 11 | @Inject(ArtusInjectEnum.Application) 12 | app: ArtusApplication; 13 | 14 | @Inject() 15 | container: Container; 16 | 17 | @Inject() 18 | lifecycleList: LifecycleList; 19 | 20 | @LifecycleHook() 21 | async configWillLoad() { 22 | await new Promise(resolve => setTimeout(resolve, 100)); 23 | this.lifecycleList.add('pluginA_configWillLoad'); 24 | } 25 | 26 | @LifecycleHook('configDidLoad') 27 | async customNameConfigDidLoad() { 28 | await new Promise(resolve => setTimeout(resolve, 100)); 29 | this.lifecycleList.add('pluginA_configDidLoad'); 30 | } 31 | 32 | @LifecycleHook('didLoad') 33 | async didLoad() { 34 | await new Promise(resolve => setTimeout(resolve, 100)); 35 | this.lifecycleList.add('pluginA_didLoad'); 36 | } 37 | 38 | @LifecycleHook('willReady') 39 | async willReady() { 40 | await new Promise(resolve => setTimeout(resolve, 100)); 41 | this.lifecycleList.add('pluginA_willReady'); 42 | } 43 | 44 | @LifecycleHook('didReady') 45 | async didReady() { 46 | await new Promise(resolve => setTimeout(resolve, 100)); 47 | this.lifecycleList.add('pluginA_didReady'); 48 | } 49 | 50 | @LifecycleHook() 51 | async beforeClose() { 52 | await new Promise(resolve => setTimeout(resolve, 100)); 53 | this.lifecycleList.add('pluginA_beforeClose'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/fixtures/app_with_lifecycle/plugins/plugin-a/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pluginA" 3 | } -------------------------------------------------------------------------------- /test/fixtures/app_with_lifecycle/plugins/plugin-b/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArtusApplication, 3 | ApplicationLifecycle, LifecycleHookUnit, LifecycleHook, ArtusInjectEnum, 4 | } from '../../../../../src'; 5 | 6 | import { Container, Inject } from '@artus/injection'; 7 | import LifecycleList from '../../lifecyclelist'; 8 | 9 | @LifecycleHookUnit() 10 | export default class AppLifecycle implements ApplicationLifecycle { 11 | @Inject(ArtusInjectEnum.Application) 12 | app: ArtusApplication; 13 | 14 | @Inject() 15 | container: Container; 16 | 17 | @Inject() 18 | lifecycleList: LifecycleList; 19 | 20 | @LifecycleHook('configWillLoad') 21 | customConfigWillLoad() { 22 | this.lifecycleList.add('pluginB_configWillLoad'); 23 | } 24 | 25 | @LifecycleHook('configDidLoad') 26 | customNameConfigDidLoad() { 27 | this.lifecycleList.add('pluginB_configDidLoad'); 28 | } 29 | 30 | @LifecycleHook('didLoad') 31 | didLoad() { 32 | this.lifecycleList.add('pluginB_didLoad'); 33 | } 34 | 35 | @LifecycleHook('willReady') 36 | willReady() { 37 | this.lifecycleList.add('pluginB_willReady'); 38 | } 39 | 40 | @LifecycleHook('didReady') 41 | didReady() { 42 | this.lifecycleList.add('pluginB_didReady'); 43 | } 44 | 45 | @LifecycleHook() 46 | beforeClose() { 47 | this.lifecycleList.add('pluginB_beforeClose'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/fixtures/app_with_lifecycle/plugins/plugin-b/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pluginB" 3 | } -------------------------------------------------------------------------------- /test/fixtures/app_with_lifecycle/test/throw.ts: -------------------------------------------------------------------------------- 1 | throw new Error('should not scan me'); 2 | -------------------------------------------------------------------------------- /test/fixtures/app_with_manifest/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | // import fs from 'fs'; 3 | import 'reflect-metadata'; 4 | import { ArtusApplication } from "../../../src"; 5 | 6 | const rootDir = path.resolve(__dirname, "./"); 7 | 8 | async function main() { 9 | const app = new ArtusApplication(); 10 | const metaFilePath = path.resolve(rootDir, 'manifest.json'); 11 | // let manifest; 12 | // if (fs.existsSync(metaFilePath)) { 13 | const manifest = require((metaFilePath)); 14 | // } else { 15 | // const scanner = new ArtusScanner({ 16 | // configDir: 'config', 17 | // needWriteFile: true, 18 | // useRelativePath: true, 19 | // extensions: ['.ts', '.js', '.json'], 20 | // app, 21 | // }); 22 | // manifest = await scanner.scan(rootDir); 23 | // } 24 | await app.load(manifest, rootDir); 25 | await app.run(); 26 | return app; 27 | 28 | } 29 | export { main }; 30 | -------------------------------------------------------------------------------- /test/fixtures/app_with_manifest/config/config.default.ts: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | httpConfig: { 4 | port: 3000, 5 | }, 6 | }; -------------------------------------------------------------------------------- /test/fixtures/app_with_manifest/controller/home.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '../../../../src/'; 2 | 3 | @Injectable() 4 | export default class DemoController { 5 | public async index() { 6 | return { code: 0, message: 'ok', data: {} }; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/app_with_manifest/lifecycle.ts: -------------------------------------------------------------------------------- 1 | import { LifecycleHookUnit, ApplicationLifecycle, LifecycleHook, Inject, ArtusApplication, ArtusInjectEnum } from '../../../src'; 2 | 3 | @LifecycleHookUnit() 4 | export default class MyLifecycle implements ApplicationLifecycle { 5 | @Inject(ArtusInjectEnum.Application) 6 | private app: ArtusApplication; 7 | 8 | @LifecycleHook() 9 | public async configWillLoad() { 10 | this.app.config.httpConfig = this.app.config.httpConfig ?? {}; 11 | this.app.config.httpConfig.key1 = 'value1'; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/app_with_manifest/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2", 3 | "refMap": { 4 | "_app": { 5 | "relativedPath": "", 6 | "pluginConfig": {}, 7 | "items": [ 8 | { 9 | "path": "config/config.default", 10 | "extname": ".ts", 11 | "filename": "config.default.ts", 12 | "loader": "config", 13 | "source": "app", 14 | "unitName": "_app", 15 | "loaderState": { 16 | "exportNames": [] 17 | } 18 | }, 19 | { 20 | "path": "controller/home", 21 | "extname": ".ts", 22 | "filename": "home.ts", 23 | "loader": "module", 24 | "source": "app", 25 | "unitName": "_app", 26 | "loaderState": { 27 | "exportNames": [ 28 | "default" 29 | ] 30 | } 31 | }, 32 | { 33 | "path": "lifecycle", 34 | "extname": ".ts", 35 | "filename": "lifecycle.ts", 36 | "loader": "lifecycle-hook-unit", 37 | "source": "app", 38 | "unitName": "_app", 39 | "loaderState": { 40 | "exportNames": [ 41 | "default" 42 | ] 43 | } 44 | } 45 | ] 46 | } 47 | }, 48 | "extraPluginConfig": {} 49 | } -------------------------------------------------------------------------------- /test/fixtures/app_with_plugin_version_check/config/plugin.default.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export default { 4 | a: { 5 | enable: true, 6 | refName: 'test', 7 | path: path.resolve(__dirname, '../../plugins/plugin_a'), 8 | }, 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /test/fixtures/app_with_plugin_version_check/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mock_app", 3 | "private": true 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/app_with_plugin_version_check/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "experimentalDecorators": true, 5 | "target": "ES6", 6 | "moduleResolution": "node" 7 | }, 8 | "rootDir": "./src", 9 | "exclude": [] 10 | } -------------------------------------------------------------------------------- /test/fixtures/app_with_preset_b/config/plugin.default.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export default { 4 | preset_b: { 5 | enable: true, 6 | path: path.resolve(__dirname, '../../plugins/preset_b'), 7 | }, 8 | }; 9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/app_with_preset_b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mock_app", 3 | "private": true 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/app_with_preset_b/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "experimentalDecorators": true, 5 | "target": "ES6", 6 | "moduleResolution": "node" 7 | }, 8 | "rootDir": "./src", 9 | "exclude": [] 10 | } -------------------------------------------------------------------------------- /test/fixtures/app_without_config/app.ts: -------------------------------------------------------------------------------- 1 | import { LifecycleHookUnit, LifecycleHook } from '../../../src/decorator'; 2 | import { ApplicationLifecycle } from '../../../src/types'; 3 | import { Container, Inject } from '@artus/injection'; 4 | import LifecycleList from './lifecyclelist'; 5 | 6 | @LifecycleHookUnit() 7 | export default class MyLifecycle implements ApplicationLifecycle { 8 | @Inject() 9 | container: Container; 10 | 11 | @Inject() 12 | lifecycleList: LifecycleList; 13 | 14 | @LifecycleHook() 15 | async configWillLoad() { 16 | this.lifecycleList.add('configWillLoad'); 17 | } 18 | 19 | @LifecycleHook() 20 | async configDidLoad() { 21 | this.lifecycleList.add('configDidLoad'); 22 | } 23 | 24 | @LifecycleHook() 25 | async willReady() { 26 | this.lifecycleList.add('willReady'); 27 | } 28 | 29 | @LifecycleHook() 30 | async didLoad() { 31 | this.lifecycleList.add('didLoad'); 32 | } 33 | 34 | @LifecycleHook('didReady') 35 | async didReady() { 36 | this.lifecycleList.add('didReady'); 37 | } 38 | 39 | @LifecycleHook() 40 | async beforeClose() { 41 | this.lifecycleList.add('beforeClose'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/fixtures/app_without_config/lifecyclelist.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ScopeEnum } from '../../../src'; 2 | 3 | @Injectable({ 4 | scope: ScopeEnum.SINGLETON, 5 | }) 6 | export default class LifecycleList { 7 | lifecycleList: string[] = []; 8 | 9 | async add(name: string) { 10 | this.lifecycleList.push(name); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/app_without_config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-app-repo-ts", 3 | "main": "src/index.ts" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/app_without_config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "experimentalDecorators": true 5 | }, 6 | "rootDir": "./", 7 | "exclude": [] 8 | } -------------------------------------------------------------------------------- /test/fixtures/custom_instance/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArtusApplication, 3 | ApplicationLifecycle, LifecycleHookUnit, LifecycleHook, ArtusInjectEnum, 4 | } from '../../../src/index'; 5 | 6 | import { Container, Inject } from '@artus/injection'; 7 | import Custom from './custom'; 8 | 9 | @LifecycleHookUnit() 10 | export default class MyLifecycle implements ApplicationLifecycle { 11 | @Inject(ArtusInjectEnum.Application) 12 | app: ArtusApplication; 13 | @Inject() 14 | container: Container; 15 | 16 | @LifecycleHook() 17 | configDidLoad() { 18 | this.container.set({ id: Custom, value: new Custom('foo') }); 19 | } 20 | 21 | @LifecycleHook() 22 | async beforeClose() { 23 | console.log(this.container.get(Custom)); 24 | } 25 | } -------------------------------------------------------------------------------- /test/fixtures/custom_instance/config/config.default.ts: -------------------------------------------------------------------------------- 1 | export default {}; -------------------------------------------------------------------------------- /test/fixtures/custom_instance/custom.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ScopeEnum } from "../../../src/index"; 2 | 3 | @Injectable({ 4 | scope: ScopeEnum.SINGLETON, 5 | }) 6 | export default class Custom { 7 | private name: string; 8 | 9 | constructor(name: string) { 10 | this.name = name; 11 | } 12 | 13 | getName(): string { 14 | return this.name; 15 | } 16 | } -------------------------------------------------------------------------------- /test/fixtures/exception_filter/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { ArtusApplication } from "../../../src"; 3 | 4 | async function main() { 5 | const app = new ArtusApplication(); 6 | app.container.set({ 7 | id: "mock_exception_set", 8 | value: new Set(), 9 | }); 10 | await app.load({ 11 | version: "2", 12 | refMap: { 13 | _app: { 14 | pluginConfig: {}, 15 | items: [ 16 | { 17 | path: path.resolve(__dirname, "./filter"), 18 | extname: ".ts", 19 | filename: "filter.ts", 20 | loader: "exception-filter", 21 | loaderState: { 22 | exportNames: [ 23 | "TestDefaultExceptionHandler", 24 | "TestAppCodeExceptionHandler", 25 | "TestWrappedExceptionHandler", 26 | "TestCustomExceptionHandler", 27 | "TestDefaultInheritExceptionHandler", 28 | "TestInheritExceptionHandler", 29 | ], 30 | }, 31 | source: "app", 32 | }, 33 | { 34 | path: path.resolve(__dirname, "../../../exception.json"), 35 | extname: ".json", 36 | filename: "exception.json", 37 | loader: "exception", 38 | source: "app", 39 | }, 40 | { 41 | path: path.resolve(__dirname, "./exception.json"), 42 | extname: ".json", 43 | filename: "exception.json", 44 | loader: "exception", 45 | source: "app", 46 | }, 47 | { 48 | path: path.resolve(__dirname, "./service"), 49 | extname: ".ts", 50 | filename: "service.ts", 51 | loader: "module", 52 | source: "app", 53 | }, 54 | ], 55 | }, 56 | }, 57 | }); 58 | await app.run(); 59 | return app; 60 | } 61 | 62 | export { main }; 63 | -------------------------------------------------------------------------------- /test/fixtures/exception_filter/error.ts: -------------------------------------------------------------------------------- 1 | import { ArtusStdError } from '../../../src'; 2 | 3 | export class TestWrappedError extends ArtusStdError { 4 | static code = 'APP:WRAPPED_ERROR'; 5 | name = 'TestWrappedError'; 6 | 7 | constructor() { 8 | super(TestWrappedError.code); 9 | } 10 | } 11 | 12 | export class TestCustomError extends Error { 13 | name = 'TestCustomError'; 14 | } 15 | 16 | export class TestDefaultInheritError extends Error { 17 | name = 'TestDefaultInheritError'; 18 | } 19 | 20 | export class TestBaseError extends Error { 21 | name = 'TestBaseError'; 22 | } 23 | 24 | export class TestInheritError extends TestBaseError { 25 | name = 'TestInheritError'; 26 | } 27 | -------------------------------------------------------------------------------- /test/fixtures/exception_filter/exception.json: -------------------------------------------------------------------------------- 1 | { 2 | "APP:TEST_ERROR": { 3 | "desc": "这是一个测试用的错误", 4 | "detailUrl": "https://github.com/artusjs" 5 | }, 6 | "APP:WRAPPED_ERROR": { 7 | "desc": "这个错误将会被自定义类包装" 8 | } 9 | } -------------------------------------------------------------------------------- /test/fixtures/exception_filter/filter.ts: -------------------------------------------------------------------------------- 1 | import { ArtusStdError, Catch, Inject } from '../../../src'; 2 | import { ExceptionFilterType } from '../../../src/exception/types'; 3 | import { TestBaseError, TestCustomError, TestWrappedError } from './error'; 4 | 5 | @Catch() 6 | export class TestDefaultExceptionHandler implements ExceptionFilterType { 7 | @Inject('mock_exception_set') 8 | mockSet: Set; 9 | 10 | async catch(err: Error) { 11 | this.mockSet.add(err.name); 12 | } 13 | } 14 | 15 | @Catch('APP:TEST_ERROR') 16 | export class TestAppCodeExceptionHandler implements ExceptionFilterType { 17 | @Inject('mock_exception_set') 18 | mockSet: Set; 19 | 20 | async catch(err: ArtusStdError) { 21 | this.mockSet.add(err.code); 22 | } 23 | } 24 | 25 | @Catch(TestWrappedError) 26 | export class TestWrappedExceptionHandler implements ExceptionFilterType { 27 | @Inject('mock_exception_set') 28 | mockSet: Set; 29 | 30 | async catch(err: TestWrappedError) { 31 | this.mockSet.add(err.code); 32 | } 33 | } 34 | 35 | @Catch(TestCustomError) 36 | export class TestCustomExceptionHandler implements ExceptionFilterType { 37 | @Inject('mock_exception_set') 38 | mockSet: Set; 39 | 40 | async catch(err: TestCustomError) { 41 | this.mockSet.add(err.name); 42 | } 43 | } 44 | 45 | @Catch(Error) 46 | export class TestDefaultInheritExceptionHandler implements ExceptionFilterType { 47 | @Inject('mock_exception_set') 48 | mockSet: Set; 49 | 50 | async catch(err: Error) { 51 | this.mockSet.add(err.name); 52 | } 53 | } 54 | 55 | @Catch(TestBaseError) 56 | export class TestInheritExceptionHandler implements ExceptionFilterType { 57 | @Inject('mock_exception_set') 58 | mockSet: Set; 59 | 60 | async catch(err: TestBaseError) { 61 | this.mockSet.add(err.name); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/fixtures/exception_filter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-fixture", 3 | "main": "boostrap.ts" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/exception_filter/service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@artus/injection'; 2 | import { ArtusStdError } from '../../../src'; 3 | import { TestCustomError, TestDefaultInheritError, TestInheritError, TestWrappedError } from './error'; 4 | 5 | @Injectable() 6 | export default class MockErrorService { 7 | 8 | throw(target: string) { 9 | switch (target) { 10 | case "default": 11 | throw new Error("default error"); 12 | case "custom": 13 | throw new TestCustomError(); 14 | case "wrapped": 15 | throw new TestWrappedError(); 16 | case "inherit": 17 | throw new TestInheritError(); 18 | case "defaultInherit": 19 | throw new TestDefaultInheritError(); 20 | default: 21 | throw new ArtusStdError(target); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/fixtures/exception_filter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "experimentalDecorators": true 5 | }, 6 | "rootDir": "./", 7 | "exclude": [] 8 | } -------------------------------------------------------------------------------- /test/fixtures/exception_invalid_filter/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { ArtusApplication } from "../../../src"; 3 | 4 | async function main() { 5 | const app = new ArtusApplication(); 6 | app.container.set({ 7 | id: "mock_exception_set", 8 | value: new Set(), 9 | }); 10 | await app.load({ 11 | version: "2", 12 | refMap: { 13 | _app: { 14 | pluginConfig: {}, 15 | items: [ 16 | { 17 | path: path.resolve(__dirname, "./filter"), 18 | extname: ".ts", 19 | filename: "filter.ts", 20 | loader: "exception-filter", 21 | loaderState: { 22 | exportNames: ["TestInvalidFilter"], 23 | }, 24 | source: "app", 25 | }, 26 | ], 27 | }, 28 | }, 29 | }); 30 | await app.run(); 31 | return app; 32 | } 33 | 34 | export { main }; 35 | -------------------------------------------------------------------------------- /test/fixtures/exception_invalid_filter/filter.ts: -------------------------------------------------------------------------------- 1 | export class TestInvalidFilter { 2 | } 3 | -------------------------------------------------------------------------------- /test/fixtures/exception_invalid_filter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-fixture", 3 | "main": "boostrap.ts" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/exception_invalid_filter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "experimentalDecorators": true 5 | }, 6 | "rootDir": "./", 7 | "exclude": [] 8 | } -------------------------------------------------------------------------------- /test/fixtures/exception_with_ts_yaml/app.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'http'; 2 | import Koa from 'koa'; 3 | import { LifecycleHookUnit, LifecycleHook } from '../../../src/decorator'; 4 | import { ApplicationLifecycle } from '../../../src/types'; 5 | 6 | export let server: Server; 7 | const koaApp = new Koa(); 8 | 9 | @LifecycleHookUnit() 10 | export default class MyLifecycle implements ApplicationLifecycle { 11 | testStr = 'Hello Artus'; 12 | 13 | @LifecycleHook() 14 | willReady() { 15 | koaApp.use(ctx => { 16 | ctx.status = 200; 17 | ctx.body = this.testStr; 18 | }); 19 | server = koaApp.listen(3000); 20 | } 21 | 22 | @LifecycleHook() 23 | beforeClose() { 24 | server?.close(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/fixtures/exception_with_ts_yaml/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { ArtusApplication } from "../../../src"; 3 | import { server } from "./app"; 4 | 5 | async function main() { 6 | const app = new ArtusApplication(); 7 | await app.load({ 8 | version: "2", 9 | refMap: { 10 | _app: { 11 | pluginConfig: {}, 12 | items: [ 13 | { 14 | path: path.resolve(__dirname, "./app"), 15 | extname: ".ts", 16 | filename: "app.ts", 17 | loader: "lifecycle-hook-unit", 18 | source: "app", 19 | }, 20 | { 21 | path: path.resolve(__dirname, "../../../exception.json"), 22 | extname: ".json", 23 | filename: "exception.json", 24 | loader: "exception", 25 | source: "app", 26 | }, 27 | { 28 | path: path.resolve(__dirname, "./exception.json"), 29 | extname: ".json", 30 | filename: "exception.json", 31 | loader: "exception", 32 | source: "app", 33 | }, 34 | ], 35 | }, 36 | }, 37 | }); 38 | await app.run(); 39 | return app; 40 | } 41 | 42 | const isListening = () => server.listening; 43 | 44 | export { main, isListening }; 45 | -------------------------------------------------------------------------------- /test/fixtures/exception_with_ts_yaml/exception.json: -------------------------------------------------------------------------------- 1 | { 2 | "APP:TEST_ERROR": { 3 | "desc": "这是一个测试用的错误", 4 | "detailUrl": "https://github.com/artusjs" 5 | } 6 | } -------------------------------------------------------------------------------- /test/fixtures/exception_with_ts_yaml/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-app-repo-koa-ts", 3 | "main": "src/index.ts" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/exception_with_ts_yaml/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "experimentalDecorators": true 5 | }, 6 | "rootDir": "./", 7 | "exclude": [] 8 | } -------------------------------------------------------------------------------- /test/fixtures/logger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-app-repo-ts", 3 | "main": "src/index.ts" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/logger/src/custom_logger.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger, ScopeEnum } from '../../../../src'; 2 | 3 | @Injectable({ 4 | scope: ScopeEnum.SINGLETON, 5 | }) 6 | export default class CustomLogger extends Logger { 7 | public info(message: string, ...args: any[]): void { 8 | console.info('[Custom]', message, ...args); 9 | } 10 | } -------------------------------------------------------------------------------- /test/fixtures/logger/src/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { Manifest } from "../../../../src"; 3 | 4 | const rootDir = path.resolve(__dirname, "./"); 5 | 6 | const defaultManifest: Manifest = { 7 | version: "2", 8 | refMap: { 9 | _app: { 10 | pluginConfig: {}, 11 | items: [ 12 | { 13 | path: path.resolve(rootDir, "./test_clazz"), 14 | extname: ".ts", 15 | filename: "test_clazz.ts", 16 | }, 17 | ], 18 | }, 19 | }, 20 | }; 21 | 22 | export const manifestWithCustomLogger: Manifest = { 23 | version: "2", 24 | refMap: { 25 | _app: { 26 | pluginConfig: {}, 27 | items: [ 28 | ...defaultManifest.refMap._app.items, 29 | { 30 | path: path.resolve(rootDir, "./test_custom_clazz"), 31 | extname: ".ts", 32 | filename: "test_custom_clazz.ts", 33 | }, 34 | { 35 | path: path.resolve(rootDir, "./custom_logger"), 36 | extname: ".ts", 37 | filename: "custom_logger.ts", 38 | }, 39 | ], 40 | }, 41 | }, 42 | }; 43 | 44 | export default defaultManifest; 45 | -------------------------------------------------------------------------------- /test/fixtures/logger/src/test_clazz.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, ScopeEnum } from '@artus/injection'; 2 | import { Logger, LoggerLevel } from '../../../../src/logger'; 3 | 4 | @Injectable({ 5 | scope: ScopeEnum.SINGLETON, 6 | }) 7 | export default class TestLoggerClazz { 8 | @Inject() 9 | private logger!: Logger; 10 | 11 | public testLog(level: LoggerLevel, message: string | Error, ...splat: any[]) { 12 | this.logger.log({ 13 | level, 14 | message, 15 | splat, 16 | }); 17 | } 18 | 19 | public testTrace(message: string, ...args: any[]) { 20 | this.logger.trace(message, ...args); 21 | } 22 | 23 | public testDebug(message: string, ...args: any[]) { 24 | this.logger.debug(message, ...args); 25 | } 26 | 27 | public testInfo(message: string, ...args: any[]) { 28 | this.logger.info(message, ...args); 29 | } 30 | 31 | public testWarn(message: string, ...args: any[]) { 32 | this.logger.warn(message, ...args); 33 | } 34 | 35 | public testError(message: string | Error, ...args: any[]) { 36 | this.logger.error(message, ...args); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/fixtures/logger/src/test_custom_clazz.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, ScopeEnum } from '@artus/injection'; 2 | import CustomLogger from './custom_logger'; 3 | 4 | @Injectable({ 5 | scope: ScopeEnum.SINGLETON, 6 | }) 7 | export default class TestCustomLoggerClazz { 8 | @Inject() 9 | private logger: CustomLogger; 10 | 11 | public testInfo(message: string, ...args: any[]) { 12 | this.logger.info(message, ...args); 13 | } 14 | 15 | public testError(message: string | Error, ...args: any[]) { 16 | this.logger.error(message, ...args); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/logger/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "experimentalDecorators": true 5 | }, 6 | "rootDir": "./src", 7 | "exclude": [] 8 | } -------------------------------------------------------------------------------- /test/fixtures/module_with_custom_loader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-app-repo-ts", 3 | "main": "src/index.ts" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/module_with_custom_loader/src/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { Manifest } from "../../../../src"; 3 | 4 | const rootDir = path.resolve(__dirname, "./"); 5 | 6 | export default { 7 | version: "2", 8 | refMap: { 9 | _app: { 10 | pluginConfig: {}, 11 | items: [ 12 | { 13 | path: path.resolve(rootDir, "./test_clazz"), 14 | extname: ".ts", 15 | filename: "test_clazz.ts", 16 | loader: "test-custom-loader", 17 | loaderState: { 18 | hello: "loaderState", 19 | }, 20 | }, 21 | ], 22 | }, 23 | }, 24 | } as Manifest; 25 | -------------------------------------------------------------------------------- /test/fixtures/module_with_custom_loader/src/loader/test_custom_loader.ts: -------------------------------------------------------------------------------- 1 | 2 | import { DefineLoader, Loader, LoaderFindOptions, ManifestItem } from '../../../../../src'; 3 | import compatibleRequire from '../../../../../src/utils/compatible_require'; 4 | 5 | interface TestLoaderState { 6 | hello: string; 7 | } 8 | 9 | @DefineLoader('test-custom-loader') 10 | export default class TestCustomLoader implements Loader { 11 | static async is(opts: LoaderFindOptions) { 12 | return opts.filename === 'test_clazz.ts'; 13 | } 14 | 15 | static async onFind(_opts: LoaderFindOptions): Promise { 16 | return { 17 | hello: 'loaderState', 18 | }; 19 | } 20 | 21 | state!: TestLoaderState; 22 | 23 | async load(item: ManifestItem) { 24 | const clazz = await compatibleRequire(item.path); 25 | if (!global.mockCustomLoaderFn) { 26 | throw new Error('Invalid Jest Environment'); 27 | } 28 | global.mockCustomLoaderFn(clazz.name); 29 | global.mockCustomLoaderFn(this.state.hello); 30 | } 31 | } -------------------------------------------------------------------------------- /test/fixtures/module_with_custom_loader/src/test_clazz.ts: -------------------------------------------------------------------------------- 1 | export default class TestClass {} 2 | -------------------------------------------------------------------------------- /test/fixtures/module_with_custom_loader/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "experimentalDecorators": true 5 | }, 6 | "rootDir": "./src", 7 | "exclude": [] 8 | } -------------------------------------------------------------------------------- /test/fixtures/module_with_js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-app-repo-js", 3 | "main": "src/index.js" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/module_with_js/src/index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const rootDir = path.resolve(__dirname, "./"); 4 | 5 | module.exports = { 6 | version: "2", 7 | pluginConfig: {}, 8 | refMap: { 9 | _app: { 10 | items: [ 11 | { 12 | id: "testServiceA", 13 | path: path.resolve(rootDir, "./test_service_a"), 14 | extname: ".js", 15 | filename: "test_service_a.js", 16 | }, 17 | { 18 | id: "testServiceB", 19 | scope: "Execution", 20 | path: path.resolve(rootDir, "./test_service_b"), 21 | extname: ".js", 22 | filename: "test_service_b.js", 23 | }, 24 | ], 25 | }, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /test/fixtures/module_with_js/src/test_service_a.js: -------------------------------------------------------------------------------- 1 | module.exports = class TestServiceA { 2 | testMethod (app) { 3 | return app.testServiceB.sayHello(); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/module_with_js/src/test_service_b.js: -------------------------------------------------------------------------------- 1 | module.exports = class TestServiceB { 2 | sayHello () { 3 | return 'Hello Artus'; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/module_with_ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-app-repo-ts", 3 | "main": "src/index.ts" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/module_with_ts/src/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { Manifest } from "../../../../src"; 3 | 4 | const rootDir = path.resolve(__dirname, "./"); 5 | 6 | export default { 7 | version: "2", 8 | refMap: { 9 | _app: { 10 | pluginConfig: {}, 11 | items: [ 12 | { 13 | path: path.resolve(rootDir, "./test_service_a"), 14 | extname: ".ts", 15 | filename: "test_service_a.ts", 16 | }, 17 | { 18 | path: path.resolve(rootDir, "./test_service_b"), 19 | extname: ".ts", 20 | filename: "test_service_b.ts", 21 | }, 22 | ], 23 | }, 24 | }, 25 | } as Manifest; 26 | -------------------------------------------------------------------------------- /test/fixtures/module_with_ts/src/test_service_a.ts: -------------------------------------------------------------------------------- 1 | import { ScopeEnum, Injectable, Inject } from '@artus/injection'; 2 | import TestServiceB from './test_service_b'; 3 | 4 | @Injectable({ 5 | id: 'testServiceA', 6 | scope: ScopeEnum.SINGLETON, 7 | }) 8 | export default class TestServiceA { 9 | @Inject('testServiceB') 10 | testServiceB: TestServiceB; 11 | 12 | testMethod() { 13 | return this.testServiceB.sayHello(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/module_with_ts/src/test_service_b.ts: -------------------------------------------------------------------------------- 1 | import { ScopeEnum, Injectable } from '@artus/injection'; 2 | 3 | @Injectable({ 4 | id: 'testServiceB', 5 | scope: ScopeEnum.SINGLETON, 6 | }) 7 | export default class TestServiceB { 8 | sayHello() { 9 | return 'Hello Artus'; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/module_with_ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "experimentalDecorators": true 5 | }, 6 | "rootDir": "./src", 7 | "exclude": [] 8 | } -------------------------------------------------------------------------------- /test/fixtures/named_export/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "named-export" 3 | } -------------------------------------------------------------------------------- /test/fixtures/named_export/src/config/config.default.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | key: 'random', 3 | }; -------------------------------------------------------------------------------- /test/fixtures/named_export/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mysql'; 2 | 3 | import Redis from './redis'; 4 | 5 | export { Redis }; -------------------------------------------------------------------------------- /test/fixtures/named_export/src/mysql.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ScopeEnum } from "../../../../src"; 2 | 3 | @Injectable({ 4 | scope: ScopeEnum.SINGLETON, 5 | }) 6 | export class Mysql { 7 | private name = 'mysql'; 8 | 9 | getName() { 10 | return this.name; 11 | } 12 | } 13 | 14 | export const number = 1; 15 | 16 | export const object = { a: 1 }; 17 | 18 | 19 | export class Mysql2 { 20 | private name = 'mysql2'; 21 | 22 | getName() { 23 | return this.name; 24 | } 25 | } -------------------------------------------------------------------------------- /test/fixtures/named_export/src/redis.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ScopeEnum } from "../../../../src"; 2 | 3 | @Injectable({ 4 | scope: ScopeEnum.SINGLETON, 5 | }) 6 | export default class Redis { 7 | private name = 'redis'; 8 | 9 | getName() { 10 | return this.name; 11 | } 12 | } 13 | 14 | @Injectable() 15 | export class Redis2 { 16 | private name = 'redis2'; 17 | 18 | getName() { 19 | return this.name; 20 | } 21 | } 22 | 23 | export { Redis }; -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_a/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-a", 3 | "dependencies": [ 4 | { 5 | "name": "plugin-b" 6 | }, 7 | { 8 | "name": "plugin-c", 9 | "optional": true 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@artus/test-plugin-a", 3 | "version": "0.0.1" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_a_other_ver/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-a" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_a_other_ver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@artus/test-plugin-a", 3 | "version": "0.0.1-alpha.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_a_same_ver/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-a" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_a_same_ver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@artus/test-plugin-a", 3 | "version": "0.0.1" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_b/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-b", 3 | "dependencies": [ 4 | { 5 | "name": "plugin-c" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@artus/test-plugin-b" 3 | } -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_c/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-c" 3 | } -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@artus/test-plugin-c" 3 | } -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_d/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-d", 3 | "dependencies": [ 4 | { 5 | "name": "plugin-c", 6 | "optional": true 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@artus/test-plugin-d" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_with_entry_a/mock_lib/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artusjs/core/b942d5095c8e9a98194a82509120bdda7f63a164/test/fixtures/plugins/plugin_with_entry_a/mock_lib/index.js -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_with_entry_a/mock_lib/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-with-entry-a" 3 | } -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_with_entry_a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@artus/test-plugin-with-entry-a", 3 | "main": "mock_lib/index.js" 4 | } -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_with_entry_b/mock_lib/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artusjs/core/b942d5095c8e9a98194a82509120bdda7f63a164/test/fixtures/plugins/plugin_with_entry_b/mock_lib/index.js -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_with_entry_b/mock_lib/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-with-entry-b" 3 | } -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_with_entry_b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@artus/test-plugin-with-entry-b", 3 | "exports": "./mock_lib/index.js", 4 | "main": "fake/index.js" 5 | } -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_with_entry_c/mock_lib/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artusjs/core/b942d5095c8e9a98194a82509120bdda7f63a164/test/fixtures/plugins/plugin_with_entry_c/mock_lib/index.js -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_with_entry_c/mock_lib/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-with-entry-c" 3 | } -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_with_entry_c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@artus/test-plugin-with-entry-c", 3 | "exports": { 4 | ".": "./mock_lib/index.js" 5 | }, 6 | "main": "fake/index.js" 7 | } -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_with_entry_wrong/mock_lib/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artusjs/core/b942d5095c8e9a98194a82509120bdda7f63a164/test/fixtures/plugins/plugin_with_entry_wrong/mock_lib/index.js -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_with_entry_wrong/mock_lib/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-with-entry-wrong" 3 | } -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_with_entry_wrong/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@artus/test-plugin-with-entry-wrong", 3 | "exports": [ 4 | "./mock_lib/index.js", 5 | "./mock_lib/index.json" 6 | ] 7 | } -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_wrong_a/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-wrong-a", 3 | "dependencies": [ 4 | { 5 | "name": "plugin-wrong-b" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_wrong_a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@artus/test-plugin-wrong-a" 3 | } -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_wrong_b/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin-wrong-b", 3 | "dependencies": [ 4 | { 5 | "name": "plugin-wrong-a" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /test/fixtures/plugins/plugin_wrong_b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@artus/test-plugin-wrong-b" 3 | } -------------------------------------------------------------------------------- /test/fixtures/plugins/preset_a/config/plugin.default.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export default { 4 | 'plugin-with-entry-a': { 5 | enable: true, 6 | path: path.resolve(__dirname, '../../plugin_with_entry_a'), 7 | }, 8 | preset_b: { 9 | enable: true, 10 | path: path.resolve(__dirname, '../../preset_b'), 11 | }, 12 | preset_c: { 13 | enable: true, 14 | path: path.resolve(__dirname, '../../preset_c'), 15 | }, 16 | // Should overwrite preset_b 17 | a: { 18 | enable: false, 19 | }, 20 | }; 21 | 22 | -------------------------------------------------------------------------------- /test/fixtures/plugins/preset_a/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preset_a" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/plugins/preset_a/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preset_a", 3 | "private": true 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/plugins/preset_a/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "experimentalDecorators": true, 5 | "target": "ES6", 6 | "moduleResolution": "node" 7 | }, 8 | "rootDir": "./src", 9 | "exclude": [] 10 | } -------------------------------------------------------------------------------- /test/fixtures/plugins/preset_b/config/plugin.default.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export default { 4 | 'plugin-with-entry-b': { 5 | enable: true, 6 | path: path.resolve(__dirname, '../../plugin_with_entry_b'), 7 | }, 8 | a: { 9 | enable: true, 10 | path: path.resolve(__dirname, '../../plugin_a'), 11 | }, 12 | b: { 13 | enable: true, 14 | path: path.resolve(__dirname, '../../plugin_b'), 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /test/fixtures/plugins/preset_b/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preset_b" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/plugins/preset_b/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preset_b", 3 | "private": true 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/plugins/preset_b/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "experimentalDecorators": true, 5 | "target": "ES6", 6 | "moduleResolution": "node" 7 | }, 8 | "rootDir": "./src", 9 | "exclude": [] 10 | } -------------------------------------------------------------------------------- /test/fixtures/plugins/preset_c/config/plugin.default.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export default { 4 | 'plugin-with-entry-c': { 5 | enable: false, 6 | path: path.resolve(__dirname, '../../plugin_with_entry_c'), 7 | }, 8 | // Should overwrite preset_b 9 | b: { 10 | enable: false, 11 | path: path.resolve(__dirname, '../../plugin_b'), 12 | }, 13 | c: { 14 | enable: false, 15 | path: path.resolve(__dirname, '../../plugin_c'), 16 | }, 17 | d: { 18 | enable: true, 19 | path: path.resolve(__dirname, '../../plugin_d'), 20 | }, 21 | }; 22 | 23 | -------------------------------------------------------------------------------- /test/fixtures/plugins/preset_c/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preset_c" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/plugins/preset_c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preset_c", 3 | "private": true 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/plugins/preset_c/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "experimentalDecorators": true, 5 | "target": "ES6", 6 | "moduleResolution": "node" 7 | }, 8 | "rootDir": "./src", 9 | "exclude": [] 10 | } -------------------------------------------------------------------------------- /test/lifecycle.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import assert from 'assert'; 3 | import { createApp } from './utils'; 4 | import LifecycleList from './fixtures/app_with_lifecycle/lifecyclelist'; 5 | import WithoutConfigLifecycleList from './fixtures/app_without_config/lifecyclelist'; 6 | 7 | describe('test/lifecycle.test.ts', () => { 8 | it('should lifecycle works without error', async () => { 9 | const appWithLifeCyclePath = path.resolve(__dirname, './fixtures/app_with_lifecycle'); 10 | const app = await createApp(appWithLifeCyclePath); 11 | const lifecycleList = app.container.get(LifecycleList).lifecycleList; 12 | 13 | assert.deepStrictEqual(lifecycleList, [ 14 | 'pluginA_configWillLoad', 15 | 'pluginB_configWillLoad', 16 | 'app_configWillLoad', 17 | 'pluginA_configDidLoad', 18 | 'pluginB_configDidLoad', 19 | 'app_configDidLoad', 20 | 'pluginA_didLoad', 21 | 'pluginB_didLoad', 22 | 'app_didLoad', 23 | 'pluginA_willReady', 24 | 'pluginB_willReady', 25 | 'app_willReady', 26 | 'pluginA_didReady', 27 | 'pluginB_didReady', 28 | 'app_didReady', 29 | ]); 30 | 31 | await app.close(); 32 | 33 | assert.deepStrictEqual(lifecycleList, [ 34 | 'pluginA_configWillLoad', 35 | 'pluginB_configWillLoad', 36 | 'app_configWillLoad', 37 | 'pluginA_configDidLoad', 38 | 'pluginB_configDidLoad', 39 | 'app_configDidLoad', 40 | 'pluginA_didLoad', 41 | 'pluginB_didLoad', 42 | 'app_didLoad', 43 | 'pluginA_willReady', 44 | 'pluginB_willReady', 45 | 'app_willReady', 46 | 'pluginA_didReady', 47 | 'pluginB_didReady', 48 | 'app_didReady', 49 | 'app_beforeClose', 50 | 'pluginB_beforeClose', 51 | 'pluginA_beforeClose', 52 | ]); 53 | }); 54 | 55 | it('should not trigger lifecyle multi times', async () => { 56 | const appWithLifeCyclePath = path.resolve(__dirname, './fixtures/app_with_lifecycle'); 57 | const app = await createApp(appWithLifeCyclePath); 58 | const lifecycleList = app.container.get(LifecycleList).lifecycleList; 59 | await Promise.all([ 60 | app.run(), 61 | app.run(), 62 | app.run(), 63 | ]); 64 | 65 | await Promise.all([ 66 | app.close(), 67 | app.close(), 68 | app.close(), 69 | ]); 70 | 71 | assert.deepStrictEqual(lifecycleList, [ 72 | 'pluginA_configWillLoad', 73 | 'pluginB_configWillLoad', 74 | 'app_configWillLoad', 75 | 'pluginA_configDidLoad', 76 | 'pluginB_configDidLoad', 77 | 'app_configDidLoad', 78 | 'pluginA_didLoad', 79 | 'pluginB_didLoad', 80 | 'app_didLoad', 81 | 'pluginA_willReady', 82 | 'pluginB_willReady', 83 | 'app_willReady', 84 | 'pluginA_didReady', 85 | 'pluginB_didReady', 86 | 'app_didReady', 87 | 'app_beforeClose', 88 | 'pluginB_beforeClose', 89 | 'pluginA_beforeClose', 90 | ]); 91 | }); 92 | 93 | it('should trigger configDidLoad without config file', async () => { 94 | const appWithoutConfigPath = path.resolve(__dirname, './fixtures/app_without_config'); 95 | const app = await createApp(appWithoutConfigPath); 96 | const lifecycleList = app.container.get(WithoutConfigLifecycleList).lifecycleList; 97 | await app.close(); 98 | 99 | assert.deepStrictEqual(lifecycleList, [ 100 | 'configWillLoad', 101 | 'configDidLoad', 102 | 'didLoad', 103 | 'willReady', 104 | 'didReady', 105 | 'beforeClose', 106 | ]); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /test/loader.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import assert from 'assert'; 4 | import path from 'path'; 5 | import { ArtusApplication, LoaderFactory, findLoaderName } from '../src'; 6 | import Custom from './fixtures/custom_instance/custom'; 7 | import { createApp } from './utils'; 8 | 9 | const initTestContainer = () => { 10 | const app = new ArtusApplication(); 11 | return app.container; 12 | }; 13 | 14 | describe('test/loader.test.ts', () => { 15 | describe('module with ts', () => { 16 | it('should load module testServiceA.ts and testServiceB.ts', async () => { 17 | const container = initTestContainer(); 18 | const loaderFactory = container.get(LoaderFactory); 19 | 20 | // Manifest Version 1 21 | const manifest = require('./fixtures/module_with_ts/src/index').default; 22 | await loaderFactory.loadManifest(manifest); 23 | assert((container.get('testServiceA') as any).testMethod() === 'Hello Artus'); 24 | }); 25 | }); 26 | 27 | describe('module with js', () => { 28 | it('should load module testServiceA.js and testServiceB.js', async () => { 29 | const container = initTestContainer(); 30 | const loaderFactory = container.get(LoaderFactory); 31 | 32 | const manifest = require('./fixtures/module_with_js/src/index'); 33 | await loaderFactory.loadManifest(manifest); 34 | const appProxy = new Proxy( 35 | {}, 36 | { 37 | get(_target, properName: string) { 38 | return container.get(properName); 39 | }, 40 | }, 41 | ); 42 | assert((container.get('testServiceA') as any).testMethod(appProxy) === 'Hello Artus'); 43 | }); 44 | }); 45 | 46 | describe('module with custom loader', () => { 47 | it('should load module test.ts with custom loader', async () => { 48 | // SEEME: the import®ister code need be replaced by scanner at production. 49 | const { 50 | default: TestCustomLoader, 51 | } = require('./fixtures/module_with_custom_loader/src/loader/test_custom_loader'); 52 | LoaderFactory.register(TestCustomLoader); 53 | 54 | const { default: manifest } = require('./fixtures/module_with_custom_loader/src/index'); 55 | 56 | const container = initTestContainer(); 57 | const loaderFactory = container.get(LoaderFactory); 58 | 59 | const { loader: loaderName } = await findLoaderName({ 60 | filename: 'test_clazz.ts', 61 | root: path.resolve(__dirname, './fixtures/module_with_custom_loader/src'), 62 | baseDir: path.resolve(__dirname, './fixtures/module_with_custom_loader/src'), 63 | configDir: 'src/config', 64 | }); 65 | expect(loaderName).toBe('test-custom-loader'); 66 | 67 | global.mockCustomLoaderFn = jest.fn(); 68 | await loaderFactory.loadManifest(manifest); 69 | expect(global.mockCustomLoaderFn).toBeCalledWith('TestClass'); 70 | expect(global.mockCustomLoaderFn).toBeCalledWith('loaderState'); 71 | }); 72 | }); 73 | 74 | describe('custom instance', () => { 75 | it('should not overide custom instance', async () => { 76 | const app = await createApp(path.resolve(__dirname, './fixtures/custom_instance')); 77 | expect(app.container.get(Custom).getName()).toBe('foo'); 78 | }); 79 | }); 80 | 81 | describe('loader event', () => { 82 | it('should emit loader event', async () => { 83 | const container = initTestContainer(); 84 | const loaderFactory = container.get(LoaderFactory); 85 | const cb = jest.fn(); 86 | loaderFactory.addLoaderListener('module', { 87 | before: () => { 88 | cb(); 89 | }, 90 | }); 91 | 92 | const manifest = require('./fixtures/module_with_ts/src/index').default; 93 | await loaderFactory.loadManifest(manifest); 94 | expect(cb).toBeCalled(); 95 | }); 96 | 97 | it('should remove listener success', async () => { 98 | const container = initTestContainer(); 99 | const loaderFactory = container.get(LoaderFactory); 100 | const cb = jest.fn(); 101 | loaderFactory.addLoaderListener('module', { 102 | before: () => { 103 | cb(); 104 | }, 105 | }); 106 | 107 | loaderFactory.removeLoaderListener('module'); 108 | 109 | const manifest = require('./fixtures/module_with_ts/src/index').default; 110 | await loaderFactory.loadManifest(manifest); 111 | expect(cb).not.toBeCalled(); 112 | }); 113 | 114 | it('should remove listener success with stage', async () => { 115 | const container = initTestContainer(); 116 | const loaderFactory = container.get(LoaderFactory); 117 | const cb = jest.fn(); 118 | const afterCallback = jest.fn(); 119 | loaderFactory.addLoaderListener('module', { 120 | before: () => { 121 | cb(); 122 | }, 123 | after: () => { 124 | afterCallback(); 125 | }, 126 | }); 127 | 128 | loaderFactory.removeLoaderListener('module', 'after'); 129 | 130 | const manifest = require('./fixtures/module_with_ts/src/index').default; 131 | await loaderFactory.loadManifest(manifest); 132 | expect(cb).toBeCalled(); 133 | expect(afterCallback).not.toBeCalled(); 134 | }); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /test/logger.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { ArtusApplication, ArtusInjectEnum, Manifest } from '../src'; 3 | import { LoggerLevel, LoggerOptions } from '../src/logger'; 4 | 5 | import TestLoggerClazz from './fixtures/logger/src/test_clazz'; 6 | import TestCustomLoggerClazz from './fixtures/logger/src/test_custom_clazz'; 7 | import { DEFAULT_EMPTY_MANIFEST } from './utils'; 8 | 9 | interface AppConfigWithLoggerOptions extends Record { 10 | logger?: LoggerOptions; 11 | } 12 | 13 | const _getAppWithConfig = async (config: AppConfigWithLoggerOptions = {}, manifest: Manifest = DEFAULT_EMPTY_MANIFEST) => { 14 | const app = new ArtusApplication(); 15 | await app.load(manifest); 16 | app.container.set({ 17 | id: ArtusInjectEnum.Config, 18 | value: config, 19 | }); 20 | return app; 21 | }; 22 | 23 | const err = new Error('test'); 24 | 25 | describe('test/logger.test.ts', () => { 26 | beforeEach(() => { 27 | console.info = jest.fn(); 28 | console.warn = jest.fn(); 29 | console.error = jest.fn(); 30 | console.debug = jest.fn(); 31 | console.trace = jest.fn(); 32 | }); 33 | 34 | describe('log message with default level (INFO)', () => { 35 | it('should log message with Logger from Contianer', async () => { 36 | const { default: manifest } = require('./fixtures/logger/src'); 37 | const app = await _getAppWithConfig({}, manifest); 38 | 39 | const testClazz = app.container.get(TestLoggerClazz); 40 | 41 | testClazz.testTrace('trace', 0, {}); 42 | expect(console.trace).toBeCalledTimes(0); 43 | 44 | testClazz.testDebug('debug', 1, {}); 45 | expect(console.debug).toBeCalledTimes(0); 46 | 47 | testClazz.testInfo('info', 2, {}); 48 | expect(console.info).toBeCalledTimes(1); 49 | expect(console.info).toBeCalledWith('info', 2, {}); 50 | 51 | testClazz.testWarn('warn', 3, {}); 52 | expect(console.warn).toBeCalledTimes(1); 53 | expect(console.warn).toBeCalledWith('warn', 3, {}); 54 | 55 | testClazz.testError('error', 4, {}); 56 | testClazz.testError(err, 5, {}); 57 | expect(console.error).toBeCalledTimes(2); 58 | expect(console.error).toBeCalledWith('error', 4, {}); 59 | expect(console.error).toBeCalledWith(err, 5, {}); 60 | }); 61 | 62 | it('should log message with Logger from Contianer and log method', async () => { 63 | const { default: manifest } = require('./fixtures/logger/src'); 64 | const app = await _getAppWithConfig({}, manifest); 65 | 66 | const testClazz = app.container.get(TestLoggerClazz); 67 | 68 | testClazz.testLog(LoggerLevel.TRACE, 'trace', 0, {}); 69 | expect(console.trace).toBeCalledTimes(0); 70 | 71 | testClazz.testLog(LoggerLevel.DEBUG, 'debug', 1, {}); 72 | expect(console.debug).toBeCalledTimes(0); 73 | 74 | testClazz.testLog(LoggerLevel.INFO, 'info', 2, {}); 75 | expect(console.info).toBeCalledTimes(1); 76 | expect(console.info).toBeCalledWith('info', 2, {}); 77 | 78 | testClazz.testLog(LoggerLevel.WARN, 'warn', 3, {}); 79 | expect(console.warn).toBeCalledTimes(1); 80 | expect(console.warn).toBeCalledWith('warn', 3, {}); 81 | 82 | testClazz.testLog(LoggerLevel.ERROR, 'error', 4, {}); 83 | testClazz.testLog(LoggerLevel.ERROR, err, 5, {}); 84 | expect(console.error).toBeCalledTimes(2); 85 | expect(console.error).toBeCalledWith('error', 4, {}); 86 | expect(console.error).toBeCalledWith(err, 5, {}); 87 | }); 88 | }); 89 | 90 | describe('log message with custom level (TRACE)', () => { 91 | it('should log message with Logger from Contianer', async () => { 92 | const { default: manifest } = require('./fixtures/logger/src'); 93 | const app = await _getAppWithConfig({ 94 | logger: { 95 | level: LoggerLevel.TRACE, 96 | }, 97 | }, manifest); 98 | 99 | const testClazz = app.container.get(TestLoggerClazz); 100 | 101 | testClazz.testTrace('trace', 0, {}); 102 | expect(console.trace).toBeCalledTimes(1); 103 | expect(console.trace).toBeCalledWith('trace', 0, {}); 104 | 105 | testClazz.testDebug('debug', 1, {}); 106 | expect(console.debug).toBeCalledTimes(1); 107 | expect(console.debug).toBeCalledWith('debug', 1, {}); 108 | 109 | testClazz.testInfo('info', 2, {}); 110 | expect(console.info).toBeCalledTimes(1); 111 | expect(console.info).toBeCalledWith('info', 2, {}); 112 | 113 | testClazz.testWarn('warn', 3, {}); 114 | expect(console.warn).toBeCalledTimes(1); 115 | expect(console.warn).toBeCalledWith('warn', 3, {}); 116 | 117 | testClazz.testError('error', 4, {}); 118 | testClazz.testError(err, 5, {}); 119 | expect(console.error).toBeCalledTimes(2); 120 | expect(console.error).toBeCalledWith('error', 4, {}); 121 | expect(console.error).toBeCalledWith(err, 5, {}); 122 | }); 123 | 124 | it('should log message with Logger from Contianer and log method', async () => { 125 | const { default: manifest } = require('./fixtures/logger/src'); 126 | const app = await _getAppWithConfig({ 127 | logger: { 128 | level: LoggerLevel.TRACE, 129 | }, 130 | }, manifest); 131 | 132 | const testClazz = app.container.get(TestLoggerClazz); 133 | 134 | testClazz.testLog(LoggerLevel.TRACE, 'trace', 0, {}); 135 | expect(console.trace).toBeCalledTimes(1); 136 | expect(console.trace).toBeCalledWith('trace', 0, {}); 137 | 138 | testClazz.testLog(LoggerLevel.DEBUG, 'debug', 1, {}); 139 | expect(console.debug).toBeCalledTimes(1); 140 | expect(console.debug).toBeCalledWith('debug', 1, {}); 141 | 142 | testClazz.testLog(LoggerLevel.INFO, 'info', 2, {}); 143 | expect(console.info).toBeCalledTimes(1); 144 | expect(console.info).toBeCalledWith('info', 2, {}); 145 | 146 | testClazz.testLog(LoggerLevel.WARN, 'warn', 3, {}); 147 | expect(console.warn).toBeCalledTimes(1); 148 | expect(console.warn).toBeCalledWith('warn', 3, {}); 149 | 150 | testClazz.testLog(LoggerLevel.ERROR, 'error', 4, {}); 151 | testClazz.testLog(LoggerLevel.ERROR, err, 5, {}); 152 | expect(console.error).toBeCalledTimes(2); 153 | expect(console.error).toBeCalledWith('error', 4, {}); 154 | expect(console.error).toBeCalledWith(err, 5, {}); 155 | }); 156 | }); 157 | 158 | describe('log message with custom Logger', () => { 159 | it('should log message with custom method', async () => { 160 | const { manifestWithCustomLogger: manifest } = require('./fixtures/logger/src'); 161 | const app = await _getAppWithConfig({}, manifest); 162 | const testClazz = app.container.get(TestCustomLoggerClazz); 163 | 164 | testClazz.testInfo('info', 1, {}); 165 | expect(console.info).toBeCalledTimes(1); 166 | expect(console.info).toBeCalledWith('[Custom]', 'info', 1, {}); 167 | }); 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /test/plugin.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import path from 'path'; 3 | import { Logger, Plugin, PluginFactory } from '../src'; 4 | 5 | const pluginPrefix = 'fixtures/plugins'; 6 | 7 | describe('test/plugin.test.ts', () => { 8 | describe('app with config', () => { 9 | it('should load plugin with dep order', async () => { 10 | const mockPluginConfig = { 11 | 'plugin-a': { 12 | enable: true, 13 | path: path.resolve(__dirname, `${pluginPrefix}/plugin_a`), 14 | }, 15 | 'plugin-b': { 16 | enable: true, 17 | path: path.resolve(__dirname, `${pluginPrefix}/plugin_b`), 18 | }, 19 | 'plugin-c': { 20 | enable: true, 21 | path: path.resolve(__dirname, `${pluginPrefix}/plugin_c`), 22 | }, 23 | 'plugin-d': { 24 | enable: true, 25 | path: path.resolve(__dirname, `${pluginPrefix}/plugin_d`), 26 | }, 27 | }; 28 | const pluginList = await PluginFactory.createFromConfig(mockPluginConfig); 29 | expect(pluginList.length).toEqual(4); 30 | pluginList.forEach(plugin => { 31 | expect(plugin).toBeInstanceOf(Plugin); 32 | expect(plugin.enable).toBeTruthy(); 33 | }); 34 | expect(pluginList.map(plugin => plugin.name)).toStrictEqual(['plugin-c', 'plugin-b', 'plugin-a', 'plugin-d']); 35 | }); 36 | 37 | it('should not load plugin with wrong order', async () => { 38 | const mockPluginConfig = { 39 | 'plugin-a': { 40 | enable: true, 41 | path: path.resolve(__dirname, `${pluginPrefix}/plugin_a`), 42 | }, 43 | 'plugin-b': { 44 | enable: true, 45 | path: path.resolve(__dirname, `${pluginPrefix}/plugin_b`), 46 | }, 47 | 'plugin-c': { 48 | enable: true, 49 | path: path.resolve(__dirname, `${pluginPrefix}/plugin_c`), 50 | }, 51 | 'plugin-d': { 52 | enable: true, 53 | path: path.resolve(__dirname, `${pluginPrefix}/plugin_d`), 54 | }, 55 | 'plugin-wrong-a': { 56 | enable: true, 57 | path: path.resolve(__dirname, `${pluginPrefix}/plugin_wrong_a`), 58 | }, 59 | 'plugin-wrong-b': { 60 | enable: true, 61 | path: path.resolve(__dirname, `${pluginPrefix}/plugin_wrong_b`), 62 | }, 63 | }; 64 | await expect(async () => { 65 | await PluginFactory.createFromConfig(mockPluginConfig); 66 | }).rejects.toThrowError(new Error(`Circular dependency found in plugins: plugin-wrong-a, plugin-wrong-b`)); 67 | }); 68 | 69 | it('should throw if dependencies missing', async () => { 70 | const mockPluginConfig = { 71 | 'plugin-a': { 72 | enable: true, 73 | path: path.resolve(__dirname, `${pluginPrefix}/plugin_a`), 74 | }, 75 | }; 76 | expect(async () => { 77 | await PluginFactory.createFromConfig(mockPluginConfig); 78 | }).rejects.toThrowError(new Error(`Plugin plugin-a need have dependency: plugin-b.`)); 79 | }); 80 | 81 | it('should throw if dependence disabled', async () => { 82 | const mockPluginConfig = { 83 | 'plugin-a': { 84 | enable: true, 85 | path: path.resolve(__dirname, `${pluginPrefix}/plugin_a`), 86 | }, 87 | 'plugin-b': { 88 | enable: false, 89 | path: path.resolve(__dirname, `${pluginPrefix}/plugin_b`), 90 | }, 91 | 'plugin-c': { 92 | enable: true, 93 | path: path.resolve(__dirname, `${pluginPrefix}/plugin_c`), 94 | }, 95 | }; 96 | await expect(async () => { 97 | await PluginFactory.createFromConfig(mockPluginConfig); 98 | }).rejects.toThrowError(new Error(`Plugin plugin-a need have dependency: plugin-b.`)); 99 | }); 100 | 101 | it('should not throw if optional dependence missing', async () => { 102 | const mockPluginConfig = { 103 | 'plugin-d': { 104 | enable: true, 105 | path: path.resolve(__dirname, `${pluginPrefix}/plugin_d`), 106 | }, 107 | }; 108 | 109 | // mock warn 110 | const originWarn = console.warn; 111 | const mockWarnFn = jest.fn(); 112 | console.warn = mockWarnFn; 113 | const pluginList = await PluginFactory.createFromConfig(mockPluginConfig, { 114 | logger: new Logger(), 115 | }); 116 | expect(pluginList.length).toEqual(1); 117 | pluginList.forEach(plugin => { 118 | expect(plugin).toBeInstanceOf(Plugin); 119 | expect(plugin.enable).toBeTruthy(); 120 | }); 121 | expect(mockWarnFn).toBeCalledWith(`Plugin plugin-d need have optional dependency: plugin-c.`); 122 | 123 | // restore warn 124 | console.warn = originWarn; 125 | }); 126 | 127 | it('should not throw if optional dependence disabled', async () => { 128 | const mockPluginConfig = { 129 | 'plugin-b': { 130 | enable: false, 131 | path: path.resolve(__dirname, `${pluginPrefix}/plugin_b`), 132 | }, 133 | 'plugin-d': { 134 | enable: true, 135 | path: path.resolve(__dirname, `${pluginPrefix}/plugin_d`), 136 | }, 137 | }; 138 | 139 | // mock warn 140 | const originWarn = console.warn; 141 | const mockWarnFn = jest.fn(); 142 | console.warn = mockWarnFn; 143 | const pluginList = await PluginFactory.createFromConfig(mockPluginConfig, { 144 | logger: new Logger(), 145 | }); 146 | expect(pluginList.length).toEqual(1); 147 | pluginList.forEach(plugin => { 148 | expect(plugin).toBeInstanceOf(Plugin); 149 | expect(plugin.enable).toBeTruthy(); 150 | }); 151 | expect(mockWarnFn).toBeCalledWith(`Plugin plugin-d need have optional dependency: plugin-c.`); 152 | 153 | // restore warn 154 | console.warn = originWarn; 155 | }); 156 | 157 | it('should calc order if optional dependence enabled', async () => { 158 | const mockPluginConfig = { 159 | 'plugin-d': { 160 | enable: true, 161 | path: path.resolve(__dirname, `${pluginPrefix}/plugin_d`), 162 | }, 163 | 'plugin-c': { 164 | enable: true, 165 | path: path.resolve(__dirname, `${pluginPrefix}/plugin_c`), 166 | }, 167 | }; 168 | 169 | const pluginList = await PluginFactory.createFromConfig(mockPluginConfig, { 170 | logger: new Logger(), 171 | }); 172 | expect(pluginList.length).toEqual(2); 173 | pluginList.forEach(plugin => { 174 | expect(plugin).toBeInstanceOf(Plugin); 175 | expect(plugin.enable).toBeTruthy(); 176 | }); 177 | expect(pluginList.map(plugin => plugin.name)).toStrictEqual(['plugin-c', 'plugin-d']); 178 | }); 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /test/scanner.test.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { ArtusScanner } from '../src/scanner'; 3 | import os from 'os'; 4 | import path from 'path'; 5 | import { DEFAULT_APP_REF, ScanPolicy } from '../src'; 6 | import { formatManifestForWindowsTest } from './utils'; 7 | 8 | describe('test/scanner.test.ts', () => { 9 | it('should be scan application', async () => { 10 | const scanner = new ArtusScanner({ needWriteFile: false, extensions: ['.ts'] }); 11 | let manifest = await scanner.scan(path.resolve(__dirname, './fixtures/app_koa_with_ts')); 12 | expect(manifest.version).toBe('2'); 13 | 14 | manifest = formatManifestForWindowsTest(manifest); 15 | expect(manifest).toMatchSnapshot(); 16 | 17 | // scan with relative root 18 | const relativeRoot = path.relative(process.cwd(), __dirname); 19 | let relativeRootManifest = await scanner.scan(path.join(relativeRoot, './fixtures/app_koa_with_ts')); 20 | relativeRootManifest = formatManifestForWindowsTest(relativeRootManifest); 21 | expect(relativeRootManifest).toStrictEqual(manifest); 22 | }); 23 | 24 | it('should scan application with all injectable class', async () => { 25 | const scanner = new ArtusScanner({ needWriteFile: false, extensions: ['.ts'] }); 26 | const manifest = await scanner.scan(path.resolve(__dirname, './fixtures/named_export')); 27 | expect(manifest?.refMap?.[DEFAULT_APP_REF]?.items).toBeDefined(); 28 | expect(manifest.refMap?.[DEFAULT_APP_REF]?.items.length).toBe(4); 29 | }); 30 | 31 | it('should scan application with named export class', async () => { 32 | const scanner = new ArtusScanner({ needWriteFile: false, extensions: ['.ts'], policy: ScanPolicy.NamedExport }); 33 | const manifest = await scanner.scan(path.resolve(__dirname, './fixtures/named_export')); 34 | expect(manifest?.refMap?.[DEFAULT_APP_REF]?.items).toBeDefined(); 35 | expect(manifest?.refMap?.[DEFAULT_APP_REF]?.items.length).toBe(4); 36 | }); 37 | 38 | it('should scan application with default export class', async () => { 39 | const scanner = new ArtusScanner({ needWriteFile: false, extensions: ['.ts'], policy: ScanPolicy.DefaultExport }); 40 | const manifest = await scanner.scan(path.resolve(__dirname, './fixtures/named_export')); 41 | expect(manifest?.refMap?.[DEFAULT_APP_REF]?.items).toBeDefined(); 42 | expect(manifest?.refMap?.[DEFAULT_APP_REF]?.items.length).toBe(2); 43 | }); 44 | 45 | it('should not throw when scan application without configdir', async () => { 46 | const scanner = new ArtusScanner({ needWriteFile: false, extensions: ['.ts'] }); 47 | const manifest = await scanner.scan(path.resolve(__dirname, './fixtures/app_without_config')); 48 | expect(manifest?.refMap?.[DEFAULT_APP_REF]?.items?.find(item => item.loader === 'config')).toBeUndefined(); 49 | }); 50 | 51 | it('should scan application with nesting preset a which defined in options', async () => { 52 | const scanner = new ArtusScanner({ 53 | needWriteFile: false, 54 | configDir: 'config', 55 | extensions: ['.ts'], 56 | plugin: { 57 | preset_a: { 58 | enable: true, 59 | path: path.resolve(__dirname, './fixtures/plugins/preset_a'), 60 | }, 61 | }, 62 | }); 63 | let manifest = await scanner.scan(path.resolve(__dirname, './fixtures/app_empty')); 64 | manifest = formatManifestForWindowsTest(manifest); 65 | expect(manifest).toMatchSnapshot(); 66 | }); 67 | 68 | it('should scan application with single preset b which defined in config', async () => { 69 | const scanner = new ArtusScanner({ needWriteFile: false, extensions: ['.ts'], configDir: 'config' }); 70 | let manifest = await scanner.scan(path.resolve(__dirname, './fixtures/app_with_preset_b')); 71 | manifest = formatManifestForWindowsTest(manifest); 72 | expect(manifest).toMatchSnapshot(); 73 | }); 74 | 75 | it('should scan application with single preset c which defined in options', async () => { 76 | const scanner = new ArtusScanner({ 77 | needWriteFile: false, 78 | configDir: 'config', 79 | extensions: ['.ts'], 80 | plugin: { 81 | preset_c: { 82 | enable: true, 83 | path: path.resolve(__dirname, './fixtures/plugins/preset_c'), 84 | }, 85 | }, 86 | }); 87 | let manifest = await scanner.scan(path.resolve(__dirname, './fixtures/app_empty')); 88 | manifest = formatManifestForWindowsTest(manifest); 89 | expect(manifest).toMatchSnapshot(); 90 | }); 91 | 92 | it('should find multipie version and fail', async () => { 93 | const scanner = new ArtusScanner({ 94 | needWriteFile: false, 95 | configDir: 'config', 96 | extensions: ['.ts'], 97 | plugin: { 98 | a: { 99 | enable: true, 100 | refName: 'test', 101 | path: path.resolve(__dirname, './fixtures/plugins/plugin_a_other_ver'), 102 | }, 103 | }, 104 | }); 105 | await expect(scanner.scan(path.resolve(__dirname, './fixtures/app_with_plugin_version_check'))).rejects.toThrowError(new Error('test has multi version of 0.0.1-alpha.0, 0.0.1')); 106 | }); 107 | it('should find multi path with same version and fail', async () => { 108 | const scanner = new ArtusScanner({ 109 | needWriteFile: false, 110 | configDir: 'config', 111 | extensions: ['.ts'], 112 | plugin: { 113 | a: { 114 | enable: true, 115 | refName: 'test', 116 | path: path.resolve(__dirname, './fixtures/plugins/plugin_a_same_ver'), 117 | }, 118 | }, 119 | }); 120 | await expect(scanner.scan(path.resolve(__dirname, './fixtures/app_with_plugin_version_check'))).rejects.toThrowError( 121 | new Error( 122 | os.platform() !== 'win32' ? 123 | `test has multi path with same version in ../plugins/plugin_a_same_ver and ../plugins/plugin_a` : 124 | `test has multi path with same version in ..\\plugins\\plugin_a_same_ver and ..\\plugins\\plugin_a`, 125 | ), 126 | ); 127 | }); 128 | 129 | }); 130 | 131 | -------------------------------------------------------------------------------- /test/utils/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import os from 'os'; 3 | import { ArtusScanner, ArtusApplication, Manifest, RefMap, PluginConfig } from '../../src'; 4 | 5 | export const DEFAULT_EMPTY_MANIFEST: Manifest = { 6 | version: '2', 7 | refMap: {}, 8 | }; 9 | 10 | export async function createApp(baseDir: string) { 11 | const scanner = new ArtusScanner({ 12 | needWriteFile: false, 13 | configDir: 'config', 14 | extensions: ['.ts'], 15 | }); 16 | const manifest = await scanner.scan(baseDir); 17 | 18 | const app = new ArtusApplication(); 19 | await app.load(manifest, baseDir); 20 | await app.run(); 21 | 22 | return app; 23 | } 24 | 25 | export function formatManifestForWindowsTest(manifest: Manifest) { 26 | if (os.platform() !== 'win32') { 27 | return manifest; 28 | } 29 | // A regexp for convert win32 path delimiter to POSIX style 30 | const pathReg = /\\/g; 31 | const newRefMap: RefMap = {}; 32 | const handlePluginConfig = (pluginConfig: PluginConfig) => { 33 | return Object.fromEntries(Object.entries(pluginConfig).map(([pluginName, pluginConfigItem]) => { 34 | if (pluginConfigItem.refName) { 35 | pluginConfigItem.refName = pluginConfigItem.refName.replace(pathReg, '/'); 36 | } 37 | return [pluginName, pluginConfigItem]; 38 | })); 39 | }; 40 | for (const [refName, refItem] of Object.entries(manifest.refMap)) { 41 | for (const pluginConfig of Object.values(refItem.pluginConfig)) { 42 | for (const pluginConfigItem of Object.values(pluginConfig)) { 43 | if (!pluginConfigItem.refName) { 44 | continue; 45 | } 46 | pluginConfigItem.refName = pluginConfigItem.refName.replace(pathReg, '/'); 47 | } 48 | } 49 | newRefMap[refName.replace(pathReg, '/')] = { 50 | ...refItem, 51 | relativedPath: refItem.relativedPath.replace(pathReg, '/'), 52 | items: refItem.items.map(item => ({ 53 | ...item, 54 | path: item.path.replace(pathReg, '/'), 55 | })), 56 | pluginConfig: Object.fromEntries(Object.entries(refItem.pluginConfig).map( 57 | ([env, pluginConfig]) => [env, handlePluginConfig(pluginConfig)], 58 | )), 59 | }; 60 | } 61 | manifest.refMap = newRefMap; 62 | manifest.extraPluginConfig = handlePluginConfig(manifest.extraPluginConfig); 63 | return manifest; 64 | } 65 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src" 5 | }, 6 | "exclude": [ 7 | "lib", 8 | "test", 9 | "**/*.test.ts" 10 | ] 11 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@artus/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "types": [ 6 | "node", 7 | "jest", 8 | "reflect-metadata" 9 | ], 10 | "module": "NodeNext", 11 | "moduleResolution": "NodeNext" 12 | }, 13 | "exclude": [ 14 | "lib", 15 | ], 16 | } --------------------------------------------------------------------------------