├── .env.example ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.json ├── README.md ├── example ├── .env ├── .env.example ├── nest-cli.json ├── package.json ├── src │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── configs │ │ └── app.config.ts │ └── main.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock ├── index.js ├── index.ts ├── package.json ├── src ├── __tests__ │ ├── .env │ ├── env-decorator.spec.ts │ ├── env-module.spec.ts │ ├── test-service.ts │ └── test.config.ts ├── cli.ts ├── index.ts └── module │ ├── env-not-found.exception.ts │ ├── env.decorator.ts │ └── env.module.ts ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | # example/src/configs/app.config.ts 2 | PORT= 3 | ENV_STRING="default env string" 4 | JSON_VARIABLE= -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'prettier', 12 | 'prettier/@typescript-eslint', 13 | ], 14 | root: true, 15 | env: { 16 | node: true, 17 | }, 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/no-explicit-any': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-unused-vars': 'off', 24 | '@typescript-eslint/ban-types': 'off', 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDEs and editors 2 | /.idea 3 | .project 4 | .classpath 5 | .c9/ 6 | *.launch 7 | .settings/ 8 | *.sublime-workspace 9 | 10 | # IDE - VSCode 11 | .idea/ 12 | .vscode/* 13 | !.vscode/settings.json 14 | !.vscode/tasks.json 15 | !.vscode/launch.json 16 | !.vscode/extensions.json 17 | src/__tests__/coverage 18 | 19 | node_modules/ 20 | dist/ 21 | example/node_modules/ 22 | example/dist 23 | src/__tests__/ 24 | 25 | # OS 26 | .DS_Store 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | yarn.lock 3 | example/ 4 | src/ 5 | node_modules/ 6 | .idea/ 7 | src/__tests__/coverage 8 | dist/__tests__/ 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "arrowParens": "avoid", 4 | "trailingComma": "all", 5 | "tabWidth": 2 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Install 2 | 3 | **Yarn** 4 | 5 | ```bash 6 | yarn add nestjs-env 7 | ``` 8 | 9 | **NPM** 10 | 11 | ```bash 12 | npm install nestjs-env --save 13 | ``` 14 | 15 | > This library is not responsible for loading environment variables from a file, for this you need to use `env-cmd` or `dotenv`. 16 | 17 | ### Getting Started 18 | 19 | Let's imagine that we have a folder called `src/config` in our project that contains several configuration files. 20 | 21 | ```bash 22 | /src 23 | ├── app.module.ts 24 | ├── app.service.ts 25 | ├── config 26 | │ ├── app.config.ts 27 | │ └── index.ts 28 | bootstrap.ts 29 | ``` 30 | 31 | Example app.config.ts 32 | 33 | ```ts 34 | import { Env } from 'nestjs-env'; 35 | 36 | export class AppConfig { 37 | @Env('PORT', { default: 3000 }) 38 | port: number; 39 | 40 | @Env('NODE_ENV') 41 | env: string; 42 | 43 | get isDevelopment() { 44 | return this.env === 'development'; 45 | } 46 | } 47 | ``` 48 | 49 | Let's register the config module in `app.module.ts` 50 | 51 | ```ts 52 | import { Module } from '@nestjs/common'; 53 | import { EnvModule } from 'nestjs-env'; 54 | import { AppConfig } from 'src/config'; 55 | 56 | @Module({ 57 | imports: [EnvModule.register([AppConfig])], 58 | }) 59 | export class AppModule {} 60 | ``` 61 | 62 | ### Usage 63 | 64 | Now we are ready to inject our `AppConfig` anywhere we'd like. 65 | 66 | ```ts 67 | import { AppConfig } from 'src/config'; 68 | 69 | @Injectable() 70 | class AppService { 71 | constructor(private readonly appConfig: AppConfig) { 72 | console.log(this.appConfig.isDevelopment); 73 | } 74 | } 75 | ``` 76 | 77 | ```ts 78 | import { AppConfig } from 'src/config'; 79 | 80 | async function bootstrap() { 81 | const app = await NestFactory.create(AppModule); 82 | const config = app.get(AppConfig); 83 | 84 | await app.listen(config.port); 85 | } 86 | ``` 87 | 88 | That's it! 89 | 90 | ### CLI 91 | 92 | The nestjs-env CLI is a command-line interface tool that helps you to build env example file. 93 | 94 | ```bash 95 | $ nestjs-env generate 96 | ``` 97 | 98 | | Options | Alias | Description | Default | 99 | |-------------|-------|--------------------------------------------------------------|--------------| 100 | | --filename | -f | Name of the file to which the example will be written. | .env.example | 101 | | --pattern | -p | Template string specifying the names of files with configs | .config.ts | 102 | | --ignore | -i | Specify directory that should be excluded | node_modules | 103 | | --directory | -d | Specifies the base directory from which file scanning begins | src/ | 104 | | --output | -o | Specify an output folder for generated file | | 105 | | --print | | Prints an output to the console | false | 106 | 107 | --- 108 | 109 | ## License 110 | 111 | The MIT License (MIT) 112 | -------------------------------------------------------------------------------- /example/.env: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | JSON_VARIABLE={"key":"json value"} 3 | -------------------------------------------------------------------------------- /example/.env.example: -------------------------------------------------------------------------------- 1 | # src/app.config.ts 2 | PORT= 3 | ENV_STRING="default env string" 4 | JSON_VARIABLE= -------------------------------------------------------------------------------- /example/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "license": "MIT", 7 | "scripts": { 8 | "prebuild": "rimraf dist", 9 | "build": "nest build", 10 | "build:env-example": "nestjs-env generate", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "env-cmd nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@nestjs/common": "^6.7.2", 24 | "@nestjs/core": "^6.7.2", 25 | "@nestjs/platform-express": "^6.7.2", 26 | "env-cmd": "^10.0.1", 27 | "nestjs-env": "^2.1.0", 28 | "reflect-metadata": "^0.1.13", 29 | "rimraf": "^3.0.0", 30 | "rxjs": "^6.5.3" 31 | }, 32 | "devDependencies": { 33 | "@nestjs/cli": "^6.9.0", 34 | "@nestjs/schematics": "^6.7.0", 35 | "@nestjs/testing": "^6.7.1", 36 | "@types/express": "^4.17.1", 37 | "@types/jest": "^24.0.18", 38 | "@types/node": "^12.7.5", 39 | "@types/supertest": "^2.0.8", 40 | "jest": "^24.9.0", 41 | "prettier": "^1.18.2", 42 | "supertest": "^4.0.2", 43 | "ts-jest": "^24.1.0", 44 | "ts-loader": "^6.1.1", 45 | "ts-node": "^8.4.1", 46 | "tsconfig-paths": "^3.9.0", 47 | "tslint": "^5.20.0", 48 | "typescript": "^3.6.3" 49 | }, 50 | "jest": { 51 | "moduleFileExtensions": [ 52 | "js", 53 | "json", 54 | "ts" 55 | ], 56 | "rootDir": "src", 57 | "testRegex": ".spec.ts$", 58 | "transform": { 59 | "^.+\\.(t|j)s$": "ts-jest" 60 | }, 61 | "coverageDirectory": "./coverage", 62 | "testEnvironment": "node" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /example/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /example/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EnvModule } from 'nestjs-env'; 3 | 4 | import { AppConfig } from './configs/app.config'; 5 | import { AppController } from './app.controller'; 6 | import { AppService } from './app.service'; 7 | 8 | @Module({ 9 | imports: [EnvModule.register([AppConfig])], 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }) 13 | export class AppModule {} 14 | -------------------------------------------------------------------------------- /example/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { AppConfig } from './configs/app.config'; 4 | 5 | @Injectable() 6 | export class AppService { 7 | constructor(private appConfig: AppConfig) {} 8 | 9 | getHello(): string { 10 | return `Hello World! ${this.appConfig.envString}`; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/src/configs/app.config.ts: -------------------------------------------------------------------------------- 1 | import { Env } from 'nestjs-env'; 2 | 3 | interface JsonVariable { 4 | key: string; 5 | } 6 | 7 | export class AppConfig { 8 | @Env('PORT') 9 | port: number; 10 | 11 | @Env('ENV_STRING', { default: 'default env string' }) 12 | envString: string; 13 | 14 | @Env('JSON_VARIABLE') 15 | jsonVariable: JsonVariable; 16 | } 17 | -------------------------------------------------------------------------------- /example/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | 3 | import { AppModule } from './app.module'; 4 | import { AppConfig } from './configs/app.config'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | const appConfig = app.get(AppConfig); 9 | 10 | console.log('port', appConfig.port, typeof appConfig.port); 11 | console.log('envString', appConfig.envString, typeof appConfig.envString); 12 | console.log( 13 | 'jsonVariable', 14 | appConfig.jsonVariable, 15 | typeof appConfig.jsonVariable, 16 | ); 17 | 18 | await app.listen(appConfig.port); 19 | } 20 | 21 | bootstrap(); 22 | -------------------------------------------------------------------------------- /example/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true 13 | }, 14 | "include": ["./src/**/*.ts"], 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function __export(m) { 3 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; 4 | } 5 | exports.__esModule = true; 6 | __export(require("./dist")); 7 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './dist'; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-env", 3 | "version": "2.1.1", 4 | "description": "Using this library you can easily work with environment variables", 5 | "author": "Antsiferov Maxim ", 6 | "license": "MIT", 7 | "bin": { 8 | "nestjs-env": "node dist/cli.js" 9 | }, 10 | "main": "dist/index.js", 11 | "types": "dist/index.d.ts", 12 | "files": [ 13 | "dist" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/AntsiferovMaxim/nestjs-env" 18 | }, 19 | "scripts": { 20 | "build": "rm -rf ./dist && tsc -p tsconfig.json", 21 | "prepublish": "yarn build", 22 | "lint": "eslint 'src/**/*.ts' --ignore-pattern 'src/**/*.spec.ts'", 23 | "format": "prettier \"**/*.ts\" --ignore-path ./.prettierignore --write && git status", 24 | "test": "env-cmd -f ./src/__tests__/.env jest", 25 | "test:watch": "env-cmd -f ./src/__tests__/.env jest --watch", 26 | "test:coverage": "env-cmd -f ./src/__tests__/.env jest --coverage" 27 | }, 28 | "devDependencies": { 29 | "@nestjs/common": "^7.6.7", 30 | "@nestjs/core": "^7.6.7", 31 | "@nestjs/platform-express": "^7.6.7", 32 | "@nestjs/testing": "^7.6.7", 33 | "@types/jest": "^24.0.23", 34 | "@types/node": "^14.14.22", 35 | "@types/recursive-readdir": "^2.2.1", 36 | "@typescript-eslint/eslint-plugin": "^4.14.1", 37 | "@typescript-eslint/parser": "^4.14.1", 38 | "env-cmd": "^10.0.1", 39 | "eslint": "^7.18.0", 40 | "eslint-config-prettier": "^7.2.0", 41 | "eslint-plugin-import": "^2.22.1", 42 | "husky": "^4.3.8", 43 | "jest": "^24.9.0", 44 | "lint-staged": "^10.5.3", 45 | "prettier": "^2.2.1", 46 | "reflect-metadata": "^0.1.13", 47 | "rxjs": "^6.5.3", 48 | "supertest": "^4.0.2", 49 | "ts-jest": "^24.2.0", 50 | "typescript": "^3.7.2" 51 | }, 52 | "peerDependencies": { 53 | "@nestjs/common": "*" 54 | }, 55 | "husky": { 56 | "hooks": { 57 | "pre-commit": "lint-staged" 58 | } 59 | }, 60 | "lint-staged": { 61 | "src/**/*.{ts,json}": [ 62 | "prettier --ignore-path ./.prettierignore --write" 63 | ] 64 | }, 65 | "jest": { 66 | "moduleFileExtensions": [ 67 | "js", 68 | "json", 69 | "ts" 70 | ], 71 | "rootDir": "src", 72 | "testRegex": ".spec.ts$", 73 | "transform": { 74 | "^.+\\.(t|j)s$": "ts-jest" 75 | }, 76 | "coverageDirectory": "./__tests__/coverage", 77 | "testEnvironment": "node" 78 | }, 79 | "keywords": [ 80 | "nestjs", 81 | "config", 82 | "nestjs-env", 83 | "env" 84 | ], 85 | "dependencies": { 86 | "recursive-readdir": "^2.2.3", 87 | "yargs": "^17.6.2" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/__tests__/.env: -------------------------------------------------------------------------------- 1 | TEST_NUMBER=1 2 | TEST_NUMBER_ZERO=0 3 | TEST_STRING=test_string 4 | TEST_BOOLEAN_FALSE=false 5 | TEST_BOOLEAN_TRUE=true 6 | TEST_JSON={"key":{"test": "value"}} 7 | -------------------------------------------------------------------------------- /src/__tests__/env-decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import { Env, EnvNotFound } from '..'; 2 | import { TestConfig } from './test.config'; 3 | 4 | describe('Env decorator', () => { 5 | const testConfig = new TestConfig(); 6 | 7 | describe('Number', () => { 8 | it('should return number type', () => { 9 | expect(typeof testConfig.testNumber).toBe('number'); 10 | }); 11 | 12 | it('should return 1', () => { 13 | expect(testConfig.testNumber).toBe(1); 14 | }); 15 | 16 | it('should return 0', () => { 17 | expect(testConfig.testNumberZero).toBe(0); 18 | }); 19 | }); 20 | 21 | describe('String', () => { 22 | it('should return string type', () => { 23 | expect(typeof testConfig.testString).toBe('string'); 24 | }); 25 | 26 | it('should return test_string', () => { 27 | expect(testConfig.testString).toBe('test_string'); 28 | }); 29 | }); 30 | 31 | describe('Boolean', () => { 32 | it('should return boolean type', () => { 33 | expect(typeof testConfig.testBooleanFalse).toBe('boolean'); 34 | }); 35 | 36 | it('should return true', () => { 37 | expect(testConfig.testBooleanTrue).toBe(true); 38 | }); 39 | 40 | it('should return false', () => { 41 | expect(testConfig.testBooleanFalse).toBe(false); 42 | }); 43 | }); 44 | 45 | describe('Json', () => { 46 | it('should return object type', () => { 47 | expect(typeof testConfig.testTestJson).toBe('object'); 48 | }); 49 | 50 | it('should return "value" string', () => { 51 | expect(testConfig.testTestJson.key.test).toBe('value'); 52 | }); 53 | }); 54 | 55 | describe('Default value', () => { 56 | it('should return 4 as default value', () => { 57 | expect(testConfig.testDefaultNumber).toBe(4); 58 | }); 59 | }); 60 | 61 | describe('Exception', () => { 62 | it('should throw exception', () => { 63 | expect(() => { 64 | class TestExceptionTest { 65 | @Env('TEST_EXCEPTION') 66 | testException: string; 67 | } 68 | 69 | new TestExceptionTest(); 70 | }).toThrow(EnvNotFound); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/__tests__/env-module.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { EnvModule } from '../module/env.module'; 3 | import { TestService } from './test-service'; 4 | import { TestConfig } from './test.config'; 5 | 6 | describe('Env module', () => { 7 | it('Will boot nest-env module successfully', async () => { 8 | const module = await Test.createTestingModule({ 9 | imports: [EnvModule.register([TestConfig])], 10 | providers: [TestService], 11 | }).compile(); 12 | const app = module.createNestApplication(); 13 | 14 | const testService = app.get(TestService); 15 | expect(testService.testMethod()).toBeTruthy(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/__tests__/test-service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { TestConfig } from './test.config'; 3 | 4 | @Injectable() 5 | export class TestService { 6 | constructor(private testConfig: TestConfig) {} 7 | 8 | testMethod(): string { 9 | return this.testConfig.testString; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/__tests__/test.config.ts: -------------------------------------------------------------------------------- 1 | import { Env } from '../module/env.decorator'; 2 | 3 | interface ITestJson { 4 | key: { 5 | test: string; 6 | }; 7 | } 8 | 9 | export class TestConfig { 10 | @Env('TEST_NUMBER') 11 | testNumber: number; 12 | 13 | @Env('TEST_NUMBER_ZERO') 14 | testNumberZero: number; 15 | 16 | @Env('TEST_STRING') 17 | testString: string; 18 | 19 | @Env('TEST_BOOLEAN_FALSE') 20 | testBooleanFalse: boolean; 21 | 22 | @Env('TEST_BOOLEAN_TRUE') 23 | testBooleanTrue: boolean; 24 | 25 | @Env('TEST_JSON') 26 | testTestJson: ITestJson; 27 | 28 | @Env('TEST_DEFAULT', { default: 4 }) 29 | testDefaultNumber: number; 30 | } 31 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as path from 'path'; 3 | import * as recursive from 'recursive-readdir'; 4 | import * as ts from 'typescript'; 5 | import * as yargs from 'yargs'; 6 | import { hideBin } from 'yargs/helpers'; 7 | import * as fs from 'fs'; 8 | import * as console from 'console'; 9 | 10 | type EnvItem = { 11 | name: string; 12 | default?: string | number | null; 13 | }; 14 | type ConfigItem = { file: string; envs: EnvItem[] }; 15 | 16 | function getNameOfProperty(payload: ts.PropertyAssignment): string | null { 17 | if (ts.isIdentifier(payload.name)) { 18 | return payload.name.escapedText.toString(); 19 | } else { 20 | return null; 21 | } 22 | } 23 | 24 | function getValueOfProperty(payload: ts.PropertyAssignment) { 25 | if (ts.isStringLiteral(payload.initializer)) { 26 | return payload.initializer.text; 27 | } else if (ts.isNumericLiteral(payload.initializer)) { 28 | return parseFloat(payload.initializer.text); 29 | } else if (payload.initializer.kind === ts.SyntaxKind.TrueKeyword) { 30 | return true; 31 | } else if (payload.initializer.kind === ts.SyntaxKind.FalseKeyword) { 32 | return false; 33 | } else if (ts.isObjectLiteralExpression(payload.initializer)) { 34 | return getPlaneObject(payload.initializer); 35 | } else { 36 | return null; 37 | } 38 | } 39 | 40 | function getPlaneObject(payload: ts.ObjectLiteralExpression): any { 41 | const object = {}; 42 | ts.forEachChild(payload, item => { 43 | if (ts.isPropertyAssignment(item)) { 44 | const name = getNameOfProperty(item); 45 | if (name !== null) { 46 | const value = getValueOfProperty(item); 47 | object[name] = value; 48 | } 49 | } 50 | }); 51 | return object; 52 | } 53 | 54 | function getEnvsFromFile(file: string) { 55 | const program = ts.createProgram([path.resolve(file)], {}); 56 | const source = program.getSourceFile(file); 57 | const envs: EnvItem[] = []; 58 | 59 | ts.forEachChild(source, node => { 60 | if (ts.isClassDeclaration(node)) { 61 | ts.forEachChild(node, member => { 62 | if (ts.isPropertyDeclaration(member)) { 63 | ts.forEachChild(member, decorator => { 64 | if (ts.isDecorator(decorator)) { 65 | ts.forEachChild(decorator, callExpression => { 66 | if (ts.isCallExpression(callExpression)) { 67 | if ( 68 | ts.isIdentifier(callExpression.expression) && 69 | callExpression.expression.escapedText === 'Env' 70 | ) { 71 | const [envName, envConfig] = callExpression.arguments; 72 | if (envConfig && ts.isObjectLiteralExpression(envConfig)) { 73 | } 74 | if (envName && ts.isStringLiteral(envName)) { 75 | const env: EnvItem = { name: envName.text }; 76 | if ( 77 | envConfig && 78 | ts.isObjectLiteralExpression(envConfig) 79 | ) { 80 | const config = getPlaneObject(envConfig); 81 | if (config) { 82 | env.default = config.default; 83 | } 84 | } 85 | envs.push(env); 86 | } 87 | } 88 | } 89 | }); 90 | } 91 | }); 92 | } 93 | }); 94 | } 95 | }); 96 | 97 | return envs; 98 | } 99 | 100 | function mapEnvItemToString(payload: EnvItem): string { 101 | const defaultValue = 102 | payload.default === undefined || payload.default === null 103 | ? '' 104 | : `"${payload.default.toString()}"`; 105 | 106 | return [payload.name, defaultValue].join('='); 107 | } 108 | 109 | function mapConfigItemToString(payload: ConfigItem): string { 110 | const envs = payload.envs.map(mapEnvItemToString).join('\n'); 111 | 112 | return [`# ${payload.file}`, envs].join('\n'); 113 | } 114 | 115 | yargs(hideBin(process.argv)).command<{ 116 | directory: string; 117 | filename: string; 118 | pattern: string; 119 | ignore: string[]; 120 | print: boolean; 121 | output?: string; 122 | }>( 123 | 'generate', 124 | 'Generate env example', 125 | payload => { 126 | return payload 127 | .option('filename', { 128 | description: 'Name of the file to which the example will be written.', 129 | alias: 'f', 130 | default: '.env.example', 131 | type: 'string', 132 | }) 133 | .option('pattern', { 134 | description: 135 | 'Template string specifying the names of files with configs', 136 | default: '.config.ts', 137 | alias: 'p', 138 | type: 'string', 139 | }) 140 | .option('ignore', { 141 | description: 'Specify directory that should be excluded', 142 | alias: 'i', 143 | type: 'array', 144 | default: ['node_modules'], 145 | }) 146 | .option('directory', { 147 | description: 148 | 'Specifies the base directory from which file scanning begins', 149 | alias: 'd', 150 | default: 'src/', 151 | type: 'string', 152 | }) 153 | .option('output', { 154 | description: 'Specify an output folder for generated file', 155 | alias: 'o', 156 | type: 'string', 157 | default: '', 158 | }) 159 | .option('print', { 160 | description: 'Prints an output to the console', 161 | default: false, 162 | type: 'boolean', 163 | }); 164 | }, 165 | async params => { 166 | const ignore = Array.isArray(params.ignore) ? params.ignore : []; 167 | const files = await recursive(params.directory, [ 168 | ...ignore, 169 | (file, stats) => { 170 | if (stats.isDirectory()) { 171 | return false; 172 | } 173 | return !path.basename(file).endsWith(params.pattern); 174 | }, 175 | ]); 176 | const configs: ConfigItem[] = []; 177 | for (const item of files) { 178 | const envs = await getEnvsFromFile(item); 179 | configs.push({ file: item, envs }); 180 | } 181 | const example = configs 182 | .filter(item => item.envs.length > 0) 183 | .map(mapConfigItemToString) 184 | .join('\n\n'); 185 | 186 | if (params.print) { 187 | console.log(example); 188 | } else { 189 | await fs.promises.writeFile( 190 | path.resolve(params.output, params.filename), 191 | example, 192 | ); 193 | } 194 | }, 195 | ).argv; 196 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Env } from './module/env.decorator'; 2 | import { EnvModule } from './module/env.module'; 3 | import { EnvNotFound } from './module/env-not-found.exception'; 4 | 5 | export { EnvModule, Env, EnvNotFound }; 6 | -------------------------------------------------------------------------------- /src/module/env-not-found.exception.ts: -------------------------------------------------------------------------------- 1 | export class EnvNotFound extends Error { 2 | constructor(name: string) { 3 | super(`Env variable ${name} not found`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/module/env.decorator.ts: -------------------------------------------------------------------------------- 1 | import { EnvNotFound } from './env-not-found.exception'; 2 | 3 | interface EnvParams { 4 | default?: string | number | boolean | object; 5 | } 6 | 7 | export function Env(key: string, params?: EnvParams) { 8 | const { default: defaultValue } = params || {}; 9 | 10 | return (target: object, propertyName: string) => { 11 | const targetType = Reflect.getMetadata('design:type', target, propertyName); 12 | const env = process.env[key]; 13 | 14 | if (env === undefined) { 15 | if (defaultValue === undefined) { 16 | throw new EnvNotFound(key); 17 | } else { 18 | Object.defineProperty(target, propertyName, { 19 | enumerable: true, 20 | configurable: false, 21 | value: defaultValue, 22 | }); 23 | return; 24 | } 25 | } 26 | 27 | Object.defineProperty(target, propertyName, { 28 | enumerable: true, 29 | configurable: false, 30 | value: castValue(env, targetType), 31 | }); 32 | }; 33 | } 34 | 35 | function castValue(value: string, targetConstructor: any) { 36 | if (targetConstructor === Object) { 37 | return JSON.parse(value); 38 | } 39 | if (targetConstructor === Boolean) { 40 | return value === 'true'; 41 | } 42 | return targetConstructor(value); 43 | } 44 | -------------------------------------------------------------------------------- /src/module/env.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Global, DynamicModule } from '@nestjs/common'; 2 | 3 | @Global() 4 | @Module({}) 5 | export class EnvModule { 6 | static register(providers: any[]): DynamicModule { 7 | return { 8 | module: EnvModule, 9 | providers: providers, 10 | exports: providers, 11 | }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "noUnusedLocals": false, 7 | "removeComments": true, 8 | "noLib": false, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es6", 12 | "sourceMap": false, 13 | "outDir": "./dist", 14 | }, 15 | "include": [ 16 | "./src/**/*.ts" 17 | ], 18 | "exclude": [ 19 | "node_modules", 20 | "**/*.spec.ts" 21 | ] 22 | } 23 | --------------------------------------------------------------------------------