@lacspace/env 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lacspace
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/dist/index.cjs ADDED
@@ -0,0 +1,148 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ var EnvError = class extends Error {
5
+ constructor(message, issues) {
6
+ super(message);
7
+ this.issues = issues;
8
+ this.name = "EnvError";
9
+ }
10
+ };
11
+ function fail(key, msg) {
12
+ throw new Error(`"${key}" ${msg}`);
13
+ }
14
+ function withDefault(raw, key, opts, cast) {
15
+ if (raw === void 0 || raw === "") {
16
+ if (opts && "default" in opts && opts.default !== void 0) return opts.default;
17
+ if (opts?.optional) return void 0;
18
+ fail(key, "is required but was not set");
19
+ }
20
+ return cast(raw);
21
+ }
22
+ function str(opts) {
23
+ return {
24
+ parse(raw, key) {
25
+ if (opts?.allowEmpty && raw === "") return "";
26
+ return withDefault(raw, key, opts, (v) => v);
27
+ }
28
+ };
29
+ }
30
+ function num(opts) {
31
+ return {
32
+ parse(raw, key) {
33
+ return withDefault(raw, key, opts, (v) => {
34
+ const n = Number(v);
35
+ if (!Number.isFinite(n)) fail(key, `must be a number, got "${v}"`);
36
+ if (opts?.min !== void 0 && n < opts.min) fail(key, `must be >= ${opts.min}`);
37
+ if (opts?.max !== void 0 && n > opts.max) fail(key, `must be <= ${opts.max}`);
38
+ return n;
39
+ });
40
+ }
41
+ };
42
+ }
43
+ function int(opts) {
44
+ const base = num(opts);
45
+ return {
46
+ parse(raw, key) {
47
+ const n = base.parse(raw, key);
48
+ if (n !== void 0 && !Number.isInteger(n)) fail(key, `must be an integer, got "${raw}"`);
49
+ return n;
50
+ }
51
+ };
52
+ }
53
+ function port(opts) {
54
+ return int({ ...opts, min: 1, max: 65535 });
55
+ }
56
+ function bool(opts) {
57
+ return {
58
+ parse(raw, key) {
59
+ return withDefault(raw, key, opts, (v) => {
60
+ const s = v.trim().toLowerCase();
61
+ if (["true", "1", "yes", "on"].includes(s)) return true;
62
+ if (["false", "0", "no", "off"].includes(s)) return false;
63
+ fail(key, `must be a boolean, got "${v}"`);
64
+ });
65
+ }
66
+ };
67
+ }
68
+ function url(opts) {
69
+ return {
70
+ parse(raw, key) {
71
+ return withDefault(raw, key, opts, (v) => {
72
+ try {
73
+ new URL(v);
74
+ } catch {
75
+ fail(key, `must be a valid URL, got "${v}"`);
76
+ }
77
+ return v;
78
+ });
79
+ }
80
+ };
81
+ }
82
+ function email(opts) {
83
+ return {
84
+ parse(raw, key) {
85
+ return withDefault(raw, key, opts, (v) => {
86
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) fail(key, `must be an email, got "${v}"`);
87
+ return v;
88
+ });
89
+ }
90
+ };
91
+ }
92
+ function oneOf(values, opts) {
93
+ return {
94
+ parse(raw, key) {
95
+ return withDefault(raw, key, opts, (v) => {
96
+ if (!values.includes(v)) fail(key, `must be one of ${values.join(", ")}, got "${v}"`);
97
+ return v;
98
+ });
99
+ }
100
+ };
101
+ }
102
+ function json(opts) {
103
+ return {
104
+ parse(raw, key) {
105
+ return withDefault(raw, key, opts, (v) => {
106
+ try {
107
+ return JSON.parse(v);
108
+ } catch {
109
+ fail(key, "must be valid JSON");
110
+ }
111
+ });
112
+ }
113
+ };
114
+ }
115
+ var defaultSource = typeof process !== "undefined" && process.env ? process.env : {};
116
+ function createEnv(schema, source = defaultSource) {
117
+ const out = {};
118
+ const issues = [];
119
+ for (const key of Object.keys(schema)) {
120
+ try {
121
+ out[key] = schema[key].parse(source[key], key);
122
+ } catch (e) {
123
+ issues.push(` \u2022 ${e.message}`);
124
+ }
125
+ }
126
+ if (issues.length) {
127
+ throw new EnvError(
128
+ `Invalid environment variables:
129
+ ${issues.join("\n")}`,
130
+ issues.map((i) => i.trim().replace(/^• /, ""))
131
+ );
132
+ }
133
+ return Object.freeze(out);
134
+ }
135
+
136
+ exports.EnvError = EnvError;
137
+ exports.bool = bool;
138
+ exports.createEnv = createEnv;
139
+ exports.email = email;
140
+ exports.int = int;
141
+ exports.json = json;
142
+ exports.num = num;
143
+ exports.oneOf = oneOf;
144
+ exports.port = port;
145
+ exports.str = str;
146
+ exports.url = url;
147
+ //# sourceMappingURL=index.cjs.map
148
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lacspace/env\n * Typed, validated environment variables — fail fast at boot.\n *\n * Declare a schema, validate `process.env` once at startup, and get a typed,\n * frozen object back. Missing or malformed variables throw a single, clear\n * error listing everything that's wrong — before your app serves traffic.\n *\n * Zero dependencies · isomorphic · fully typed.\n */\n\nexport interface Validator<T> {\n parse(raw: string | undefined, key: string): T;\n}\n\nexport class EnvError extends Error {\n constructor(\n message: string,\n public issues: string[],\n ) {\n super(message);\n this.name = \"EnvError\";\n }\n}\n\nfunction fail(key: string, msg: string): never {\n throw new Error(`\"${key}\" ${msg}`);\n}\n\ninterface BaseOpts<T> {\n default?: T;\n optional?: boolean;\n}\n\nfunction withDefault<T>(\n raw: string | undefined,\n key: string,\n opts: BaseOpts<T> | undefined,\n cast: (v: string) => T,\n): T {\n if (raw === undefined || raw === \"\") {\n if (opts && \"default\" in opts && opts.default !== undefined) return opts.default;\n if (opts?.optional) return undefined as unknown as T;\n fail(key, \"is required but was not set\");\n }\n return cast(raw);\n}\n\n/** A string. `allowEmpty` keeps \"\"; otherwise empty is treated as missing. */\nexport function str(opts?: BaseOpts<string> & { allowEmpty?: boolean }): Validator<string> {\n return {\n parse(raw, key) {\n if (opts?.allowEmpty && raw === \"\") return \"\";\n return withDefault(raw, key, opts, (v) => v);\n },\n };\n}\n\n/** A number (integer or float). */\nexport function num(opts?: BaseOpts<number> & { min?: number; max?: number }): Validator<number> {\n return {\n parse(raw, key) {\n return withDefault(raw, key, opts, (v) => {\n const n = Number(v);\n if (!Number.isFinite(n)) fail(key, `must be a number, got \"${v}\"`);\n if (opts?.min !== undefined && n < opts.min) fail(key, `must be >= ${opts.min}`);\n if (opts?.max !== undefined && n > opts.max) fail(key, `must be <= ${opts.max}`);\n return n;\n });\n },\n };\n}\n\n/** An integer. */\nexport function int(opts?: BaseOpts<number> & { min?: number; max?: number }): Validator<number> {\n const base = num(opts);\n return {\n parse(raw, key) {\n const n = base.parse(raw, key);\n if (n !== undefined && !Number.isInteger(n)) fail(key, `must be an integer, got \"${raw}\"`);\n return n;\n },\n };\n}\n\n/** A TCP port (1–65535). */\nexport function port(opts?: BaseOpts<number>): Validator<number> {\n return int({ ...opts, min: 1, max: 65535 });\n}\n\n/** A boolean. Accepts true/1/yes/on and false/0/no/off (case-insensitive). */\nexport function bool(opts?: BaseOpts<boolean>): Validator<boolean> {\n return {\n parse(raw, key) {\n return withDefault(raw, key, opts, (v) => {\n const s = v.trim().toLowerCase();\n if ([\"true\", \"1\", \"yes\", \"on\"].includes(s)) return true;\n if ([\"false\", \"0\", \"no\", \"off\"].includes(s)) return false;\n fail(key, `must be a boolean, got \"${v}\"`);\n });\n },\n };\n}\n\n/** A valid URL string. */\nexport function url(opts?: BaseOpts<string>): Validator<string> {\n return {\n parse(raw, key) {\n return withDefault(raw, key, opts, (v) => {\n try {\n new URL(v);\n } catch {\n fail(key, `must be a valid URL, got \"${v}\"`);\n }\n return v;\n });\n },\n };\n}\n\n/** A plausible email address. */\nexport function email(opts?: BaseOpts<string>): Validator<string> {\n return {\n parse(raw, key) {\n return withDefault(raw, key, opts, (v) => {\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(v)) fail(key, `must be an email, got \"${v}\"`);\n return v;\n });\n },\n };\n}\n\n/** One of a fixed set of string values. */\nexport function oneOf<const T extends string>(\n values: readonly T[],\n opts?: BaseOpts<T>,\n): Validator<T> {\n return {\n parse(raw, key) {\n return withDefault(raw, key, opts, (v) => {\n if (!values.includes(v as T)) fail(key, `must be one of ${values.join(\", \")}, got \"${v}\"`);\n return v as T;\n });\n },\n };\n}\n\n/** JSON-parsed value. */\nexport function json<T = unknown>(opts?: BaseOpts<T>): Validator<T> {\n return {\n parse(raw, key) {\n return withDefault(raw, key, opts, (v) => {\n try {\n return JSON.parse(v) as T;\n } catch {\n fail(key, \"must be valid JSON\");\n }\n });\n },\n };\n}\n\nexport type InferEnv<S extends Record<string, Validator<unknown>>> = {\n readonly [K in keyof S]: S[K] extends Validator<infer T> ? T : never;\n};\n\nconst defaultSource: Record<string, string | undefined> =\n typeof process !== \"undefined\" && process.env ? process.env : {};\n\n/**\n * Validate a schema against a source (defaults to `process.env`) and return a\n * typed, frozen object. Throws a single {@link EnvError} listing every problem.\n * @example\n * export const env = createEnv({\n * NODE_ENV: oneOf([\"development\", \"production\", \"test\"], { default: \"development\" }),\n * PORT: port({ default: 3000 }),\n * DATABASE_URL: url(),\n * DEBUG: bool({ default: false }),\n * });\n */\nexport function createEnv<S extends Record<string, Validator<unknown>>>(\n schema: S,\n source: Record<string, string | undefined> = defaultSource,\n): InferEnv<S> {\n const out: Record<string, unknown> = {};\n const issues: string[] = [];\n for (const key of Object.keys(schema)) {\n try {\n out[key] = schema[key]!.parse(source[key], key);\n } catch (e) {\n issues.push(` • ${(e as Error).message}`);\n }\n }\n if (issues.length) {\n throw new EnvError(\n `Invalid environment variables:\\n${issues.join(\"\\n\")}`,\n issues.map((i) => i.trim().replace(/^• /, \"\")),\n );\n }\n return Object.freeze(out) as InferEnv<S>;\n}\n"]}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @lacspace/env
3
+ * Typed, validated environment variables — fail fast at boot.
4
+ *
5
+ * Declare a schema, validate `process.env` once at startup, and get a typed,
6
+ * frozen object back. Missing or malformed variables throw a single, clear
7
+ * error listing everything that's wrong — before your app serves traffic.
8
+ *
9
+ * Zero dependencies · isomorphic · fully typed.
10
+ */
11
+ interface Validator<T> {
12
+ parse(raw: string | undefined, key: string): T;
13
+ }
14
+ declare class EnvError extends Error {
15
+ issues: string[];
16
+ constructor(message: string, issues: string[]);
17
+ }
18
+ interface BaseOpts<T> {
19
+ default?: T;
20
+ optional?: boolean;
21
+ }
22
+ /** A string. `allowEmpty` keeps ""; otherwise empty is treated as missing. */
23
+ declare function str(opts?: BaseOpts<string> & {
24
+ allowEmpty?: boolean;
25
+ }): Validator<string>;
26
+ /** A number (integer or float). */
27
+ declare function num(opts?: BaseOpts<number> & {
28
+ min?: number;
29
+ max?: number;
30
+ }): Validator<number>;
31
+ /** An integer. */
32
+ declare function int(opts?: BaseOpts<number> & {
33
+ min?: number;
34
+ max?: number;
35
+ }): Validator<number>;
36
+ /** A TCP port (1–65535). */
37
+ declare function port(opts?: BaseOpts<number>): Validator<number>;
38
+ /** A boolean. Accepts true/1/yes/on and false/0/no/off (case-insensitive). */
39
+ declare function bool(opts?: BaseOpts<boolean>): Validator<boolean>;
40
+ /** A valid URL string. */
41
+ declare function url(opts?: BaseOpts<string>): Validator<string>;
42
+ /** A plausible email address. */
43
+ declare function email(opts?: BaseOpts<string>): Validator<string>;
44
+ /** One of a fixed set of string values. */
45
+ declare function oneOf<const T extends string>(values: readonly T[], opts?: BaseOpts<T>): Validator<T>;
46
+ /** JSON-parsed value. */
47
+ declare function json<T = unknown>(opts?: BaseOpts<T>): Validator<T>;
48
+ type InferEnv<S extends Record<string, Validator<unknown>>> = {
49
+ readonly [K in keyof S]: S[K] extends Validator<infer T> ? T : never;
50
+ };
51
+ /**
52
+ * Validate a schema against a source (defaults to `process.env`) and return a
53
+ * typed, frozen object. Throws a single {@link EnvError} listing every problem.
54
+ * @example
55
+ * export const env = createEnv({
56
+ * NODE_ENV: oneOf(["development", "production", "test"], { default: "development" }),
57
+ * PORT: port({ default: 3000 }),
58
+ * DATABASE_URL: url(),
59
+ * DEBUG: bool({ default: false }),
60
+ * });
61
+ */
62
+ declare function createEnv<S extends Record<string, Validator<unknown>>>(schema: S, source?: Record<string, string | undefined>): InferEnv<S>;
63
+
64
+ export { EnvError, type InferEnv, type Validator, bool, createEnv, email, int, json, num, oneOf, port, str, url };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @lacspace/env
3
+ * Typed, validated environment variables — fail fast at boot.
4
+ *
5
+ * Declare a schema, validate `process.env` once at startup, and get a typed,
6
+ * frozen object back. Missing or malformed variables throw a single, clear
7
+ * error listing everything that's wrong — before your app serves traffic.
8
+ *
9
+ * Zero dependencies · isomorphic · fully typed.
10
+ */
11
+ interface Validator<T> {
12
+ parse(raw: string | undefined, key: string): T;
13
+ }
14
+ declare class EnvError extends Error {
15
+ issues: string[];
16
+ constructor(message: string, issues: string[]);
17
+ }
18
+ interface BaseOpts<T> {
19
+ default?: T;
20
+ optional?: boolean;
21
+ }
22
+ /** A string. `allowEmpty` keeps ""; otherwise empty is treated as missing. */
23
+ declare function str(opts?: BaseOpts<string> & {
24
+ allowEmpty?: boolean;
25
+ }): Validator<string>;
26
+ /** A number (integer or float). */
27
+ declare function num(opts?: BaseOpts<number> & {
28
+ min?: number;
29
+ max?: number;
30
+ }): Validator<number>;
31
+ /** An integer. */
32
+ declare function int(opts?: BaseOpts<number> & {
33
+ min?: number;
34
+ max?: number;
35
+ }): Validator<number>;
36
+ /** A TCP port (1–65535). */
37
+ declare function port(opts?: BaseOpts<number>): Validator<number>;
38
+ /** A boolean. Accepts true/1/yes/on and false/0/no/off (case-insensitive). */
39
+ declare function bool(opts?: BaseOpts<boolean>): Validator<boolean>;
40
+ /** A valid URL string. */
41
+ declare function url(opts?: BaseOpts<string>): Validator<string>;
42
+ /** A plausible email address. */
43
+ declare function email(opts?: BaseOpts<string>): Validator<string>;
44
+ /** One of a fixed set of string values. */
45
+ declare function oneOf<const T extends string>(values: readonly T[], opts?: BaseOpts<T>): Validator<T>;
46
+ /** JSON-parsed value. */
47
+ declare function json<T = unknown>(opts?: BaseOpts<T>): Validator<T>;
48
+ type InferEnv<S extends Record<string, Validator<unknown>>> = {
49
+ readonly [K in keyof S]: S[K] extends Validator<infer T> ? T : never;
50
+ };
51
+ /**
52
+ * Validate a schema against a source (defaults to `process.env`) and return a
53
+ * typed, frozen object. Throws a single {@link EnvError} listing every problem.
54
+ * @example
55
+ * export const env = createEnv({
56
+ * NODE_ENV: oneOf(["development", "production", "test"], { default: "development" }),
57
+ * PORT: port({ default: 3000 }),
58
+ * DATABASE_URL: url(),
59
+ * DEBUG: bool({ default: false }),
60
+ * });
61
+ */
62
+ declare function createEnv<S extends Record<string, Validator<unknown>>>(schema: S, source?: Record<string, string | undefined>): InferEnv<S>;
63
+
64
+ export { EnvError, type InferEnv, type Validator, bool, createEnv, email, int, json, num, oneOf, port, str, url };
package/dist/index.js ADDED
@@ -0,0 +1,136 @@
1
+ // src/index.ts
2
+ var EnvError = class extends Error {
3
+ constructor(message, issues) {
4
+ super(message);
5
+ this.issues = issues;
6
+ this.name = "EnvError";
7
+ }
8
+ };
9
+ function fail(key, msg) {
10
+ throw new Error(`"${key}" ${msg}`);
11
+ }
12
+ function withDefault(raw, key, opts, cast) {
13
+ if (raw === void 0 || raw === "") {
14
+ if (opts && "default" in opts && opts.default !== void 0) return opts.default;
15
+ if (opts?.optional) return void 0;
16
+ fail(key, "is required but was not set");
17
+ }
18
+ return cast(raw);
19
+ }
20
+ function str(opts) {
21
+ return {
22
+ parse(raw, key) {
23
+ if (opts?.allowEmpty && raw === "") return "";
24
+ return withDefault(raw, key, opts, (v) => v);
25
+ }
26
+ };
27
+ }
28
+ function num(opts) {
29
+ return {
30
+ parse(raw, key) {
31
+ return withDefault(raw, key, opts, (v) => {
32
+ const n = Number(v);
33
+ if (!Number.isFinite(n)) fail(key, `must be a number, got "${v}"`);
34
+ if (opts?.min !== void 0 && n < opts.min) fail(key, `must be >= ${opts.min}`);
35
+ if (opts?.max !== void 0 && n > opts.max) fail(key, `must be <= ${opts.max}`);
36
+ return n;
37
+ });
38
+ }
39
+ };
40
+ }
41
+ function int(opts) {
42
+ const base = num(opts);
43
+ return {
44
+ parse(raw, key) {
45
+ const n = base.parse(raw, key);
46
+ if (n !== void 0 && !Number.isInteger(n)) fail(key, `must be an integer, got "${raw}"`);
47
+ return n;
48
+ }
49
+ };
50
+ }
51
+ function port(opts) {
52
+ return int({ ...opts, min: 1, max: 65535 });
53
+ }
54
+ function bool(opts) {
55
+ return {
56
+ parse(raw, key) {
57
+ return withDefault(raw, key, opts, (v) => {
58
+ const s = v.trim().toLowerCase();
59
+ if (["true", "1", "yes", "on"].includes(s)) return true;
60
+ if (["false", "0", "no", "off"].includes(s)) return false;
61
+ fail(key, `must be a boolean, got "${v}"`);
62
+ });
63
+ }
64
+ };
65
+ }
66
+ function url(opts) {
67
+ return {
68
+ parse(raw, key) {
69
+ return withDefault(raw, key, opts, (v) => {
70
+ try {
71
+ new URL(v);
72
+ } catch {
73
+ fail(key, `must be a valid URL, got "${v}"`);
74
+ }
75
+ return v;
76
+ });
77
+ }
78
+ };
79
+ }
80
+ function email(opts) {
81
+ return {
82
+ parse(raw, key) {
83
+ return withDefault(raw, key, opts, (v) => {
84
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) fail(key, `must be an email, got "${v}"`);
85
+ return v;
86
+ });
87
+ }
88
+ };
89
+ }
90
+ function oneOf(values, opts) {
91
+ return {
92
+ parse(raw, key) {
93
+ return withDefault(raw, key, opts, (v) => {
94
+ if (!values.includes(v)) fail(key, `must be one of ${values.join(", ")}, got "${v}"`);
95
+ return v;
96
+ });
97
+ }
98
+ };
99
+ }
100
+ function json(opts) {
101
+ return {
102
+ parse(raw, key) {
103
+ return withDefault(raw, key, opts, (v) => {
104
+ try {
105
+ return JSON.parse(v);
106
+ } catch {
107
+ fail(key, "must be valid JSON");
108
+ }
109
+ });
110
+ }
111
+ };
112
+ }
113
+ var defaultSource = typeof process !== "undefined" && process.env ? process.env : {};
114
+ function createEnv(schema, source = defaultSource) {
115
+ const out = {};
116
+ const issues = [];
117
+ for (const key of Object.keys(schema)) {
118
+ try {
119
+ out[key] = schema[key].parse(source[key], key);
120
+ } catch (e) {
121
+ issues.push(` \u2022 ${e.message}`);
122
+ }
123
+ }
124
+ if (issues.length) {
125
+ throw new EnvError(
126
+ `Invalid environment variables:
127
+ ${issues.join("\n")}`,
128
+ issues.map((i) => i.trim().replace(/^• /, ""))
129
+ );
130
+ }
131
+ return Object.freeze(out);
132
+ }
133
+
134
+ export { EnvError, bool, createEnv, email, int, json, num, oneOf, port, str, url };
135
+ //# sourceMappingURL=index.js.map
136
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @lacspace/env\n * Typed, validated environment variables — fail fast at boot.\n *\n * Declare a schema, validate `process.env` once at startup, and get a typed,\n * frozen object back. Missing or malformed variables throw a single, clear\n * error listing everything that's wrong — before your app serves traffic.\n *\n * Zero dependencies · isomorphic · fully typed.\n */\n\nexport interface Validator<T> {\n parse(raw: string | undefined, key: string): T;\n}\n\nexport class EnvError extends Error {\n constructor(\n message: string,\n public issues: string[],\n ) {\n super(message);\n this.name = \"EnvError\";\n }\n}\n\nfunction fail(key: string, msg: string): never {\n throw new Error(`\"${key}\" ${msg}`);\n}\n\ninterface BaseOpts<T> {\n default?: T;\n optional?: boolean;\n}\n\nfunction withDefault<T>(\n raw: string | undefined,\n key: string,\n opts: BaseOpts<T> | undefined,\n cast: (v: string) => T,\n): T {\n if (raw === undefined || raw === \"\") {\n if (opts && \"default\" in opts && opts.default !== undefined) return opts.default;\n if (opts?.optional) return undefined as unknown as T;\n fail(key, \"is required but was not set\");\n }\n return cast(raw);\n}\n\n/** A string. `allowEmpty` keeps \"\"; otherwise empty is treated as missing. */\nexport function str(opts?: BaseOpts<string> & { allowEmpty?: boolean }): Validator<string> {\n return {\n parse(raw, key) {\n if (opts?.allowEmpty && raw === \"\") return \"\";\n return withDefault(raw, key, opts, (v) => v);\n },\n };\n}\n\n/** A number (integer or float). */\nexport function num(opts?: BaseOpts<number> & { min?: number; max?: number }): Validator<number> {\n return {\n parse(raw, key) {\n return withDefault(raw, key, opts, (v) => {\n const n = Number(v);\n if (!Number.isFinite(n)) fail(key, `must be a number, got \"${v}\"`);\n if (opts?.min !== undefined && n < opts.min) fail(key, `must be >= ${opts.min}`);\n if (opts?.max !== undefined && n > opts.max) fail(key, `must be <= ${opts.max}`);\n return n;\n });\n },\n };\n}\n\n/** An integer. */\nexport function int(opts?: BaseOpts<number> & { min?: number; max?: number }): Validator<number> {\n const base = num(opts);\n return {\n parse(raw, key) {\n const n = base.parse(raw, key);\n if (n !== undefined && !Number.isInteger(n)) fail(key, `must be an integer, got \"${raw}\"`);\n return n;\n },\n };\n}\n\n/** A TCP port (1–65535). */\nexport function port(opts?: BaseOpts<number>): Validator<number> {\n return int({ ...opts, min: 1, max: 65535 });\n}\n\n/** A boolean. Accepts true/1/yes/on and false/0/no/off (case-insensitive). */\nexport function bool(opts?: BaseOpts<boolean>): Validator<boolean> {\n return {\n parse(raw, key) {\n return withDefault(raw, key, opts, (v) => {\n const s = v.trim().toLowerCase();\n if ([\"true\", \"1\", \"yes\", \"on\"].includes(s)) return true;\n if ([\"false\", \"0\", \"no\", \"off\"].includes(s)) return false;\n fail(key, `must be a boolean, got \"${v}\"`);\n });\n },\n };\n}\n\n/** A valid URL string. */\nexport function url(opts?: BaseOpts<string>): Validator<string> {\n return {\n parse(raw, key) {\n return withDefault(raw, key, opts, (v) => {\n try {\n new URL(v);\n } catch {\n fail(key, `must be a valid URL, got \"${v}\"`);\n }\n return v;\n });\n },\n };\n}\n\n/** A plausible email address. */\nexport function email(opts?: BaseOpts<string>): Validator<string> {\n return {\n parse(raw, key) {\n return withDefault(raw, key, opts, (v) => {\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(v)) fail(key, `must be an email, got \"${v}\"`);\n return v;\n });\n },\n };\n}\n\n/** One of a fixed set of string values. */\nexport function oneOf<const T extends string>(\n values: readonly T[],\n opts?: BaseOpts<T>,\n): Validator<T> {\n return {\n parse(raw, key) {\n return withDefault(raw, key, opts, (v) => {\n if (!values.includes(v as T)) fail(key, `must be one of ${values.join(\", \")}, got \"${v}\"`);\n return v as T;\n });\n },\n };\n}\n\n/** JSON-parsed value. */\nexport function json<T = unknown>(opts?: BaseOpts<T>): Validator<T> {\n return {\n parse(raw, key) {\n return withDefault(raw, key, opts, (v) => {\n try {\n return JSON.parse(v) as T;\n } catch {\n fail(key, \"must be valid JSON\");\n }\n });\n },\n };\n}\n\nexport type InferEnv<S extends Record<string, Validator<unknown>>> = {\n readonly [K in keyof S]: S[K] extends Validator<infer T> ? T : never;\n};\n\nconst defaultSource: Record<string, string | undefined> =\n typeof process !== \"undefined\" && process.env ? process.env : {};\n\n/**\n * Validate a schema against a source (defaults to `process.env`) and return a\n * typed, frozen object. Throws a single {@link EnvError} listing every problem.\n * @example\n * export const env = createEnv({\n * NODE_ENV: oneOf([\"development\", \"production\", \"test\"], { default: \"development\" }),\n * PORT: port({ default: 3000 }),\n * DATABASE_URL: url(),\n * DEBUG: bool({ default: false }),\n * });\n */\nexport function createEnv<S extends Record<string, Validator<unknown>>>(\n schema: S,\n source: Record<string, string | undefined> = defaultSource,\n): InferEnv<S> {\n const out: Record<string, unknown> = {};\n const issues: string[] = [];\n for (const key of Object.keys(schema)) {\n try {\n out[key] = schema[key]!.parse(source[key], key);\n } catch (e) {\n issues.push(` • ${(e as Error).message}`);\n }\n }\n if (issues.length) {\n throw new EnvError(\n `Invalid environment variables:\\n${issues.join(\"\\n\")}`,\n issues.map((i) => i.trim().replace(/^• /, \"\")),\n );\n }\n return Object.freeze(out) as InferEnv<S>;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@lacspace/env",
3
+ "version": "1.0.0",
4
+ "description": "Typed, validated environment variables — declare a schema, validate process.env at boot, get a typed frozen object or a clear fail-fast error. A zero-dependency t3-env / envalid alternative.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "sideEffects": false,
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "keywords": [
30
+ "env",
31
+ "environment-variables",
32
+ "dotenv",
33
+ "env-validation",
34
+ "t3-env",
35
+ "envalid",
36
+ "config",
37
+ "type-safe",
38
+ "typescript"
39
+ ],
40
+ "author": "Lacspace <contact@lacspace.com>",
41
+ "license": "MIT",
42
+ "homepage": "https://lacspace.com/packages",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/lacspace/npm-packages.git",
46
+ "directory": "env"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/lacspace/npm-packages/issues"
50
+ },
51
+ "engines": {
52
+ "node": ">=18"
53
+ }
54
+ }