@adonisjs/env 4.2.0-1 → 4.2.0-3

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/src/loader.ts ADDED
@@ -0,0 +1,149 @@
1
+ /*
2
+ * @adonisjs/env
3
+ *
4
+ * (c) AdonisJS
5
+ *
6
+ * For the full copyright and license information, please view the LICENSE
7
+ * file that was distributed with this source code.
8
+ */
9
+
10
+ import { fileURLToPath } from 'node:url'
11
+ import { readFile } from 'node:fs/promises'
12
+ import { isAbsolute, join } from 'node:path'
13
+
14
+ import debug from './debug.js'
15
+
16
+ /**
17
+ * Read the contents of one or more dot-env files. Following is how the files
18
+ * are read.
19
+ *
20
+ * - Load file from the "ENV_PATH" environment file.
21
+ * (Raise error if file is missing)
22
+ *
23
+ * - If "ENV_PATH" is not defined, then find ".env" file in the app root.
24
+ * (Ignore if file is missing)
25
+ *
26
+ * - Find ".env.[NODE_ENV]" file in the app root.
27
+ * (Ignore if file is missing)
28
+ *
29
+ * ```ts
30
+ * const loader = new EnvLoader(new URL('./', import.meta.url))
31
+ *
32
+ * const { envContents, currentEnvContents } = await loader.load()
33
+ *
34
+ * // envContents: Contents of .env or file specified via ENV_PATH
35
+ * // currentEnvContents: Contents of .env.[NODE_ENV] file
36
+ * ```
37
+ */
38
+ export class EnvLoader {
39
+ #appRoot: string
40
+ #loadExampleFile: boolean
41
+
42
+ constructor(appRoot: string | URL, loadExampleFile: boolean = false) {
43
+ this.#appRoot = typeof appRoot === 'string' ? appRoot : fileURLToPath(appRoot)
44
+ this.#loadExampleFile = loadExampleFile
45
+ }
46
+
47
+ /**
48
+ * Optionally read a file from the disk
49
+ */
50
+ async #loadFile(filePath: string | URL): Promise<{ fileExists: boolean; contents: string }> {
51
+ try {
52
+ const contents = await readFile(filePath, 'utf-8')
53
+ return { contents, fileExists: true }
54
+ } catch (error) {
55
+ /* c8 ignore next 3 */
56
+ if (error.code !== 'ENOENT') {
57
+ throw error
58
+ }
59
+
60
+ return { contents: '', fileExists: false }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Load contents of the main dot-env file and the current
66
+ * environment dot-env file
67
+ */
68
+ async load(): Promise<{ contents: string; path: string; fileExists: boolean }[]> {
69
+ const ENV_PATH = process.env.ENV_PATH
70
+ const NODE_ENV = process.env.NODE_ENV
71
+ const envFiles: { path: string; contents: string; fileExists: boolean }[] = []
72
+
73
+ if (debug.enabled) {
74
+ debug('ENV_PATH variable is %s', ENV_PATH ? 'set' : 'not set')
75
+ debug('NODE_ENV variable is %s', NODE_ENV ? 'set' : 'not set')
76
+ }
77
+
78
+ /**
79
+ * Base path to load .env files from
80
+ */
81
+ const baseEnvPath = ENV_PATH
82
+ ? isAbsolute(ENV_PATH)
83
+ ? ENV_PATH
84
+ : join(this.#appRoot, ENV_PATH)
85
+ : this.#appRoot
86
+
87
+ if (debug.enabled) {
88
+ debug('dot-env files base path "%s"', baseEnvPath)
89
+ }
90
+
91
+ /**
92
+ * 1st
93
+ * The top most priority is given to the ".env.[NODE_ENV].local" file
94
+ */
95
+ if (NODE_ENV) {
96
+ const nodeEnvLocalFile = join(baseEnvPath, `.env.${NODE_ENV}.local`)
97
+ envFiles.push({
98
+ path: nodeEnvLocalFile,
99
+ ...(await this.#loadFile(nodeEnvLocalFile)),
100
+ })
101
+ }
102
+
103
+ /**
104
+ * 2nd
105
+ * Next, we give priority to the ".env.local" file
106
+ */
107
+ if (!NODE_ENV || !['test', 'testing'].includes(NODE_ENV)) {
108
+ const envLocalFile = join(baseEnvPath, '.env.local')
109
+ envFiles.push({
110
+ path: envLocalFile,
111
+ ...(await this.#loadFile(envLocalFile)),
112
+ })
113
+ }
114
+
115
+ /**
116
+ * 3rd
117
+ * Next, we give priority to the ".env.[NODE_ENV]" file
118
+ */
119
+ if (NODE_ENV) {
120
+ const nodeEnvFile = join(baseEnvPath, `.env.${NODE_ENV}`)
121
+ envFiles.push({
122
+ path: nodeEnvFile,
123
+ ...(await this.#loadFile(nodeEnvFile)),
124
+ })
125
+ }
126
+
127
+ /**
128
+ * Finally, we push the contents of the ".env" file.
129
+ */
130
+ const envFile = join(baseEnvPath, '.env')
131
+ envFiles.push({
132
+ path: envFile,
133
+ ...(await this.#loadFile(envFile)),
134
+ })
135
+
136
+ /**
137
+ * Load example file
138
+ */
139
+ if (this.#loadExampleFile) {
140
+ const envExampleFile = join(baseEnvPath, '.env.example')
141
+ envFiles.push({
142
+ path: envExampleFile,
143
+ ...(await this.#loadFile(envExampleFile)),
144
+ })
145
+ }
146
+
147
+ return envFiles
148
+ }
149
+ }
package/src/parser.ts ADDED
@@ -0,0 +1,185 @@
1
+ /*
2
+ * @adonisjs/env
3
+ *
4
+ * (c) AdonisJS
5
+ *
6
+ * For the full copyright and license information, please view the LICENSE
7
+ * file that was distributed with this source code.
8
+ */
9
+
10
+ import dotenv, { DotenvParseOutput } from 'dotenv'
11
+
12
+ /**
13
+ * Env parser parses the environment variables from a string formatted
14
+ * as a key-value pair seperated using an `=`. For example:
15
+ *
16
+ * ```dotenv
17
+ * PORT=3333
18
+ * HOST=127.0.0.1
19
+ * ```
20
+ *
21
+ * The variables can reference other environment variables as well using `$`.
22
+ * For example:
23
+ *
24
+ * ```dotenv
25
+ * PORT=3333
26
+ * REDIS_PORT=$PORT
27
+ * ```
28
+ *
29
+ * The variables using characters other than letters can wrap variable
30
+ * named inside a curly brace.
31
+ *
32
+ * ```dotenv
33
+ * APP-PORT=3333
34
+ * REDIS_PORT=${APP-PORT}
35
+ * ```
36
+ *
37
+ * You can escape the `$` sign with a backtick.
38
+ *
39
+ * ```dotenv
40
+ * REDIS_PASSWORD=foo\$123
41
+ * ```
42
+ *
43
+ * ## Usage
44
+ *
45
+ * ```ts
46
+ * const parser = new EnvParser(envContents)
47
+ * const output = parser.parse()
48
+ *
49
+ * // The output is a key-value pair
50
+ * ```
51
+ */
52
+ export class EnvParser {
53
+ #envContents: string
54
+ #preferProcessEnv: boolean = true
55
+
56
+ constructor(envContents: string, options?: { ignoreProcessEnv: boolean }) {
57
+ if (options?.ignoreProcessEnv) {
58
+ this.#preferProcessEnv = false
59
+ }
60
+
61
+ this.#envContents = envContents
62
+ }
63
+
64
+ /**
65
+ * Returns the value from the parsed object
66
+ */
67
+ #getValue(key: string, parsed: DotenvParseOutput): string {
68
+ if (this.#preferProcessEnv && process.env[key]) {
69
+ return process.env[key]!
70
+ }
71
+
72
+ if (parsed[key]) {
73
+ return this.#interpolate(parsed[key], parsed)
74
+ }
75
+
76
+ return process.env[key] || ''
77
+ }
78
+
79
+ /**
80
+ * Interpolating the token wrapped inside the mustache braces.
81
+ */
82
+ #interpolateMustache(token: string, parsed: DotenvParseOutput) {
83
+ /**
84
+ * Finding the closing brace. If closing brace is missing, we
85
+ * consider the block as a normal string
86
+ */
87
+ const closingBrace = token.indexOf('}')
88
+ if (closingBrace === -1) {
89
+ return token
90
+ }
91
+
92
+ /**
93
+ * Then we pull everything until the closing brace, except
94
+ * the opening brace and trim off all white spaces.
95
+ */
96
+ const varReference = token.slice(1, closingBrace).trim()
97
+
98
+ /**
99
+ * Getting the value of the reference inside the braces
100
+ */
101
+ return `${this.#getValue(varReference, parsed)}${token.slice(closingBrace + 1)}`
102
+ }
103
+
104
+ /**
105
+ * Interpolating the variable reference starting with a
106
+ * `$`. We only capture numbers,letter and underscore.
107
+ * For other characters, one can use the mustache
108
+ * braces.
109
+ */
110
+ #interpolateVariable(token: string, parsed: any) {
111
+ return token.replace(/[a-zA-Z0-9_]+/, (key) => {
112
+ return this.#getValue(key, parsed)
113
+ })
114
+ }
115
+
116
+ /**
117
+ * Interpolates the referenced values
118
+ */
119
+ #interpolate(value: string, parsed: DotenvParseOutput): string {
120
+ const tokens = value.split('$')
121
+
122
+ let newValue = ''
123
+ let skipNextToken = true
124
+
125
+ tokens.forEach((token) => {
126
+ /**
127
+ * If the value is an escaped sequence, then we replace it
128
+ * with a `$` and then skip the next token.
129
+ */
130
+ if (token === '\\') {
131
+ newValue += '$'
132
+ skipNextToken = true
133
+ return
134
+ }
135
+
136
+ /**
137
+ * Use the value as it is when "skipNextToken" is set to true.
138
+ */
139
+ if (skipNextToken) {
140
+ /**
141
+ * Replace the ending escape sequence with a $
142
+ */
143
+ newValue += token.replace(/\\$/, '$')
144
+ /**
145
+ * and then skip the next token if it ends with escape sequence
146
+ */
147
+ if (token.endsWith('\\')) {
148
+ return
149
+ }
150
+ } else {
151
+ /**
152
+ * Handle mustache block
153
+ */
154
+ if (token.startsWith('{')) {
155
+ newValue += this.#interpolateMustache(token, parsed)
156
+ return
157
+ }
158
+
159
+ /**
160
+ * Process all words as variable
161
+ */
162
+ newValue += this.#interpolateVariable(token, parsed)
163
+ }
164
+
165
+ /**
166
+ * Process next token
167
+ */
168
+ skipNextToken = false
169
+ })
170
+
171
+ return newValue
172
+ }
173
+
174
+ /**
175
+ * Parse the env string to an object of environment variables.
176
+ */
177
+ parse(): DotenvParseOutput {
178
+ const envCollection = dotenv.parse(this.#envContents.trim())
179
+
180
+ return Object.keys(envCollection).reduce<DotenvParseOutput>((result, key) => {
181
+ result[key] = this.#getValue(key, envCollection)
182
+ return result
183
+ }, {})
184
+ }
185
+ }
@@ -0,0 +1,83 @@
1
+ /*
2
+ * @adonisjs/application
3
+ *
4
+ * (c) AdonisJS
5
+ *
6
+ * For the full copyright and license information, please view the LICENSE
7
+ * file that was distributed with this source code.
8
+ */
9
+
10
+ import debug from './debug.js'
11
+ import { EnvParser } from './parser.js'
12
+ import { EnvLoader } from './loader.js'
13
+
14
+ /**
15
+ * Env processors loads, parses and process environment variables.
16
+ */
17
+ export class EnvProcessor {
18
+ /**
19
+ * App root is needed to load files
20
+ */
21
+ #appRoot: URL
22
+
23
+ constructor(appRoot: URL) {
24
+ this.#appRoot = appRoot
25
+ }
26
+
27
+ /**
28
+ * Parse env variables from raw contents
29
+ */
30
+ #processContents(envContents: string, store: Record<string, any>) {
31
+ /**
32
+ * Collected env variables
33
+ */
34
+ if (!envContents.trim()) {
35
+ return store
36
+ }
37
+
38
+ const values = new EnvParser(envContents).parse()
39
+ Object.keys(values).forEach((key) => {
40
+ let value = process.env[key]
41
+
42
+ if (!value) {
43
+ value = values[key]
44
+ process.env[key] = values[key]
45
+ }
46
+
47
+ if (!store[key]) {
48
+ store[key] = value
49
+ }
50
+ })
51
+
52
+ return store
53
+ }
54
+
55
+ /**
56
+ * Parse env variables by loading dot files.
57
+ */
58
+ async #loadAndProcessDotFiles() {
59
+ const loader = new EnvLoader(this.#appRoot)
60
+ const envFiles = await loader.load()
61
+
62
+ if (debug.enabled) {
63
+ debug(
64
+ 'processing .env files (priority from top to bottom) %O',
65
+ envFiles.map((file) => file.path)
66
+ )
67
+ }
68
+
69
+ /**
70
+ * Collected env variables
71
+ */
72
+ const envValues: Record<string, any> = {}
73
+ envFiles.forEach(({ contents }) => this.#processContents(contents, envValues))
74
+ return envValues
75
+ }
76
+
77
+ /**
78
+ * Process env variables
79
+ */
80
+ async process() {
81
+ return this.#loadAndProcessDotFiles()
82
+ }
83
+ }
@@ -0,0 +1,63 @@
1
+ /*
2
+ * @adonisjs/env
3
+ *
4
+ * (c) AdonisJS
5
+ *
6
+ * For the full copyright and license information, please view the LICENSE
7
+ * file that was distributed with this source code.
8
+ */
9
+
10
+ import type { Exception } from '@poppinss/utils'
11
+ import { ValidateFn } from '@poppinss/validator-lite'
12
+
13
+ import { E_INVALID_ENV_VARIABLES } from './exceptions.js'
14
+
15
+ /**
16
+ * Exposes the API to validate environment variables against a
17
+ * pre-defined schema.
18
+ *
19
+ * The class is not exported in the main API and used internally.
20
+ */
21
+ export class EnvValidator<Schema extends { [key: string]: ValidateFn<unknown> }> {
22
+ #schema: Schema
23
+ #error: Exception
24
+
25
+ constructor(schema: Schema) {
26
+ this.#schema = schema
27
+ this.#error = new E_INVALID_ENV_VARIABLES()
28
+ }
29
+
30
+ /**
31
+ * Accepts an object of values to validate against the pre-defined
32
+ * schema.
33
+ *
34
+ * The return value is a merged copy of the original object and the
35
+ * values mutated by the schema validator.
36
+ */
37
+ validate(values: { [K: string]: string | undefined }): {
38
+ [K in keyof Schema]: ReturnType<Schema[K]>
39
+ } {
40
+ const help: string[] = []
41
+
42
+ const validated = Object.keys(this.#schema).reduce(
43
+ (result, key) => {
44
+ const value = process.env[key] || values[key]
45
+
46
+ try {
47
+ result[key] = this.#schema[key](key, value) as any
48
+ } catch (error) {
49
+ help.push(`- ${error.message}`)
50
+ }
51
+ return result
52
+ },
53
+ { ...values }
54
+ ) as { [K in keyof Schema]: ReturnType<Schema[K]> }
55
+
56
+ if (help.length) {
57
+ this.#error.help = help.join('\n')
58
+ throw this.#error
59
+ }
60
+
61
+ return validated
62
+ }
63
+ }