Skip to content

Commit

Permalink
feat: read env with default value
Browse files Browse the repository at this point in the history
  • Loading branch information
fengmk2 committed Feb 10, 2025
1 parent 1f601be commit a0c8788
Show file tree
Hide file tree
Showing 12 changed files with 338 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": [
"eslint-config-egg/typescript",
"eslint-config-egg/lib/rules/enforce-node-prefix"
]
}
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: CI

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
Job:
name: Node.js
uses: node-modules/github-actions/.github/workflows/node-test.yml@master
with:
os: 'ubuntu-latest, macos-latest, windows-latest'
version: '20, 22'
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
23 changes: 23 additions & 0 deletions .github/workflows/pkg.pr.new.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Publish Any Commit
on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 20

- name: Install dependencies
run: npm install

- name: Build
run: npm run prepublishOnly --if-present

- run: npx pkg-pr-new publish
13 changes: 13 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Release

on:
push:
branches: [ master ]

jobs:
release:
name: Node.js
uses: node-modules/github-actions/.github/workflows/node-release.yml@master
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,4 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.tshy
Empty file added CHANGELOG.md
Empty file.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2025 node_modules
Copyright(c) 2025 - present node-modules and the contributors.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,50 @@
# read-env-value

[![NPM Version](https://img.shields.io/npm/v/read-env-value)](https://www.npmjs.com/package/read-env-value)
[![NPM Downloads](https://img.shields.io/npm/dm/read-env-value)](https://www.npmjs.com/package/read-env-value)
[![NPM License](https://img.shields.io/npm/l/read-env-value)](https://github.com/node-modules/read-env-value/blob/master/LICENSE)
[![codecov](https://codecov.io/gh/node-modules/read-env-value/branch/master/graph/badge.svg)](https://codecov.io/gh/node-modules/read-env-value)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/node-modules/read-env-value/ci.yml?branch=master)](https://github.com/node-modules/read-env-value/actions/workflows/ci.yml?query=branch%3Amaster)

Read env with default value

## Usage

```ts
import { env } from 'read-env-value';

// read env 'NODE_ENV' with default value 'development'
const value = env('NODE_ENV', 'string', 'development');
```

## API

### env(key: string, type: 'string' | 'number' | 'boolean', defaultValue?: string | number | boolean)

#### Parameters

- `key`: The environment variable key to read.
- `type`: The type of the value to be returned.
- `defaultValue`: The default value to return if the environment variable is not set.

#### Returns

- The value of the environment variable.

#### Example

```ts
import { env } from 'read-env-value';

const value = env('NODE_ENV', 'string', 'development');
```

## License

[MIT](./LICENSE)

## Contributors

[![Contributors](https://contrib.rocks/image?repo=node-modules/read-env-value)](https://github.com/node-modules/read-env-value/graphs/contributors)

Made with [contributors-img](https://contrib.rocks).
72 changes: 72 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"name": "read-env-value",
"version": "1.0.0",
"description": "Read env with default value",
"repository": {
"type": "git",
"url": "git+ssh://[email protected]/node-modules/read-env-value.git"
},
"keywords": [
"env",
"environment"
],
"author": "fengmk2",
"license": "MIT",
"bugs": {
"url": "https://github.com/node-modules/read-env-value/issues"
},
"homepage": "https://github.com/node-modules/read-env-value#readme",
"engines": {
"node": ">= 20.0.0"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.17.3",
"@eggjs/tsconfig": "1",
"@types/mocha": "10",
"@types/node": "22",
"@vitest/coverage-v8": "^3.0.5",
"eslint": "8",
"eslint-config-egg": "14",
"mm": "^4.0.2",
"tshy": "3",
"tshy-after": "1",
"typescript": "5",
"vitest": "^3.0.5"
},
"scripts": {
"lint": "eslint --cache src test --ext .ts",
"pretest": "npm run lint -- --fix",
"test": "vitest",
"preci": "npm run lint",
"ci": "vitest run --coverage",
"postci": "npm run prepublishOnly",
"prepublishOnly": "tshy && tshy-after && attw --pack"
},
"type": "module",
"tshy": {
"exports": {
".": "./src/index.ts",
"./package.json": "./package.json"
}
},
"exports": {
".": {
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
},
"require": {
"types": "./dist/commonjs/index.d.ts",
"default": "./dist/commonjs/index.js"
}
},
"./package.json": "./package.json"
},
"files": [
"dist",
"src"
],
"types": "./dist/commonjs/index.d.ts",
"main": "./dist/commonjs/index.js",
"module": "./dist/esm/index.js"
}
46 changes: 46 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export type ValueType = 'string' | 'boolean' | 'number';
export type DefaultValue = string | boolean | number | undefined;

type TypeMap<D> = {
'string': D extends undefined ? string | undefined : string;
'boolean': D extends undefined ? boolean | undefined : boolean;
'number': D extends undefined ? number | undefined : number;
};

type EnvReturn<V extends ValueType, D extends DefaultValue> = TypeMap<D>[V];

export function env<V extends ValueType, D extends DefaultValue>(key: string, valueType: V, defaultValue?: D): EnvReturn<V, D> {
let value = process.env[key];
if (typeof value === 'string') {
value = value.trim();
}
if (!value) {
return defaultValue as EnvReturn<V, D>;
}

if (valueType === 'string') {
return value as EnvReturn<V, D>;
}

if (valueType === 'boolean') {
let booleanValue = false;
if (value === 'true' || value === '1') {
booleanValue = true;
} else if (value === 'false' || value === '0') {
booleanValue = false;
} else {
throw new TypeError(`Invalid boolean value: ${value} on process.env.${key}`);
}
return booleanValue as EnvReturn<V, D>;
}

if (valueType === 'number') {
const numberValue = Number(value);
if (isNaN(numberValue)) {
throw new TypeError(`Invalid number value: ${value} on process.env.${key}`);
}
return numberValue as EnvReturn<V, D>;
}

throw new TypeError(`Invalid value type: ${valueType}`);
}
101 changes: 101 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import assert from 'node:assert/strict';
import { test, beforeEach } from 'vitest';
import { mock, restore } from 'mm';
import { env } from '../src/index.js';

beforeEach(restore);

test('should return default value if env is not set', () => {
assert.equal(env('TEST_ENV_STRING', 'string', 'default'), 'default');
assert.equal(env('TEST_ENV_BOOLEAN', 'boolean', false), false);
assert.equal(env('TEST_ENV_NUMBER', 'number', 0), 0);
});

test('should return undefined if env is not set', () => {
assert.equal(env('TEST_ENV_STRING', 'string'), undefined);
assert.equal(env('TEST_ENV_BOOLEAN', 'boolean'), undefined);
assert.equal(env('TEST_ENV_NUMBER', 'number'), undefined);

assert.equal(env('TEST_ENV_STRING', 'string') ?? null, null);
assert.equal(env('TEST_ENV_NUMBER', 'number') ?? 0, 0);
assert.equal(env('TEST_ENV_BOOLEAN', 'boolean') ?? false, false);
assert.equal(env('TEST_ENV_STRING', 'string') ?? 'default', 'default');
});

test('should return env value if env is set to empty string', () => {
mock(process.env, 'TEST_ENV_STRING', '');
mock(process.env, 'TEST_ENV_BOOLEAN', '');
mock(process.env, 'TEST_ENV_NUMBER', '');

assert.equal(env('TEST_ENV_STRING', 'string', ''), '');
assert.equal(env('TEST_ENV_STRING', 'string'), undefined);
assert.equal(env('TEST_ENV_STRING', 'string', 'default string'), 'default string');
assert.equal(env('TEST_ENV_BOOLEAN', 'boolean', true), true);
assert.equal(env('TEST_ENV_BOOLEAN', 'boolean', false), false);
assert.equal(env('TEST_ENV_NUMBER', 'number', 3306), 3306);
assert.equal(env('TEST_ENV_NUMBER', 'number', 0), 0);
assert.equal(env('TEST_ENV_NUMBER', 'number', -123), -123);

mock(process.env, 'TEST_ENV_STRING', ' ');
mock(process.env, 'TEST_ENV_BOOLEAN', ' \t ');
mock(process.env, 'TEST_ENV_NUMBER', ' \t\t\t ');

assert.equal(env('TEST_ENV_STRING', 'string', ''), '');
assert.equal(env('TEST_ENV_STRING', 'string', 'default string'), 'default string');
assert.equal(env('TEST_ENV_BOOLEAN', 'boolean', true), true);
assert.equal(env('TEST_ENV_BOOLEAN', 'boolean', false), false);
assert.equal(env('TEST_ENV_NUMBER', 'number', 3306), 3306);
assert.equal(env('TEST_ENV_NUMBER', 'number', 0), 0);
});

test('should throw error if env is set to invalid value', () => {
mock(process.env, 'TEST_ENV_BOOLEAN', 'invalid');
assert.throws(() => env('TEST_ENV_BOOLEAN', 'boolean', false), /Invalid boolean value: invalid on process.env.TEST_ENV_BOOLEAN/);

mock(process.env, 'TEST_ENV_NUMBER', 'invalid');
assert.throws(() => env('TEST_ENV_NUMBER', 'number', 0), /Invalid number value: invalid on process.env.TEST_ENV_NUMBER/);

mock(process.env, 'TEST_ENV_NUMBER', 'abc');
assert.throws(() => env('TEST_ENV_NUMBER', 'number', 0), /Invalid number value: abc on process.env.TEST_ENV_NUMBER/);
});

test('should throw error if value type is invalid', () => {
mock(process.env, 'TEST_ENV_STRING', '123');
assert.throws(() => (env as any)('TEST_ENV_STRING', 'float', 'default'), /Invalid value type: float/);
});

test('should work with string value', () => {
mock(process.env, 'TEST_ENV_STRING', 'http://localhost:3000');
assert.equal(env('TEST_ENV_STRING', 'string', 'default'), 'http://localhost:3000');

mock(process.env, 'TEST_ENV_STRING', ' ');
assert.equal(env('TEST_ENV_STRING', 'string', 'default'), 'default');
});

test('should work with boolean value', () => {
mock(process.env, 'TEST_ENV_BOOLEAN', 'true');
assert.equal(env('TEST_ENV_BOOLEAN', 'boolean', false), true);

mock(process.env, 'TEST_ENV_BOOLEAN', 'false');
assert.equal(env('TEST_ENV_BOOLEAN', 'boolean', true), false);

mock(process.env, 'TEST_ENV_BOOLEAN', '1');
assert.equal(env('TEST_ENV_BOOLEAN', 'boolean', false), true);

mock(process.env, 'TEST_ENV_BOOLEAN', '0');
assert.equal(env('TEST_ENV_BOOLEAN', 'boolean', true), false);
});

test('should work with number value', () => {
mock(process.env, 'TEST_ENV_NUMBER', '123');
assert.equal(env('TEST_ENV_NUMBER', 'number', 0), 123);

mock(process.env, 'TEST_ENV_NUMBER', '-123');
assert.equal(env('TEST_ENV_NUMBER', 'number', 0), -123);

mock(process.env, 'TEST_ENV_NUMBER', '123.456');
assert.equal(env('TEST_ENV_NUMBER', 'number', 0), 123.456);

mock(process.env, 'TEST_ENV_NUMBER', '0');
assert.equal(env('TEST_ENV_NUMBER', 'number', 10), 0);
});
10 changes: 10 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "@eggjs/tsconfig",
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}

0 comments on commit a0c8788

Please sign in to comment.