@emergente-labs/effect-env 0.1.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 +21 -0
- package/README.md +80 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.js +142 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Emergente Labs
|
|
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,80 @@
|
|
|
1
|
+
# @emergente-labs/effect-env
|
|
2
|
+
|
|
3
|
+
Type-safe environment variable configuration for [Effect-TS](https://effect.website) applications. The "T3 env" equivalent for Effect -- clean DX, per-environment defaults, proxy access, and test helpers.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @emergente-labs/effect-env effect
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createEnv } from "@emergente-labs/effect-env";
|
|
15
|
+
import { Config, Effect, Redacted } from "effect";
|
|
16
|
+
|
|
17
|
+
const { env, requiredSecrets } = createEnv({
|
|
18
|
+
vars: {
|
|
19
|
+
// Required in all environments
|
|
20
|
+
databaseUrl: Config.redacted("DATABASE_URL"),
|
|
21
|
+
|
|
22
|
+
// Optional in development, required in production
|
|
23
|
+
apiKey: [Config.string("API_KEY"), "dev-key-123"] as const,
|
|
24
|
+
|
|
25
|
+
// Number config with default
|
|
26
|
+
port: [Config.number("PORT"), 3000] as const,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// requiredSecrets = ["DATABASE_URL"]
|
|
31
|
+
|
|
32
|
+
// Access individual variables
|
|
33
|
+
const program = Effect.gen(function* () {
|
|
34
|
+
const dbUrl = yield* env.databaseUrl; // Redacted<string>
|
|
35
|
+
const key = yield* env.apiKey; // string
|
|
36
|
+
const port = yield* env.port; // number
|
|
37
|
+
|
|
38
|
+
// Or access all at once
|
|
39
|
+
const all = yield* env;
|
|
40
|
+
// { databaseUrl: Redacted<string>, apiKey: string, port: number }
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## API
|
|
45
|
+
|
|
46
|
+
### `createEnv(options)`
|
|
47
|
+
|
|
48
|
+
Creates a typed environment configuration from a record of Config definitions.
|
|
49
|
+
|
|
50
|
+
Each variable can be:
|
|
51
|
+
- `Config.Config<T>` -- required in all environments
|
|
52
|
+
- `[Config.Config<T>, T] as const` -- required only in production, uses default in dev/test
|
|
53
|
+
|
|
54
|
+
Returns `{ env, requiredSecrets }`:
|
|
55
|
+
- `env` -- dual-access proxy: `yield* env.key` for individual values, `yield* env` for all
|
|
56
|
+
- `requiredSecrets` -- array of env var names that must be set in production
|
|
57
|
+
|
|
58
|
+
### `createEnvVar(config, defaultValueInDevelopment?)`
|
|
59
|
+
|
|
60
|
+
Lower-level function for creating individual environment-aware configs.
|
|
61
|
+
|
|
62
|
+
### `createTestEnvLayer(envMap)`
|
|
63
|
+
|
|
64
|
+
Creates an Effect Layer that provides mock environment variables for testing.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { createTestEnvLayer } from "@emergente-labs/effect-env";
|
|
68
|
+
|
|
69
|
+
const testLayer = createTestEnvLayer({
|
|
70
|
+
DATABASE_URL: "postgresql://localhost:5432/test",
|
|
71
|
+
NODE_ENV: "test",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Use in tests
|
|
75
|
+
const result = yield* myEffect.pipe(Effect.provide(testLayer));
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Config, Layer } from 'effect';
|
|
2
|
+
|
|
3
|
+
/** A config with no default (required in all environments) */
|
|
4
|
+
type RequiredConfig<T> = Config.Config<T>;
|
|
5
|
+
/** A config with a default value (required only in production) */
|
|
6
|
+
type OptionalConfig<T> = readonly [Config.Config<T>, T];
|
|
7
|
+
/** Union of both config types */
|
|
8
|
+
type EnvVarDefinition<T = unknown> = RequiredConfig<T> | OptionalConfig<T>;
|
|
9
|
+
/**
|
|
10
|
+
* The input record type for `createEnv`. Each key maps to either:
|
|
11
|
+
* - `Config.Config<T>` - required in all environments
|
|
12
|
+
* - `readonly [Config.Config<T>, T]` - required only in production, uses default in development
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const vars: EnvVarsInput = {
|
|
16
|
+
* databaseUrl: Config.redacted("DATABASE_URL"),
|
|
17
|
+
* apiKey: [Config.string("API_KEY"), "dev-key"] as const,
|
|
18
|
+
* };
|
|
19
|
+
*/
|
|
20
|
+
type EnvVarsInput = Record<string, EnvVarDefinition>;
|
|
21
|
+
/** Extract the success type from a config definition */
|
|
22
|
+
type ExtractConfigType<T> = T extends OptionalConfig<infer U> ? U : T extends RequiredConfig<infer U> ? U : never;
|
|
23
|
+
/** Map input record to resolved Config types */
|
|
24
|
+
type ResolvedEnvVars<T extends EnvVarsInput> = {
|
|
25
|
+
[K in keyof T]: Config.Config<ExtractConfigType<T[K]>>;
|
|
26
|
+
};
|
|
27
|
+
/** The resolved plain values object */
|
|
28
|
+
type ResolvedEnvValues<T extends EnvVarsInput> = {
|
|
29
|
+
[K in keyof T]: ExtractConfigType<T[K]>;
|
|
30
|
+
};
|
|
31
|
+
/** The proxy type that allows both `yield* env.key` and `yield* env` */
|
|
32
|
+
type EnvProxy<T extends EnvVarsInput> = Config.Config<ResolvedEnvValues<T>> & ResolvedEnvVars<T>;
|
|
33
|
+
/** The env proxy combined with metadata about required secrets */
|
|
34
|
+
interface CreateEnvReturn<EnvVars extends EnvVarsInput> {
|
|
35
|
+
/** The env proxy that supports `yield* env.key` and `yield* env` */
|
|
36
|
+
readonly env: EnvProxy<EnvVars>;
|
|
37
|
+
/** Environment variable names that are required (no default value) */
|
|
38
|
+
readonly requiredSecrets: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Options for `createEnv`.
|
|
42
|
+
*
|
|
43
|
+
* @template EnvVars - The record of environment variable definitions
|
|
44
|
+
*/
|
|
45
|
+
interface CreateEnvOptions<EnvVars extends EnvVarsInput> {
|
|
46
|
+
/** Record of environment variable definitions */
|
|
47
|
+
vars: EnvVars;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates a typed environment configuration from a record of Config definitions.
|
|
52
|
+
*
|
|
53
|
+
* Each variable can be defined as:
|
|
54
|
+
* - `Config.Config<T>` - required in all environments
|
|
55
|
+
* - `[Config.Config<T>, T] as const` - required only in production, uses the
|
|
56
|
+
* provided default value in development/test environments
|
|
57
|
+
*
|
|
58
|
+
* @template EnvVars - The record of environment variable definitions
|
|
59
|
+
*
|
|
60
|
+
* @param options - Configuration options
|
|
61
|
+
* @param options.vars - Record of environment variable definitions
|
|
62
|
+
*
|
|
63
|
+
* @returns An object containing:
|
|
64
|
+
* - `env`: An Effect Config proxy that supports both `yield* env.key` for
|
|
65
|
+
* individual values and `yield* env` for all values.
|
|
66
|
+
* - `requiredSecrets`: Array of environment variable names that are required
|
|
67
|
+
* (no default value) and must be set for deployment.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* const { env, requiredSecrets } = createEnv({
|
|
71
|
+
* vars: {
|
|
72
|
+
* databaseUrl: Config.redacted("DATABASE_URL"),
|
|
73
|
+
* apiKey: [Config.string("API_KEY"), "dev-key-123"] as const,
|
|
74
|
+
* },
|
|
75
|
+
* });
|
|
76
|
+
*
|
|
77
|
+
* // requiredSecrets = ["DATABASE_URL"]
|
|
78
|
+
*
|
|
79
|
+
* // In an Effect generator:
|
|
80
|
+
* const dbUrl = yield* env.databaseUrl; // Redacted
|
|
81
|
+
* const key = yield* env.apiKey; // string
|
|
82
|
+
* const all = yield* env; // { databaseUrl: Redacted, apiKey: string }
|
|
83
|
+
*/
|
|
84
|
+
declare function createEnv<EnvVars extends EnvVarsInput>(options: CreateEnvOptions<EnvVars>): CreateEnvReturn<EnvVars>;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Creates an environment variable configuration with optional development fallback.
|
|
88
|
+
*
|
|
89
|
+
* @template T - The type of the environment variable value
|
|
90
|
+
* @param config - The Config instance to read the environment variable
|
|
91
|
+
* @param defaultValueInDevelopment - Optional default value to use in non-production environments.
|
|
92
|
+
* - If provided: Variable is required only in production, uses default in development
|
|
93
|
+
* - If omitted: Variable is required in all environments
|
|
94
|
+
* @returns A Config that validates the environment variable based on the current NODE_ENV
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* // Required in all environments (no default)
|
|
98
|
+
* const dbUrl = createEnvVar(Config.string("DATABASE_URL"));
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* // Required only in production, uses default in development
|
|
102
|
+
* const apiKey = createEnvVar(Config.string("API_KEY"), "dev-key-123");
|
|
103
|
+
*/
|
|
104
|
+
declare function createEnvVar<T>(config: Config.Config<T>, defaultValueInDevelopment?: T): Config.Config<T>;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Creates a test layer with a given set of environment variables.
|
|
108
|
+
* Useful for testing code that depends on environment variables.
|
|
109
|
+
*
|
|
110
|
+
* @param envMap - A Map or Record of environment variable names to their values
|
|
111
|
+
* @returns A Layer that provides the environment variables to the Effect runtime
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* const testLayer = createTestEnvLayer(new Map([
|
|
115
|
+
* ["DATABASE_URL", "postgresql://user:password@localhost:5432/db"],
|
|
116
|
+
* ["NODE_ENV", "test"],
|
|
117
|
+
* ]));
|
|
118
|
+
*/
|
|
119
|
+
declare function createTestEnvLayer(envMap: Map<string, string> | Record<string, string>): Layer.Layer<never>;
|
|
120
|
+
|
|
121
|
+
export { type CreateEnvOptions, type CreateEnvReturn, type EnvProxy, type EnvVarsInput, type ResolvedEnvValues, type ResolvedEnvVars, createEnv, createEnvVar, createTestEnvLayer };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// src/createEnv.ts
|
|
2
|
+
import { Config as Config2 } from "effect";
|
|
3
|
+
|
|
4
|
+
// src/createEnvVar.ts
|
|
5
|
+
import { Config, Option, ConfigError, Either, Schema } from "effect";
|
|
6
|
+
var environment = Config.string("NODE_ENV");
|
|
7
|
+
var configSchema = Schema.Union(
|
|
8
|
+
Schema.Struct({
|
|
9
|
+
name: Schema.String
|
|
10
|
+
}),
|
|
11
|
+
Schema.Struct({
|
|
12
|
+
original: Schema.suspend(() => configSchema)
|
|
13
|
+
}),
|
|
14
|
+
Schema.Struct({
|
|
15
|
+
config: Schema.suspend(() => configSchema)
|
|
16
|
+
}),
|
|
17
|
+
Schema.Struct({
|
|
18
|
+
first: Schema.suspend(() => configSchema),
|
|
19
|
+
second: Schema.suspend(() => configSchema)
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
var extractPath = (v) => {
|
|
23
|
+
if ("name" in v) {
|
|
24
|
+
return [v.name];
|
|
25
|
+
}
|
|
26
|
+
if ("original" in v) {
|
|
27
|
+
const extracted = extractPath(v.original);
|
|
28
|
+
if (extracted.length) {
|
|
29
|
+
return extracted;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if ("first" in v) {
|
|
33
|
+
const first = extractPath(v.first);
|
|
34
|
+
if (first.length > 0) return first;
|
|
35
|
+
return extractPath(v.second);
|
|
36
|
+
}
|
|
37
|
+
if ("config" in v) {
|
|
38
|
+
const extracted = extractPath(v.config);
|
|
39
|
+
if (extracted.length) {
|
|
40
|
+
return extracted;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return [];
|
|
44
|
+
};
|
|
45
|
+
function getConfigPath(config) {
|
|
46
|
+
const decoded = Schema.decodeUnknownEither(configSchema)(config);
|
|
47
|
+
return Either.match(decoded, {
|
|
48
|
+
onLeft: () => [],
|
|
49
|
+
onRight: extractPath
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function createMissingDataError(config, optionalInDevelopment) {
|
|
53
|
+
const path = getConfigPath(config);
|
|
54
|
+
const message = optionalInDevelopment ? "Environment variable required in production" : "Environment variable required in all environments";
|
|
55
|
+
return ConfigError.MissingData(path, message);
|
|
56
|
+
}
|
|
57
|
+
function isValueRequired(env, optionalInDevelopment) {
|
|
58
|
+
if (optionalInDevelopment) {
|
|
59
|
+
return env === "production";
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
function createEnvVar(config, defaultValueInDevelopment) {
|
|
64
|
+
const optionalInDevelopment = defaultValueInDevelopment !== void 0;
|
|
65
|
+
return Config.all({
|
|
66
|
+
env: environment,
|
|
67
|
+
value: Config.option(config)
|
|
68
|
+
}).pipe(
|
|
69
|
+
Config.mapOrFail(({ env, value }) => {
|
|
70
|
+
const isRequired = isValueRequired(env, optionalInDevelopment);
|
|
71
|
+
if (isRequired && Option.isNone(value)) {
|
|
72
|
+
return Either.left(createMissingDataError(config, optionalInDevelopment));
|
|
73
|
+
}
|
|
74
|
+
return Either.right(
|
|
75
|
+
Option.getOrElse(() => defaultValueInDevelopment)(value)
|
|
76
|
+
);
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/createEnv.ts
|
|
82
|
+
function isOptionalConfig(value) {
|
|
83
|
+
return Array.isArray(value) && value.length === 2;
|
|
84
|
+
}
|
|
85
|
+
function hasName(config) {
|
|
86
|
+
return "name" in config && typeof config.name === "string";
|
|
87
|
+
}
|
|
88
|
+
function hasOriginal(config) {
|
|
89
|
+
return "original" in config && config.original !== null && typeof config.original === "object";
|
|
90
|
+
}
|
|
91
|
+
function hasFirst(config) {
|
|
92
|
+
return "first" in config && config.first !== null && typeof config.first === "object";
|
|
93
|
+
}
|
|
94
|
+
function extractConfigName(config) {
|
|
95
|
+
if (hasName(config)) return config.name;
|
|
96
|
+
if (hasOriginal(config)) return extractConfigName(config.original);
|
|
97
|
+
if (hasFirst(config)) return extractConfigName(config.first);
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
function createEnv(options) {
|
|
101
|
+
const processedVars = {};
|
|
102
|
+
const requiredSecrets = [];
|
|
103
|
+
for (const key of Object.keys(options.vars)) {
|
|
104
|
+
const value = options.vars[key];
|
|
105
|
+
if (value === void 0) continue;
|
|
106
|
+
if (isOptionalConfig(value)) {
|
|
107
|
+
processedVars[key] = createEnvVar(value[0], value[1]);
|
|
108
|
+
} else {
|
|
109
|
+
processedVars[key] = createEnvVar(value);
|
|
110
|
+
const envVarName = extractConfigName(value);
|
|
111
|
+
if (envVarName) {
|
|
112
|
+
requiredSecrets.push(envVarName);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const allEnvVars = Config2.all(processedVars);
|
|
117
|
+
const env = new Proxy(allEnvVars, {
|
|
118
|
+
get(target, prop) {
|
|
119
|
+
if (typeof prop === "string" && prop in processedVars) {
|
|
120
|
+
return Config2.map(
|
|
121
|
+
target,
|
|
122
|
+
(allValues) => allValues[prop]
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return target[prop];
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
return { env, requiredSecrets };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/createTestEnvLayer.ts
|
|
132
|
+
import { Layer, ConfigProvider } from "effect";
|
|
133
|
+
function createTestEnvLayer(envMap) {
|
|
134
|
+
const map = envMap instanceof Map ? envMap : new Map(Object.entries(envMap));
|
|
135
|
+
return Layer.setConfigProvider(ConfigProvider.fromMap(map));
|
|
136
|
+
}
|
|
137
|
+
export {
|
|
138
|
+
createEnv,
|
|
139
|
+
createEnvVar,
|
|
140
|
+
createTestEnvLayer
|
|
141
|
+
};
|
|
142
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/createEnv.ts","../src/createEnvVar.ts","../src/createTestEnvLayer.ts"],"sourcesContent":["import { Config } from \"effect\";\nimport { createEnvVar } from \"./createEnvVar\";\nimport type {\n EnvVarsInput,\n EnvVarDefinition,\n OptionalConfig,\n CreateEnvOptions,\n CreateEnvReturn,\n ResolvedEnvValues,\n EnvProxy,\n} from \"./types\";\n\nfunction isOptionalConfig<T>(\n value: EnvVarDefinition<T>,\n): value is OptionalConfig<T> {\n return Array.isArray(value) && value.length === 2;\n}\n\n/** Config with a direct name property (primitive configs like string, number, etc.) */\ninterface ConfigWithName {\n readonly name: string;\n}\n\n/** Config that wraps another config (mapped, validated, branded) */\ninterface ConfigWithOriginal {\n readonly original: Config.Config<unknown>;\n}\n\n/** Config with a fallback (orElse, withDefault) */\ninterface ConfigWithFirst {\n readonly first: Config.Config<unknown>;\n}\n\nfunction hasName(config: Config.Config<unknown>): config is Config.Config<unknown> & ConfigWithName {\n return \"name\" in config && typeof config.name === \"string\";\n}\n\nfunction hasOriginal(config: Config.Config<unknown>): config is Config.Config<unknown> & ConfigWithOriginal {\n return \"original\" in config && config.original !== null && typeof config.original === \"object\";\n}\n\nfunction hasFirst(config: Config.Config<unknown>): config is Config.Config<unknown> & ConfigWithFirst {\n return \"first\" in config && config.first !== null && typeof config.first === \"object\";\n}\n\n/**\n * Extracts the environment variable name from a Config object.\n * Works with nested, mapped, validated, and other transformed configs.\n */\nfunction extractConfigName(config: Config.Config<unknown>): string | undefined {\n if (hasName(config)) return config.name;\n if (hasOriginal(config)) return extractConfigName(config.original);\n if (hasFirst(config)) return extractConfigName(config.first);\n return undefined;\n}\n\n/**\n * Creates a typed environment configuration from a record of Config definitions.\n *\n * Each variable can be defined as:\n * - `Config.Config<T>` - required in all environments\n * - `[Config.Config<T>, T] as const` - required only in production, uses the\n * provided default value in development/test environments\n *\n * @template EnvVars - The record of environment variable definitions\n *\n * @param options - Configuration options\n * @param options.vars - Record of environment variable definitions\n *\n * @returns An object containing:\n * - `env`: An Effect Config proxy that supports both `yield* env.key` for\n * individual values and `yield* env` for all values.\n * - `requiredSecrets`: Array of environment variable names that are required\n * (no default value) and must be set for deployment.\n *\n * @example\n * const { env, requiredSecrets } = createEnv({\n * vars: {\n * databaseUrl: Config.redacted(\"DATABASE_URL\"),\n * apiKey: [Config.string(\"API_KEY\"), \"dev-key-123\"] as const,\n * },\n * });\n *\n * // requiredSecrets = [\"DATABASE_URL\"]\n *\n * // In an Effect generator:\n * const dbUrl = yield* env.databaseUrl; // Redacted\n * const key = yield* env.apiKey; // string\n * const all = yield* env; // { databaseUrl: Redacted, apiKey: string }\n */\nexport function createEnv<EnvVars extends EnvVarsInput>(\n options: CreateEnvOptions<EnvVars>,\n): CreateEnvReturn<EnvVars> {\n const processedVars: Record<string, Config.Config<unknown>> = {};\n const requiredSecrets: string[] = [];\n\n for (const key of Object.keys(options.vars)) {\n const value = options.vars[key];\n if (value === undefined) continue;\n\n if (isOptionalConfig(value)) {\n // Tuple: [Config, defaultValue] - optional, has a default\n processedVars[key] = createEnvVar(value[0], value[1]);\n } else {\n // Just a Config (required in all environments)\n processedVars[key] = createEnvVar(value);\n // Extract the env var name for required secrets\n const envVarName = extractConfigName(value);\n if (envVarName) {\n requiredSecrets.push(envVarName);\n }\n }\n }\n\n const allEnvVars = Config.all(processedVars) as Config.Config<\n ResolvedEnvValues<EnvVars>\n >;\n\n const env = new Proxy(allEnvVars, {\n get(target, prop: string | symbol) {\n if (typeof prop === \"string\" && prop in processedVars) {\n return Config.map(\n target,\n (allValues) => allValues[prop as keyof typeof allValues],\n );\n }\n return target[prop as keyof typeof target];\n },\n }) as EnvProxy<EnvVars>;\n\n return { env, requiredSecrets };\n}\n","import { Config, Option, ConfigError, Either, Schema } from \"effect\";\n\nconst environment = Config.string(\"NODE_ENV\");\n\n/* walker to get the path from a config type that has transformations, like `Config.string(\"SECRET_KEY\").pipe(Config.array)` */\ninterface ConfigName {\n name: string;\n}\n\ninterface ConfigOriginal {\n original: ConfigType;\n}\n\ninterface ConfigFirstSecond {\n first: ConfigType;\n second: ConfigType;\n}\n\ninterface ConfigBuried {\n config: ConfigType;\n}\n\ntype ConfigType =\n | ConfigName\n | ConfigOriginal\n | ConfigFirstSecond\n | ConfigBuried;\n\nconst configSchema = Schema.Union(\n Schema.Struct({\n name: Schema.String,\n }),\n Schema.Struct({\n original: Schema.suspend((): Schema.Schema<ConfigType> => configSchema),\n }),\n Schema.Struct({\n config: Schema.suspend((): Schema.Schema<ConfigType> => configSchema),\n }),\n Schema.Struct({\n first: Schema.suspend((): Schema.Schema<ConfigType> => configSchema),\n second: Schema.suspend((): Schema.Schema<ConfigType> => configSchema),\n }),\n);\n\nconst extractPath = (v: typeof configSchema.Type): string[] => {\n if (\"name\" in v) {\n return [v.name];\n }\n\n if (\"original\" in v) {\n const extracted = extractPath(v.original);\n if (extracted.length) {\n return extracted;\n }\n }\n\n if (\"first\" in v) {\n const first = extractPath(v.first);\n if (first.length > 0) return first;\n return extractPath(v.second);\n }\n\n if (\"config\" in v) {\n const extracted = extractPath(v.config);\n if (extracted.length) {\n return extracted;\n }\n }\n\n return [];\n};\n/* end of walker */\n\nfunction getConfigPath(config: Config.Config<unknown>): string[] {\n const decoded = Schema.decodeUnknownEither(configSchema)(config);\n return Either.match(decoded, {\n onLeft: () => [],\n onRight: extractPath,\n });\n}\n\nfunction createMissingDataError(\n config: Config.Config<unknown>,\n optionalInDevelopment: boolean,\n): ConfigError.ConfigError {\n const path = getConfigPath(config);\n const message = optionalInDevelopment\n ? \"Environment variable required in production\"\n : \"Environment variable required in all environments\";\n return ConfigError.MissingData(path, message);\n}\n\nfunction isValueRequired(env: string, optionalInDevelopment: boolean): boolean {\n if (optionalInDevelopment) {\n return env === \"production\";\n }\n return true;\n}\n\n/**\n * Creates an environment variable configuration with optional development fallback.\n *\n * @template T - The type of the environment variable value\n * @param config - The Config instance to read the environment variable\n * @param defaultValueInDevelopment - Optional default value to use in non-production environments.\n * - If provided: Variable is required only in production, uses default in development\n * - If omitted: Variable is required in all environments\n * @returns A Config that validates the environment variable based on the current NODE_ENV\n *\n * @example\n * // Required in all environments (no default)\n * const dbUrl = createEnvVar(Config.string(\"DATABASE_URL\"));\n *\n * @example\n * // Required only in production, uses default in development\n * const apiKey = createEnvVar(Config.string(\"API_KEY\"), \"dev-key-123\");\n */\nexport function createEnvVar<T>(\n config: Config.Config<T>,\n defaultValueInDevelopment?: T,\n): Config.Config<T> {\n const optionalInDevelopment = defaultValueInDevelopment !== undefined;\n\n return Config.all({\n env: environment,\n value: Config.option(config),\n }).pipe(\n Config.mapOrFail(({ env, value }) => {\n const isRequired = isValueRequired(env, optionalInDevelopment);\n\n if (isRequired && Option.isNone(value)) {\n return Either.left(createMissingDataError(config, optionalInDevelopment));\n }\n\n return Either.right(\n Option.getOrElse(() => defaultValueInDevelopment as T)(value),\n );\n }),\n );\n}\n","import { Layer, ConfigProvider } from \"effect\";\n\n/**\n * Creates a test layer with a given set of environment variables.\n * Useful for testing code that depends on environment variables.\n *\n * @param envMap - A Map or Record of environment variable names to their values\n * @returns A Layer that provides the environment variables to the Effect runtime\n *\n * @example\n * const testLayer = createTestEnvLayer(new Map([\n * [\"DATABASE_URL\", \"postgresql://user:password@localhost:5432/db\"],\n * [\"NODE_ENV\", \"test\"],\n * ]));\n */\nexport function createTestEnvLayer(\n envMap: Map<string, string> | Record<string, string>,\n): Layer.Layer<never> {\n const map = envMap instanceof Map ? envMap : new Map(Object.entries(envMap));\n return Layer.setConfigProvider(ConfigProvider.fromMap(map));\n}\n"],"mappings":"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","names":["Config","Config"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@emergente-labs/effect-env",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Type-safe Effect-TS environment variable configuration with development fallbacks",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Emergente Labs",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/emergente-labs/effect-env"
|
|
11
|
+
},
|
|
12
|
+
"bugs": "https://github.com/emergente-labs/effect-env/issues",
|
|
13
|
+
"homepage": "https://github.com/emergente-labs/effect-env#readme",
|
|
14
|
+
"keywords": ["effect", "effect-ts", "environment", "config", "env", "type-safe", "typed-env"],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18.0.0"
|
|
17
|
+
},
|
|
18
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"prepare": "pnpm build",
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test:watch": "vitest",
|
|
31
|
+
"typecheck": "tsc --noEmit",
|
|
32
|
+
"prepublishOnly": "pnpm run build && pnpm run test"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"effect": "^3.19.8"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@effect/vitest": "^0.27.0",
|
|
39
|
+
"tsup": "^8.4.0",
|
|
40
|
+
"typescript": "^5.9.3",
|
|
41
|
+
"vitest": "^4.0.16"
|
|
42
|
+
}
|
|
43
|
+
}
|