enwow 0.0.1

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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-PRESENT blasdfaa <https://github.com/blasdfaa>
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.
package/README.md ADDED
@@ -0,0 +1,252 @@
1
+ # enwow
2
+
3
+ [![npm version][npm-version-src]][npm-version-href]
4
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
+ [![bundle][bundle-src]][bundle-href]
6
+ [![License][license-src]][license-href]
7
+
8
+ Cross-platform environment variables loader and validator with [Standard Schema](https://standardschema.dev/) support.
9
+
10
+ ## Features
11
+
12
+ - 🚀 **Cross-platform** - Works on Node.js, Bun, and Deno
13
+ - ✅ **Standard Schema** - Compatible with Zod, Valibot, ArkType, and more
14
+ - 📁 **.env file loading** - With proper file priority support
15
+ - 🔒 **Type-safe** - Full TypeScript support with type inference
16
+ - ðŸŠķ **Lightweight** - Minimal dependencies
17
+
18
+ ## Installation
19
+
20
+ ```sh
21
+ npm install enwow
22
+ # or
23
+ pnpm add enwow
24
+ # or
25
+ yarn add enwow
26
+ ```
27
+
28
+ You'll also need a schema library that supports Standard Schema:
29
+
30
+ ```sh
31
+ npm install zod
32
+ # or
33
+ npm install valibot
34
+ # or any other Standard Schema compatible library
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ### Basic Usage
40
+
41
+ ```typescript
42
+ import { Env } from 'enwow'
43
+ import { z } from 'zod'
44
+
45
+ // Define your schema
46
+ const env = await Env.create({
47
+ PORT: z.string().transform(Number).default('3000'),
48
+ HOST: z.string().default('localhost'),
49
+ DATABASE_URL: z.string().url(),
50
+ DEBUG: z.string().optional(),
51
+ })
52
+
53
+ // Type-safe access
54
+ env.get('PORT') // number
55
+ env.get('HOST') // string
56
+ env.get('DATABASE_URL') // string
57
+ env.get('DEBUG') // string | undefined
58
+ ```
59
+
60
+ ### With .env Files
61
+
62
+ ```typescript
63
+ import { Env } from 'enwow'
64
+ import { z } from 'zod'
65
+
66
+ const env = await Env.create(new URL('./', import.meta.url), {
67
+ PORT: z.string().transform(Number).default('3000'),
68
+ HOST: z.string().default('localhost'),
69
+ })
70
+
71
+ // Values from .env files are loaded and validated
72
+ ```
73
+
74
+ ## File Loading Priority
75
+
76
+ Files are loaded in the following order (highest priority first):
77
+
78
+ | Priority | File Name | Environment | Notes |
79
+ |----------|-----------|-------------|-------|
80
+ | 1st | `.env.[NODE_ENV].local` | Current environment | Loaded when NODE_ENV is set |
81
+ | 2nd | `.env.local` | All | Not loaded in test environment |
82
+ | 3rd | `.env.[NODE_ENV]` | Current environment | Loaded when NODE_ENV is set |
83
+ | 4th | `.env` | All | Always loaded |
84
+
85
+ `process.env` always has the highest priority and will override values from any file.
86
+
87
+ ## API
88
+
89
+ ### `Env.create(schema, options?)`
90
+
91
+ Create a new Env instance without loading .env files.
92
+
93
+ ```typescript
94
+ const env = await Env.create({
95
+ PORT: z.string().transform(Number),
96
+ })
97
+ ```
98
+
99
+ ### `Env.create(path, schema, options?)`
100
+
101
+ Create a new Env instance and load .env files from the specified directory.
102
+
103
+ ```typescript
104
+ const env = await Env.create(new URL('./', import.meta.url), {
105
+ PORT: z.string().transform(Number),
106
+ })
107
+ ```
108
+
109
+ ### Options
110
+
111
+ ```typescript
112
+ interface EnvCreateOptions {
113
+ // Ignore existing process.env values
114
+ ignoreProcessEnv?: boolean
115
+
116
+ // Override NODE_ENV for file loading
117
+ nodeEnv?: string
118
+
119
+ // Custom environment variables source
120
+ envSource?: Record<string, string | undefined>
121
+ }
122
+ ```
123
+
124
+ ### `env.get(key)`
125
+
126
+ Get a validated environment variable value.
127
+
128
+ ```typescript
129
+ const port = env.get('PORT') // Type: number
130
+ ```
131
+
132
+ ### `env.all()`
133
+
134
+ Get all validated environment variables as an object.
135
+
136
+ ```typescript
137
+ const all = env.all() // Type: { PORT: number, HOST: string, ... }
138
+ ```
139
+
140
+ ### `env.has(key)`
141
+
142
+ Check if a variable is defined.
143
+
144
+ ```typescript
145
+ if (env.has('DEBUG')) {
146
+ // ...
147
+ }
148
+ ```
149
+
150
+ ## Error Handling
151
+
152
+ When validation fails, an `EnvValidationError` is thrown:
153
+
154
+ ```typescript
155
+ import { EnvValidationError } from 'enwow'
156
+
157
+ try {
158
+ const env = await Env.create({
159
+ REQUIRED_VAR: z.string(),
160
+ })
161
+ }
162
+ catch (error) {
163
+ if (error instanceof EnvValidationError) {
164
+ console.log('Validation failed:')
165
+ for (const issue of error.issues) {
166
+ console.log(` ${issue.path}: ${issue.message}`)
167
+ }
168
+ }
169
+ }
170
+ ```
171
+
172
+ ## Advanced Usage
173
+
174
+ ### With Valibot
175
+
176
+ ```typescript
177
+ import { Env } from 'enwow'
178
+ import * as v from 'valibot'
179
+
180
+ const env = await Env.create({
181
+ PORT: v.pipe(v.string(), v.transform(Number), v.minValue(1), v.maxValue(65535)),
182
+ HOST: v.optional(v.string(), 'localhost'),
183
+ })
184
+ ```
185
+
186
+ ### With ArkType
187
+
188
+ ```typescript
189
+ import { type } from 'arktype'
190
+ import { Env } from 'enwow'
191
+
192
+ const env = await Env.create({
193
+ PORT: type('string.integer').pipe(Number),
194
+ HOST: type('string').default('localhost'),
195
+ })
196
+ ```
197
+
198
+ ### Manual Validation
199
+
200
+ ```typescript
201
+ import { EnvValidator, parseEnv } from 'enwow'
202
+ import { z } from 'zod'
203
+
204
+ const validator = new EnvValidator({
205
+ PORT: z.string().transform(Number),
206
+ })
207
+
208
+ const result = validator.validate(process.env)
209
+ ```
210
+
211
+ ### Direct File Parsing
212
+
213
+ ```typescript
214
+ import { EnvLoader, EnvParser } from 'enwow'
215
+
216
+ const loader = new EnvLoader(new URL('./', import.meta.url))
217
+ const files = await loader.load()
218
+
219
+ for (const file of files) {
220
+ const parser = new EnvParser(file.contents)
221
+ const parsed = parser.parse()
222
+ console.log(parsed)
223
+ }
224
+ ```
225
+
226
+ ## Contribution
227
+
228
+ <details>
229
+ <summary>Local development</summary>
230
+
231
+ - Clone this repository
232
+ - Install the latest LTS version of [Node.js](https://nodejs.org/en/)
233
+ - Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`
234
+ - Install dependencies using `pnpm install`
235
+ - Run tests using `pnpm test`
236
+
237
+ </details>
238
+
239
+ ## License
240
+
241
+ [MIT](./LICENSE.md) License
242
+
243
+ <!-- Badges -->
244
+
245
+ [npm-version-src]: https://img.shields.io/npm/v/enwow?style=flat&colorA=080f12&colorB=1fa669
246
+ [npm-version-href]: https://npmjs.com/package/enwow
247
+ [npm-downloads-src]: https://img.shields.io/npm/dm/enwow?style=flat&colorA=080f12&colorB=1fa669
248
+ [npm-downloads-href]: https://npmjs.com/package/enwow
249
+ [bundle-src]: https://img.shields.io/bundlephobia/minzip/enwow?style=flat&colorA=080f12&colorB=1fa669&label=minzip
250
+ [bundle-href]: https://bundlephobia.com/result?p=enwow
251
+ [license-src]: https://img.shields.io/github/license/enwow/enwow.svg?style=flat&colorA=080f12&colorB=1fa669
252
+ [license-href]: https://github.com/enwow/enwow/blob/main/LICENSE.md
@@ -0,0 +1,322 @@
1
+ import { StandardSchemaV1 } from "@standard-schema/spec";
2
+
3
+ //#region src/types.d.ts
4
+
5
+ /**
6
+ * Extract the output type from a Standard Schema
7
+ */
8
+ type SchemaOutput<T> = T extends StandardSchemaV1<infer _Input, infer Output> ? Output : never;
9
+ /**
10
+ * Extract the input type from a Standard Schema
11
+ */
12
+ type SchemaInput<T> = T extends StandardSchemaV1<infer Input, infer _Output> ? Input : never;
13
+ /**
14
+ * A schema that conforms to Standard Schema specification
15
+ */
16
+ type Schema<T = unknown> = StandardSchemaV1<T>;
17
+ /**
18
+ * Record of schemas for environment variables
19
+ */
20
+ type EnvSchema = Record<string, Schema>;
21
+ /**
22
+ * Infer the validated output type from a schema record
23
+ */
24
+ type InferEnv<T extends EnvSchema> = { [K in keyof T]: SchemaOutput<T[K]> };
25
+ /**
26
+ * Options for Env.create()
27
+ */
28
+ interface EnvCreateOptions {
29
+ /**
30
+ * Ignore existing process.env values
31
+ * @default false
32
+ */
33
+ ignoreProcessEnv?: boolean;
34
+ /**
35
+ * Override the NODE_ENV value for file loading
36
+ * Useful for testing
37
+ */
38
+ nodeEnv?: string;
39
+ /**
40
+ * Custom environment variables source
41
+ * Useful for testing or custom sources
42
+ */
43
+ envSource?: Record<string, string | undefined>;
44
+ }
45
+ /**
46
+ * Environment variable validation issue
47
+ */
48
+ interface EnvIssue {
49
+ /**
50
+ * The path to the variable (usually the variable name)
51
+ */
52
+ path?: string;
53
+ /**
54
+ * The error message
55
+ */
56
+ message: string;
57
+ }
58
+ /**
59
+ * Result of loading .env files
60
+ */
61
+ interface LoadedEnvFile {
62
+ /**
63
+ * Path to the loaded file
64
+ */
65
+ path: string;
66
+ /**
67
+ * Raw contents of the file
68
+ */
69
+ contents: string;
70
+ }
71
+ /**
72
+ * Parsed environment variables
73
+ */
74
+ type ParsedEnv = Record<string, string>;
75
+ //#endregion
76
+ //#region src/env.d.ts
77
+ /**
78
+ * Env class for managing environment variables
79
+ *
80
+ * Provides type-safe access to environment variables after validation
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * import { Env } from 'enwow'
85
+ * import { z } from 'zod'
86
+ *
87
+ * // Create with path to load .env files
88
+ * const env = await Env.create(new URL('./', import.meta.url), {
89
+ * PORT: z.string().transform(Number).default('3000'),
90
+ * HOST: z.string().default('localhost'),
91
+ * })
92
+ *
93
+ * // Or create without path (only process.env)
94
+ * const env = await Env.create({
95
+ * PORT: z.string().transform(Number),
96
+ * })
97
+ *
98
+ * // Type-safe access
99
+ * env.get('PORT') // number
100
+ * env.get('HOST') // string
101
+ * ```
102
+ */
103
+ declare class Env<T extends EnvSchema> {
104
+ private readonly values;
105
+ private constructor();
106
+ /**
107
+ * Create a new Env instance
108
+ *
109
+ * @param pathOrSchema - Either a URL path to load .env files from, or a schema object
110
+ * @param schema - The schema definition (required if pathOrSchema is a URL)
111
+ * @param options - Options for loading and validation
112
+ * @returns A new Env instance with validated values
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * // With .env file loading
117
+ * const env = await Env.create(new URL('./', import.meta.url), {
118
+ * PORT: z.string().transform(Number),
119
+ * })
120
+ *
121
+ * // Without .env file loading
122
+ * const env = await Env.create({
123
+ * PORT: z.string().transform(Number),
124
+ * })
125
+ * ```
126
+ */
127
+ static create<T extends EnvSchema>(schema: T, options?: EnvCreateOptions): Promise<Env<T>>;
128
+ static create<T extends EnvSchema>(path: URL, schema: T, options?: EnvCreateOptions): Promise<Env<T>>;
129
+ /**
130
+ * Get a validated environment variable value
131
+ * @param key - The variable name
132
+ * @returns The validated value
133
+ */
134
+ get<K$1 extends keyof T>(key: K$1): InferEnv<T>[K$1];
135
+ /**
136
+ * Get all validated environment variables as an object
137
+ */
138
+ all(): InferEnv<T>;
139
+ /**
140
+ * Check if a variable is defined
141
+ */
142
+ has(key: keyof T): boolean;
143
+ }
144
+ //#endregion
145
+ //#region src/errors.d.ts
146
+ /**
147
+ * Error thrown when environment variables validation fails
148
+ */
149
+ declare class EnvValidationError extends Error {
150
+ name: string;
151
+ issues: EnvIssue[];
152
+ constructor(issues: EnvIssue[]);
153
+ constructor(message: string, issues?: EnvIssue[]);
154
+ }
155
+ /**
156
+ * Error thrown when a required environment variable is missing
157
+ */
158
+ declare class EnvMissingError extends Error {
159
+ name: string;
160
+ variableName: string;
161
+ constructor(variableName: string);
162
+ }
163
+ //#endregion
164
+ //#region src/loader.d.ts
165
+ interface LoaderOptions {
166
+ /**
167
+ * The NODE_ENV value to use for determining which files to load
168
+ * Defaults to process.env.NODE_ENV
169
+ */
170
+ nodeEnv?: string;
171
+ /**
172
+ * Files to load (override default file resolution)
173
+ */
174
+ files?: string[];
175
+ }
176
+ /**
177
+ * Environment file loader
178
+ *
179
+ * Loads .env files from a directory with the following priority (highest first):
180
+ * 1. .env.[NODE_ENV].local - Loaded when NODE_ENV is set
181
+ * 2. .env.local - Loaded in all environments except test
182
+ * 3. .env.[NODE_ENV] - Loaded when NODE_ENV is set
183
+ * 4. .env - Loaded in all environments
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * import { EnvLoader } from 'enwow'
188
+ *
189
+ * const loader = new EnvLoader(new URL('./', import.meta.url))
190
+ * const files = await loader.load()
191
+ *
192
+ * for (const file of files) {
193
+ * console.log(file.path, file.contents)
194
+ * }
195
+ * ```
196
+ */
197
+ declare class EnvLoader {
198
+ private readonly directory;
199
+ private readonly options;
200
+ /**
201
+ * Create a new EnvLoader
202
+ * @param directory - The directory to load .env files from (as URL or string path)
203
+ * @param options - Loader options
204
+ */
205
+ constructor(directory: URL | string, options?: LoaderOptions);
206
+ /**
207
+ * Load all .env files and return their contents
208
+ * Files are returned in priority order (highest priority first)
209
+ */
210
+ load(): Promise<LoadedEnvFile[]>;
211
+ /**
212
+ * Get the list of .env file names to load, in priority order
213
+ */
214
+ private getFileNames;
215
+ private getNodeEnv;
216
+ /**
217
+ * Read a file contents in a cross-platform way
218
+ */
219
+ private readFile;
220
+ }
221
+ /**
222
+ * Convenience function to load .env files from a directory
223
+ */
224
+ declare function loadEnv(directory: URL | string, options?: LoaderOptions): Promise<LoadedEnvFile[]>;
225
+ //#endregion
226
+ //#region src/parser.d.ts
227
+ interface ParseOptions {
228
+ /**
229
+ * Ignore existing process.env values when resolving interpolations
230
+ * @default false
231
+ */
232
+ ignoreProcessEnv?: boolean;
233
+ /**
234
+ * Custom environment source for interpolation
235
+ */
236
+ envSource?: Record<string, string | undefined>;
237
+ }
238
+ /**
239
+ * Parse .env file contents into a key-value object
240
+ *
241
+ * Supports:
242
+ * - Comments (lines starting with #)
243
+ * - Empty lines
244
+ * - Single and double quoted values
245
+ * - Multi-line values with quoted strings
246
+ * - Variable interpolation with $VAR or ${VAR} syntax (not in single quotes)
247
+ *
248
+ * @example
249
+ * ```ts
250
+ * const parser = new EnvParser(`
251
+ * PORT=3000
252
+ * HOST=localhost
253
+ * MESSAGE="Hello World"
254
+ * `)
255
+ * const result = parser.parse()
256
+ * // { PORT: '3000', HOST: 'localhost', MESSAGE: 'Hello World' }
257
+ * ```
258
+ */
259
+ declare class EnvParser {
260
+ private readonly contents;
261
+ private readonly options;
262
+ constructor(contents: string, options?: ParseOptions);
263
+ /**
264
+ * Parse the .env contents and return a key-value object
265
+ */
266
+ parse(): ParsedEnv;
267
+ /**
268
+ * Extract value from quoted string, removing the quotes
269
+ */
270
+ private extractQuotedValue;
271
+ /**
272
+ * Interpolate variables in a value
273
+ * Supports $VAR and ${VAR} syntax
274
+ */
275
+ private interpolate;
276
+ }
277
+ /**
278
+ * Convenience function to parse .env contents
279
+ */
280
+ declare function parseEnv(contents: string, options?: ParseOptions): ParsedEnv;
281
+ //#endregion
282
+ //#region src/validator.d.ts
283
+ /**
284
+ * Validator for environment variables using Standard Schema
285
+ *
286
+ * @example
287
+ * ```ts
288
+ * import { EnvValidator } from 'enwow'
289
+ * import { z } from 'zod'
290
+ *
291
+ * const validator = new EnvValidator({
292
+ * PORT: z.string().transform(Number),
293
+ * HOST: z.string().default('localhost'),
294
+ * })
295
+ *
296
+ * const result = validator.validate(process.env)
297
+ * // result is typed as { PORT: number, HOST: string }
298
+ * ```
299
+ */
300
+ declare class EnvValidator<T extends EnvSchema> {
301
+ private readonly schema;
302
+ constructor(schema: T);
303
+ /**
304
+ * Validate environment variables against the schema
305
+ * @param env - The environment variables to validate
306
+ * @returns The validated and transformed environment variables
307
+ * @throws EnvValidationError if validation fails
308
+ */
309
+ validate(env: Record<string, string | undefined>): InferEnv<T>;
310
+ /**
311
+ * Validate a single value against its schema
312
+ */
313
+ private validateValue;
314
+ }
315
+ /**
316
+ * Create a validator for environment variables
317
+ * @param schema - The schema definition
318
+ * @returns A validator instance
319
+ */
320
+ declare function createValidator<T extends EnvSchema>(schema: T): EnvValidator<T>;
321
+ //#endregion
322
+ export { Env, type EnvCreateOptions, type EnvIssue, EnvLoader, EnvMissingError, EnvParser, type EnvSchema, EnvValidationError, EnvValidator, type InferEnv, type LoadedEnvFile, type LoaderOptions, type ParseOptions, type ParsedEnv, type Schema, type SchemaInput, type SchemaOutput, createValidator, loadEnv, parseEnv };
package/dist/index.mjs ADDED
@@ -0,0 +1,416 @@
1
+ import { env, runtime } from "std-env";
2
+ import { basename, join } from "pathe";
3
+
4
+ //#region src/loader.ts
5
+ /**
6
+ * Environment file loader
7
+ *
8
+ * Loads .env files from a directory with the following priority (highest first):
9
+ * 1. .env.[NODE_ENV].local - Loaded when NODE_ENV is set
10
+ * 2. .env.local - Loaded in all environments except test
11
+ * 3. .env.[NODE_ENV] - Loaded when NODE_ENV is set
12
+ * 4. .env - Loaded in all environments
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { EnvLoader } from 'enwow'
17
+ *
18
+ * const loader = new EnvLoader(new URL('./', import.meta.url))
19
+ * const files = await loader.load()
20
+ *
21
+ * for (const file of files) {
22
+ * console.log(file.path, file.contents)
23
+ * }
24
+ * ```
25
+ */
26
+ var EnvLoader = class {
27
+ directory;
28
+ options;
29
+ /**
30
+ * Create a new EnvLoader
31
+ * @param directory - The directory to load .env files from (as URL or string path)
32
+ * @param options - Loader options
33
+ */
34
+ constructor(directory, options = {}) {
35
+ this.directory = directory instanceof URL ? directory.pathname : directory;
36
+ this.options = options;
37
+ }
38
+ /**
39
+ * Load all .env files and return their contents
40
+ * Files are returned in priority order (highest priority first)
41
+ */
42
+ async load() {
43
+ const files = await this.getFileNames();
44
+ const results = [];
45
+ for (const fileName of files) {
46
+ const filePath = join(this.directory, fileName);
47
+ try {
48
+ const contents = await this.readFile(filePath);
49
+ if (contents !== null) results.push({
50
+ path: filePath,
51
+ contents
52
+ });
53
+ } catch {}
54
+ }
55
+ return results;
56
+ }
57
+ /**
58
+ * Get the list of .env file names to load, in priority order
59
+ */
60
+ async getFileNames() {
61
+ if (this.options.files) return this.options.files.map((f) => basename(f));
62
+ const nodeEnv = this.options.nodeEnv ?? await this.getNodeEnv();
63
+ const files = [];
64
+ if (nodeEnv) files.push(`.env.${nodeEnv}.local`);
65
+ if (!(nodeEnv === "test" || nodeEnv === "testing")) files.push(".env.local");
66
+ if (nodeEnv) files.push(`.env.${nodeEnv}`);
67
+ files.push(".env");
68
+ return files;
69
+ }
70
+ async getNodeEnv() {
71
+ return env.NODE_ENV;
72
+ }
73
+ /**
74
+ * Read a file contents in a cross-platform way
75
+ */
76
+ async readFile(path) {
77
+ if (runtime === "bun") {
78
+ const file = Bun.file(path);
79
+ if (!await file.exists()) return null;
80
+ return file.text();
81
+ }
82
+ if (runtime === "deno") try {
83
+ return await Deno.readTextFile(path);
84
+ } catch {
85
+ return null;
86
+ }
87
+ const { readFile } = await import("node:fs/promises");
88
+ try {
89
+ return await readFile(path, "utf-8");
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+ };
95
+ /**
96
+ * Convenience function to load .env files from a directory
97
+ */
98
+ async function loadEnv(directory, options) {
99
+ return new EnvLoader(directory, options).load();
100
+ }
101
+
102
+ //#endregion
103
+ //#region src/parser.ts
104
+ /**
105
+ * Parse .env file contents into a key-value object
106
+ *
107
+ * Supports:
108
+ * - Comments (lines starting with #)
109
+ * - Empty lines
110
+ * - Single and double quoted values
111
+ * - Multi-line values with quoted strings
112
+ * - Variable interpolation with $VAR or ${VAR} syntax (not in single quotes)
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * const parser = new EnvParser(`
117
+ * PORT=3000
118
+ * HOST=localhost
119
+ * MESSAGE="Hello World"
120
+ * `)
121
+ * const result = parser.parse()
122
+ * // { PORT: '3000', HOST: 'localhost', MESSAGE: 'Hello World' }
123
+ * ```
124
+ */
125
+ var EnvParser = class {
126
+ contents;
127
+ options;
128
+ constructor(contents, options = {}) {
129
+ this.contents = contents;
130
+ this.options = options;
131
+ }
132
+ /**
133
+ * Parse the .env contents and return a key-value object
134
+ */
135
+ parse() {
136
+ const result = {};
137
+ const lines = this.contents.split(/\r?\n/);
138
+ let currentLine = "";
139
+ let inQuotes = null;
140
+ let currentKey = "";
141
+ for (let i = 0; i < lines.length; i++) {
142
+ const line = lines[i];
143
+ if (inQuotes) {
144
+ currentLine += `\n${line}`;
145
+ if (line.includes(inQuotes)) {
146
+ const cleanValue = this.extractQuotedValue(currentLine, inQuotes);
147
+ if (inQuotes === "'") result[currentKey] = cleanValue;
148
+ else result[currentKey] = this.interpolate(cleanValue, result);
149
+ inQuotes = null;
150
+ currentLine = "";
151
+ currentKey = "";
152
+ }
153
+ continue;
154
+ }
155
+ const trimmed = line.trim();
156
+ if (!trimmed || trimmed.startsWith("#")) continue;
157
+ const equalsIndex = trimmed.indexOf("=");
158
+ if (equalsIndex === -1) continue;
159
+ const key = trimmed.slice(0, equalsIndex).trim();
160
+ let value = trimmed.slice(equalsIndex + 1);
161
+ const firstChar = value[0];
162
+ if (firstChar === "\"" || firstChar === "'") {
163
+ const closingQuote = value.lastIndexOf(firstChar);
164
+ if (closingQuote > 0 && closingQuote !== 0) {
165
+ const cleanValue = this.extractQuotedValue(value, firstChar);
166
+ if (firstChar === "'") result[key] = cleanValue;
167
+ else result[key] = this.interpolate(cleanValue, result);
168
+ } else {
169
+ inQuotes = firstChar;
170
+ currentLine = value;
171
+ currentKey = key;
172
+ }
173
+ } else {
174
+ const commentIndex = value.indexOf(" #");
175
+ if (commentIndex !== -1) value = value.slice(0, commentIndex);
176
+ value = value.trim();
177
+ result[key] = this.interpolate(value, result);
178
+ }
179
+ }
180
+ return result;
181
+ }
182
+ /**
183
+ * Extract value from quoted string, removing the quotes
184
+ */
185
+ extractQuotedValue(value, quote) {
186
+ let cleanValue = value.slice(1);
187
+ const closingIndex = cleanValue.lastIndexOf(quote);
188
+ if (closingIndex !== -1) cleanValue = cleanValue.slice(0, closingIndex);
189
+ return cleanValue;
190
+ }
191
+ /**
192
+ * Interpolate variables in a value
193
+ * Supports $VAR and ${VAR} syntax
194
+ */
195
+ interpolate(value, parsed) {
196
+ const envSource = this.options.envSource ?? env;
197
+ let result = value.replace(/\$\{([^}]+)\}/g, (_, varName) => {
198
+ if (parsed[varName] !== void 0) return parsed[varName];
199
+ if (!this.options.ignoreProcessEnv && envSource[varName] !== void 0) return envSource[varName];
200
+ return "";
201
+ });
202
+ result = result.replace(/(?<!\$)\$([A-Z_]\w*)/gi, (_, varName) => {
203
+ if (parsed[varName] !== void 0) return parsed[varName];
204
+ if (!this.options.ignoreProcessEnv && envSource[varName] !== void 0) return envSource[varName];
205
+ return "";
206
+ });
207
+ return result;
208
+ }
209
+ };
210
+ /**
211
+ * Convenience function to parse .env contents
212
+ */
213
+ function parseEnv(contents, options) {
214
+ return new EnvParser(contents, options).parse();
215
+ }
216
+
217
+ //#endregion
218
+ //#region src/errors.ts
219
+ /**
220
+ * Error thrown when environment variables validation fails
221
+ */
222
+ var EnvValidationError = class EnvValidationError extends Error {
223
+ name = "EnvValidationError";
224
+ issues;
225
+ constructor(messageOrIssues, issues) {
226
+ if (typeof messageOrIssues === "string") {
227
+ super(messageOrIssues);
228
+ this.issues = issues ?? [];
229
+ } else {
230
+ super("Environment variables validation failed");
231
+ this.issues = messageOrIssues;
232
+ }
233
+ Error.captureStackTrace?.(this, EnvValidationError);
234
+ }
235
+ };
236
+ /**
237
+ * Error thrown when a required environment variable is missing
238
+ */
239
+ var EnvMissingError = class EnvMissingError extends Error {
240
+ name = "EnvMissingError";
241
+ variableName;
242
+ constructor(variableName) {
243
+ super(`Missing required environment variable: ${variableName}`);
244
+ this.variableName = variableName;
245
+ Error.captureStackTrace?.(this, EnvMissingError);
246
+ }
247
+ };
248
+
249
+ //#endregion
250
+ //#region src/validator.ts
251
+ /**
252
+ * Validator for environment variables using Standard Schema
253
+ *
254
+ * @example
255
+ * ```ts
256
+ * import { EnvValidator } from 'enwow'
257
+ * import { z } from 'zod'
258
+ *
259
+ * const validator = new EnvValidator({
260
+ * PORT: z.string().transform(Number),
261
+ * HOST: z.string().default('localhost'),
262
+ * })
263
+ *
264
+ * const result = validator.validate(process.env)
265
+ * // result is typed as { PORT: number, HOST: string }
266
+ * ```
267
+ */
268
+ var EnvValidator = class {
269
+ schema;
270
+ constructor(schema) {
271
+ this.schema = schema;
272
+ }
273
+ /**
274
+ * Validate environment variables against the schema
275
+ * @param env - The environment variables to validate
276
+ * @returns The validated and transformed environment variables
277
+ * @throws EnvValidationError if validation fails
278
+ */
279
+ validate(env$1) {
280
+ const result = {};
281
+ const issues = [];
282
+ for (const [key, schema] of Object.entries(this.schema)) {
283
+ const value = env$1[key];
284
+ const validationResult = this.validateValue(schema, value, key);
285
+ if (validationResult.success) result[key] = validationResult.value;
286
+ else issues.push(...validationResult.issues);
287
+ }
288
+ if (issues.length > 0) throw new EnvValidationError(issues);
289
+ return result;
290
+ }
291
+ /**
292
+ * Validate a single value against its schema
293
+ */
294
+ validateValue(schema, value, key) {
295
+ const result = schema["~standard"].validate(value);
296
+ if (result instanceof Promise) throw new TypeError(`Async validation is not supported. The schema for "${key}" appears to be async. Please use synchronous schemas only.`);
297
+ if (result.issues === void 0) return {
298
+ success: true,
299
+ value: result.value
300
+ };
301
+ return {
302
+ success: false,
303
+ issues: result.issues.map((issue) => ({
304
+ path: key,
305
+ message: issue.message
306
+ }))
307
+ };
308
+ }
309
+ };
310
+ /**
311
+ * Create a validator for environment variables
312
+ * @param schema - The schema definition
313
+ * @returns A validator instance
314
+ */
315
+ function createValidator(schema) {
316
+ return new EnvValidator(schema);
317
+ }
318
+
319
+ //#endregion
320
+ //#region src/env.ts
321
+ /**
322
+ * Env class for managing environment variables
323
+ *
324
+ * Provides type-safe access to environment variables after validation
325
+ *
326
+ * @example
327
+ * ```ts
328
+ * import { Env } from 'enwow'
329
+ * import { z } from 'zod'
330
+ *
331
+ * // Create with path to load .env files
332
+ * const env = await Env.create(new URL('./', import.meta.url), {
333
+ * PORT: z.string().transform(Number).default('3000'),
334
+ * HOST: z.string().default('localhost'),
335
+ * })
336
+ *
337
+ * // Or create without path (only process.env)
338
+ * const env = await Env.create({
339
+ * PORT: z.string().transform(Number),
340
+ * })
341
+ *
342
+ * // Type-safe access
343
+ * env.get('PORT') // number
344
+ * env.get('HOST') // string
345
+ * ```
346
+ */
347
+ var Env = class Env {
348
+ values;
349
+ constructor(values) {
350
+ this.values = values;
351
+ }
352
+ static async create(pathOrSchema, schemaOrOptions, options) {
353
+ let path;
354
+ let schema;
355
+ let opts = {};
356
+ if (pathOrSchema instanceof URL) {
357
+ path = pathOrSchema;
358
+ schema = schemaOrOptions;
359
+ opts = options ?? {};
360
+ } else {
361
+ schema = pathOrSchema;
362
+ opts = schemaOrOptions ?? {};
363
+ }
364
+ let envValues = {};
365
+ if (path) {
366
+ const files = await new EnvLoader(path, { nodeEnv: opts.nodeEnv }).load();
367
+ for (const file of files.reverse()) {
368
+ const parsed = new EnvParser(file.contents, {
369
+ ignoreProcessEnv: opts.ignoreProcessEnv,
370
+ envSource: opts.envSource
371
+ }).parse();
372
+ envValues = {
373
+ ...envValues,
374
+ ...parsed
375
+ };
376
+ }
377
+ }
378
+ if (!opts.ignoreProcessEnv) {
379
+ const processEnv = opts.envSource ?? getProcessEnv();
380
+ envValues = {
381
+ ...envValues,
382
+ ...processEnv
383
+ };
384
+ }
385
+ return new Env(new EnvValidator(schema).validate(envValues));
386
+ }
387
+ /**
388
+ * Get a validated environment variable value
389
+ * @param key - The variable name
390
+ * @returns The validated value
391
+ */
392
+ get(key) {
393
+ return this.values[key];
394
+ }
395
+ /**
396
+ * Get all validated environment variables as an object
397
+ */
398
+ all() {
399
+ return { ...this.values };
400
+ }
401
+ /**
402
+ * Check if a variable is defined
403
+ */
404
+ has(key) {
405
+ return this.values[key] !== void 0;
406
+ }
407
+ };
408
+ /**
409
+ * Get the current process environment in a cross-platform way
410
+ */
411
+ function getProcessEnv() {
412
+ return env;
413
+ }
414
+
415
+ //#endregion
416
+ export { Env, EnvLoader, EnvMissingError, EnvParser, EnvValidationError, EnvValidator, createValidator, loadEnv, parseEnv };
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "enwow",
3
+ "type": "module",
4
+ "version": "0.0.1",
5
+ "description": "Cross-platform environment variables loader and validator with Standard Schema support",
6
+ "author": "blasdfaa",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/blasdfaa/enwow#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/blasdfaa/enwow.git"
12
+ },
13
+ "bugs": "https://github.com/blasdfaa/enwow/issues",
14
+ "keywords": [
15
+ "env",
16
+ "environment",
17
+ "variables",
18
+ "dotenv",
19
+ "validation",
20
+ "standard-schema",
21
+ "zod",
22
+ "valibot",
23
+ "arktype",
24
+ "typescript"
25
+ ],
26
+ "sideEffects": false,
27
+ "exports": {
28
+ ".": "./dist/index.mjs",
29
+ "./package.json": "./package.json"
30
+ },
31
+ "main": "./dist/index.mjs",
32
+ "module": "./dist/index.mjs",
33
+ "types": "./dist/index.d.mts",
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "dependencies": {
38
+ "@standard-schema/spec": "^1.0.0",
39
+ "pathe": "^2.0.0",
40
+ "std-env": "^4.0.0-rc.1"
41
+ },
42
+ "devDependencies": {
43
+ "@antfu/eslint-config": "^6.6.1",
44
+ "@antfu/ni": "^28.0.0",
45
+ "@antfu/utils": "^9.3.0",
46
+ "@standard-schema/spec": "^1.0.0",
47
+ "@types/node": "^25.0.1",
48
+ "bumpp": "^10.3.2",
49
+ "eslint": "^9.39.2",
50
+ "lint-staged": "^16.2.7",
51
+ "publint": "^0.3.16",
52
+ "simple-git-hooks": "^2.13.1",
53
+ "tinyexec": "^1.0.2",
54
+ "tsdown": "^0.17.3",
55
+ "tsx": "^4.21.0",
56
+ "typescript": "^5.9.3",
57
+ "vite": "^7.2.7",
58
+ "vitest": "^4.0.15",
59
+ "vitest-package-exports": "^0.1.1",
60
+ "yaml": "^2.8.2",
61
+ "zod": "^3.24.0"
62
+ },
63
+ "simple-git-hooks": {
64
+ "pre-commit": "pnpm i --frozen-lockfile --ignore-scripts --offline && npx lint-staged"
65
+ },
66
+ "lint-staged": {
67
+ "*": "eslint --fix"
68
+ },
69
+ "scripts": {
70
+ "build": "tsdown",
71
+ "dev": "tsdown --watch",
72
+ "lint": "eslint",
73
+ "release": "bumpp",
74
+ "test": "vitest",
75
+ "typecheck": "tsc"
76
+ }
77
+ }