@adonisjs/env 4.0.0-2 → 4.0.2-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,21 +25,22 @@ import { EnvLoader } from '@adonisjs/env'
25
25
  const lookupPath = new URL('./', import.meta.url)
26
26
  const loader = new EnvLoader(lookupPath)
27
27
 
28
- const { envContents, currentEnvContents } = await loader.load()
28
+ const envFiles = await loader.load()
29
29
  ```
30
30
 
31
- ### `envContents`
31
+ The return value is an array of objects with following properties.
32
32
 
33
- - The `envContents` is read from the `.env` file from the root of the `lookupPath`.
34
- - No exceptions are raised if the `.env` file is missing. In brief, it is optional to have a `.env` file.
35
- - If the `ENV_PATH` environment variable is set, the loader will use that instead of the `.env` file. This allows overwriting the location of the dot-env file.
33
+ - `path`: The path to the loaded dot-env file.
34
+ - `contents`: The contents of the file.
36
35
 
36
+ Following is the list of loaded files. The array is ordered by the priority of the files. The first file has the highest priority and must override the variables from the last file.
37
37
 
38
- ### `currentEnvContents`
39
-
40
- - The `currentEnvContents` contents are read from the `.env.[NODE_ENV]` file.
41
- - If the current `NODE_ENV = 'development'`, then the contents of this variable will be from the `.env.development` file and so on.
42
- - The contents of this file should take precendence over the `.env` file.
38
+ | Priority | File name | Environment | Should I `.gitignore` it | Notes |
39
+ |----------|-----------|-------------|--------------------------|-------|
40
+ | 1st | `.env.[NODE_ENV].local` | Current environment | Yes | Loaded when `NODE_ENV` is set |
41
+ | 2nd | `.env.local` | All | Yes | Loaded in all the environments except `test` or `testing` environments |
42
+ | 3rd | `.env.[NODE_ENV]` | Current environment | No | Loaded when `NODE_ENV` is set |
43
+ | 4th | `.env` | All | Depends | Loaded in all the environments. You should `.gitignore` it when storing secrets in this file |
43
44
 
44
45
  ## EnvParser
45
46
  The `EnvParser` class is responsible for parsing the contents of the `.env` file(s) and converting them into an object.
@@ -49,21 +50,22 @@ import { EnvLoader, EnvParser } from '@adonisjs/env'
49
50
 
50
51
  const lookupPath = new URL('./', import.meta.url)
51
52
  const loader = new EnvLoader(lookupPath)
52
- const { envContents, currentEnvContents } = await loader.load()
53
+ const envFiles = await loader.load()
53
54
 
54
- const envParser = new EnvParser(envContents)
55
- const currentEnvParser = new EnvParser(currentEnvContents)
55
+ const envParser = new EnvParser(`
56
+ PORT=3000
57
+ HOST=localhost
58
+ `)
56
59
 
57
- console.log(envParser.parse()) // { key: value }
58
- console.log(currentEnvParser.parse()) // { key: value }
60
+ console.log(envParser.parse()) // { PORT: '3000', HOST: 'localhost' }
59
61
  ```
60
62
 
61
63
  The return value of `parser.parse` is an object with key-value pair. The parser also has support for interpolation.
62
64
 
63
- You can also instruct the parser to prefer existing `process.env` values when they exist. When `preferProcessEnv` is set to `true`, the value from the env contents will be discarded in favor of existing `process.env` value.
65
+ By default, the parser prefers existing `process.env` values when they exist. However, you can instruct the parser to ignore existing `process.env` files as follows.
64
66
 
65
67
  ```ts
66
- new EnvParser(envContents, { preferProcessEnv: true })
68
+ new EnvParser(envContents, { ignoreProcessEnv: true })
67
69
  ```
68
70
 
69
71
  ## Validating environment variables
@@ -92,39 +94,35 @@ validate(process.env)
92
94
 
93
95
  Following is a complete example of using the `EnvLoader`, `EnvParser`, and the validator to set up environment variables.
94
96
 
97
+ > **Note**: Existing `process.env` variables have the top most priority over the variables defined in any of the files.
98
+
95
99
  ```ts
96
100
  import { EnvLoader, EnvParser, Env } from '@adonisjs/env'
97
101
 
98
102
  const lookupPath = new URL('./', import.meta.url)
99
103
  const loader = new EnvLoader(lookupPath)
100
- const { envContents, currentEnvContents } = await loader.load()
101
-
102
- /**
103
- * Loop over all the current env parsed values and let
104
- * them take precedence over the existing process.env
105
- * values.
106
- */
107
- const currentEnvValues = new EnvParser(currentEnvContents).parse()
108
- Object.keys(currentEnvValues).forEach((key) => {
109
- process.env[key] = currentEnvValues[key]
110
- })
104
+ const envFiles = await loader.load()
105
+
106
+ let envValues = {}
111
107
 
112
- const envValues = new EnvParser(envContents, { preferProcessEnv: true }).parse()
113
-
114
- /**
115
- * Loop over all the parsed env values and set
116
- * them on "process.env"
117
- *
118
- * However, if the value already exists on "process.env", then
119
- * do not overwrite it, instead update the envValues
120
- * object.
121
- */
122
- Object.keys(envValues).forEach((key) => {
123
- if (process.env[key]) {
124
- envValues[key] = process.env[key]
125
- } else {
126
- process.env[key] = envValues[key]
108
+ envFiles.forEach(({ contents }) => {
109
+ if (!contents.trim()) {
110
+ return
127
111
  }
112
+
113
+ const values = new EnvParser(contents).parse()
114
+ Object.keys(values).forEach((key) => {
115
+ let value = process.env[key]
116
+
117
+ if (!value) {
118
+ value = values[key]
119
+ process.env[key] = values[key]
120
+ }
121
+
122
+ if (!envValues[key]) {
123
+ envValues[key] = value
124
+ }
125
+ })
128
126
  })
129
127
 
130
128
  // Now perform the validation
@@ -133,7 +131,7 @@ const validate = Env.rules({
133
131
  HOST: Env.schema.string({ format: 'host' })
134
132
  })
135
133
 
136
- const validated = validate({ ...envValues, ...currentEnvValues })
134
+ const validated = validate(envValues)
137
135
  const env = new Env(validated)
138
136
 
139
137
  env.get('PORT') // is a number
@@ -143,6 +141,21 @@ env.get('NODE_ENV') // is unknown, hence a string or undefined
143
141
 
144
142
  The above code may seem like a lot of work to set up environment variables. However, you have fine-grained control over each step. In the case of AdonisJS, all this boilerplate is hidden inside the framework's application bootstrapping logic.
145
143
 
144
+ ## Known Exceptions
145
+
146
+ ### E_INVALID_ENV_VARIABLES
147
+ The exception is raised during environment variables validation exception. The exception is raised with `Validation failed for one or more environment variables` message.
148
+
149
+ You can access the detailed error messages using the `error.cause` property.
150
+
151
+ ```ts
152
+ try {
153
+ validate(envValues)
154
+ } catch (error) {
155
+ console.log(error.cause)
156
+ }
157
+ ```
158
+
146
159
  [gh-workflow-image]: https://img.shields.io/github/workflow/status/adonisjs/env/test?style=for-the-badge
147
160
  [gh-workflow-url]: https://github.com/adonisjs/env/actions/workflows/test.yml "Github action"
148
161
 
@@ -3,7 +3,7 @@ export declare class EnvLoader {
3
3
  #private;
4
4
  constructor(appRoot: string | URL);
5
5
  load(): Promise<{
6
- envContents: string;
7
- currentEnvContents: string;
8
- }>;
6
+ path: string;
7
+ contents: string;
8
+ }[]>;
9
9
  }
@@ -1,13 +1,12 @@
1
1
  import { fileURLToPath } from 'node:url';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { isAbsolute, join } from 'node:path';
4
- import { MissingEnvPathFileException } from './exceptions/missing_env_path_file.js';
5
4
  export class EnvLoader {
6
5
  #appRoot;
7
6
  constructor(appRoot) {
8
7
  this.#appRoot = typeof appRoot === 'string' ? appRoot : fileURLToPath(appRoot);
9
8
  }
10
- async #loadFile(filePath, optional = false) {
9
+ async #loadFile(filePath) {
11
10
  try {
12
11
  return await readFile(filePath, 'utf-8');
13
12
  }
@@ -15,20 +14,44 @@ export class EnvLoader {
15
14
  if (error.code !== 'ENOENT') {
16
15
  throw error;
17
16
  }
18
- if (optional) {
19
- return '';
20
- }
21
- throw new MissingEnvPathFileException(`Cannot find env file from "ENV_PATH"`, {
22
- cause: error,
23
- });
17
+ return '';
24
18
  }
25
19
  }
26
20
  async load() {
27
- const envFile = process.env.ENV_PATH || '.env';
28
- const envPath = isAbsolute(envFile) ? envFile : join(this.#appRoot, envFile);
29
- const envContents = await this.#loadFile(envPath, !process.env.ENV_PATH);
30
- const currentEnvPath = join(this.#appRoot, `.env.${process.env.NODE_ENV}`);
31
- const currentEnvContents = await this.#loadFile(currentEnvPath, true);
32
- return { envContents, currentEnvContents };
21
+ const ENV_PATH = process.env.ENV_PATH;
22
+ const NODE_ENV = process.env.NODE_ENV;
23
+ const envFiles = [];
24
+ const baseEnvPath = ENV_PATH
25
+ ? isAbsolute(ENV_PATH)
26
+ ? ENV_PATH
27
+ : join(this.#appRoot, ENV_PATH)
28
+ : this.#appRoot;
29
+ if (NODE_ENV) {
30
+ const nodeEnvLocalFile = join(baseEnvPath, `.env.${process.env.NODE_ENV}.local`);
31
+ envFiles.push({
32
+ path: nodeEnvLocalFile,
33
+ contents: await this.#loadFile(nodeEnvLocalFile),
34
+ });
35
+ }
36
+ if (!NODE_ENV || !['test', 'testing'].includes(NODE_ENV)) {
37
+ const envLocalFile = join(baseEnvPath, '.env.local');
38
+ envFiles.push({
39
+ path: envLocalFile,
40
+ contents: await this.#loadFile(envLocalFile),
41
+ });
42
+ }
43
+ if (NODE_ENV) {
44
+ const nodeEnvFile = join(baseEnvPath, `.env.${process.env.NODE_ENV}`);
45
+ envFiles.push({
46
+ path: nodeEnvFile,
47
+ contents: await this.#loadFile(nodeEnvFile),
48
+ });
49
+ }
50
+ const envFile = join(baseEnvPath, '.env');
51
+ envFiles.push({
52
+ path: envFile,
53
+ contents: await this.#loadFile(envFile),
54
+ });
55
+ return envFiles;
33
56
  }
34
57
  }
@@ -2,7 +2,7 @@ import { DotenvParseOutput } from 'dotenv';
2
2
  export declare class EnvParser {
3
3
  #private;
4
4
  constructor(envContents: string, options?: {
5
- preferProcessEnv: boolean;
5
+ ignoreProcessEnv: boolean;
6
6
  });
7
7
  parse(): DotenvParseOutput;
8
8
  }
@@ -1,10 +1,10 @@
1
1
  import dotenv from 'dotenv';
2
2
  export class EnvParser {
3
3
  #envContents;
4
- #preferProcessEnv = false;
4
+ #preferProcessEnv = true;
5
5
  constructor(envContents, options) {
6
- if (options?.preferProcessEnv) {
7
- this.#preferProcessEnv = true;
6
+ if (options?.ignoreProcessEnv) {
7
+ this.#preferProcessEnv = false;
8
8
  }
9
9
  this.#envContents = envContents;
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonisjs/env",
3
- "version": "4.0.0-2",
3
+ "version": "4.0.2-0",
4
4
  "description": "Environment variable manager for Node.js",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "scripts": {
16
16
  "pretest": "npm run lint",
17
- "test": "npm run vscode:test",
17
+ "test": "c8 npm run vscode:test",
18
18
  "clean": "del-cli build",
19
19
  "compile": "npm run lint && npm run clean && tsc",
20
20
  "build": "npm run compile",
@@ -41,9 +41,10 @@
41
41
  "@japa/runner": "^2.2.2",
42
42
  "@japa/spec-reporter": "^1.3.2",
43
43
  "@poppinss/dev-utils": "^2.0.3",
44
- "@swc/core": "^1.3.9",
44
+ "@swc/core": "^1.3.14",
45
45
  "@types/fs-extra": "^9.0.13",
46
46
  "@types/node": "^18.11.0",
47
+ "c8": "^7.12.0",
47
48
  "del-cli": "^5.0.0",
48
49
  "eslint": "^8.25.0",
49
50
  "eslint-config-prettier": "^8.5.0",
@@ -114,5 +115,14 @@
114
115
  "tag": "next",
115
116
  "branch": "main",
116
117
  "anyBranch": false
118
+ },
119
+ "c8": {
120
+ "reporter": [
121
+ "text",
122
+ "html"
123
+ ],
124
+ "exclude": [
125
+ "tests/**"
126
+ ]
117
127
  }
118
128
  }
@@ -1,5 +0,0 @@
1
- import { Exception } from '@poppinss/utils';
2
- export declare class MissingEnvPathFileException extends Exception {
3
- static status: number;
4
- static code: string;
5
- }
@@ -1,5 +0,0 @@
1
- import { Exception } from '@poppinss/utils';
2
- export class MissingEnvPathFileException extends Exception {
3
- static status = 500;
4
- static code = 'E_MISSING_ENV_PATH_FILE';
5
- }