@arki/env 0.0.1 → 0.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/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/urls.d.ts +1 -1
- package/dist/contracts/urls.d.ts.map +1 -1
- package/dist/contracts/urls.js +1 -1
- package/dist/contracts/urls.js.map +1 -1
- package/dist/core/define-env.d.ts +48 -7
- package/dist/core/define-env.d.ts.map +1 -1
- package/dist/core/define-env.js +67 -9
- package/dist/core/define-env.js.map +1 -1
- package/dist/dot.d.ts +16 -18
- package/dist/dot.d.ts.map +1 -1
- package/dist/dot.js +17 -9
- package/dist/dot.js.map +1 -1
- package/package.json +8 -2
- package/src/contracts/common.ts +31 -0
- package/src/contracts/index.ts +17 -0
- package/src/contracts/parsing.ts +27 -0
- package/src/contracts/urls.ts +24 -0
- package/src/core/define-env.ts +134 -0
- package/src/core/index.ts +2 -0
- package/src/dot.ts +95 -0
- package/src/index.ts +2 -0
|
@@ -2,7 +2,7 @@ export declare const contracts: {
|
|
|
2
2
|
json: <T extends import("zod").ZodType>(schema: T) => import("zod").ZodPipe<import("zod").ZodString, import("zod").ZodTransform<Awaited<import("zod").infer<T>>, string>>;
|
|
3
3
|
csvArray: <T extends import("zod").ZodType>(itemSchema: T) => import("zod").ZodPipe<import("zod").ZodTransform<unknown, unknown>, import("zod").ZodArray<T>>;
|
|
4
4
|
csvSet: <T extends import("zod").ZodType>(itemSchema: T) => import("zod").ZodPipe<import("zod").ZodTransform<unknown, unknown>, import("zod").ZodArray<T>>;
|
|
5
|
-
url: (defaultValue?: string) => import("zod").
|
|
5
|
+
url: (defaultValue?: string) => import("zod").ZodURL | import("zod").ZodDefault<import("zod").ZodURL>;
|
|
6
6
|
databaseUrl: () => import("zod").ZodURL;
|
|
7
7
|
redisUrl: () => import("zod").ZodURL;
|
|
8
8
|
string: (defaultValue?: string) => import("zod").ZodString | import("zod").ZodDefault<import("zod").ZodString>;
|
package/dist/contracts/urls.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export declare const urlContracts: {
|
|
3
|
-
url: (defaultValue?: string) => z.
|
|
3
|
+
url: (defaultValue?: string) => z.ZodURL | z.ZodDefault<z.ZodURL>;
|
|
4
4
|
databaseUrl: () => z.ZodURL;
|
|
5
5
|
redisUrl: () => z.ZodURL;
|
|
6
6
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"urls.d.ts","sourceRoot":"","sources":["../../src/contracts/urls.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,YAAY;yBACF,MAAM;;;
|
|
1
|
+
{"version":3,"file":"urls.d.ts","sourceRoot":"","sources":["../../src/contracts/urls.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,YAAY;yBACF,MAAM;;;CAoB5B,CAAC"}
|
package/dist/contracts/urls.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"urls.js","sourceRoot":"","sources":["../../src/contracts/urls.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,GAAG,EAAE,CAAC,YAAqB,EAAE,EAAE;QAC7B,MAAM,MAAM,GAAG,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"urls.js","sourceRoot":"","sources":["../../src/contracts/urls.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,GAAG,EAAE,CAAC,YAAqB,EAAE,EAAE;QAC7B,MAAM,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;QACvB,OAAO,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAC9D,CAAC;IAED,WAAW,EAAE,GAAG,EAAE,CAChB,CAAC;SACE,GAAG,EAAE;SACL,MAAM,CACL,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,aAAa,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,eAAe,CAAC,EACvE,2DAA2D,CAC5D;IAEL,QAAQ,EAAE,GAAG,EAAE,CACb,CAAC;SACE,GAAG,EAAE;SACL,MAAM,CACL,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,WAAW,CAAC,EAChE,iDAAiD,CAClD;CACN,CAAC"}
|
|
@@ -1,13 +1,54 @@
|
|
|
1
|
+
import { createEnv } from '@t3-oss/env-core';
|
|
2
|
+
import type { CreateEnv, DefaultCombinedSchema } from '@t3-oss/env-core';
|
|
1
3
|
import type { ZodType } from 'zod';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
+
/** The issue shape t3-oss hands to `onValidationError`, derived so it stays in sync with the lib. */
|
|
5
|
+
type ValidationIssues = Parameters<NonNullable<Parameters<typeof createEnv>[0]['onValidationError']>>[0];
|
|
6
|
+
/**
|
|
7
|
+
* Thin, type-preserving wrapper over `@t3-oss/env-core`'s `createEnv`. Generic over the server,
|
|
8
|
+
* client, shared and composed schemas so the parsed env keeps its exact inferred types — the wrapper
|
|
9
|
+
* adds the ARKI defaults (auto `runtimeEnv`, prod-safe `skipValidation`, a legible labelled error
|
|
10
|
+
* handler) without widening any value to `unknown`.
|
|
11
|
+
*
|
|
12
|
+
* `TPrefix` is kept as a string-literal generic on purpose: `createEnv` uses it to enforce that
|
|
13
|
+
* client keys (and only client keys) carry the prefix. Widening it to `string` would make every
|
|
14
|
+
* server key collapse to a type-level `ErrorMessage`, so the prefix must stay a literal.
|
|
15
|
+
*/
|
|
16
|
+
export declare function defineEnv<TServer extends Record<string, ZodType>, TClient extends Record<string, ZodType> = NonNullable<unknown>, TShared extends Record<string, ZodType> = NonNullable<unknown>, const TPrefix extends string = '', const TExtends extends Array<Record<string, unknown>> = []>(config: {
|
|
17
|
+
/**
|
|
18
|
+
* Label shown in validation-error logs, e.g. `'@acme/backend'`. Without it a misconfigured
|
|
19
|
+
* deploy logs a bare "Invalid environment variables" with no hint which env failed — exactly the
|
|
20
|
+
* blind failure this wrapper exists to prevent.
|
|
21
|
+
*/
|
|
22
|
+
name?: string;
|
|
23
|
+
server: TServer;
|
|
24
|
+
/** Client-side schema. Every key must start with `clientPrefix` (enforced by `createEnv`). */
|
|
25
|
+
client?: TClient;
|
|
26
|
+
/** Prefix client keys must carry, e.g. `'VITE_'`. Defaults to `''` (no client vars). */
|
|
27
|
+
clientPrefix?: TPrefix;
|
|
28
|
+
/** Shared vars available to both client and server, unprefixed (e.g. `NODE_ENV`). */
|
|
29
|
+
shared?: TShared;
|
|
30
|
+
/**
|
|
31
|
+
* Already-built envs to compose in (e.g. `@arki/auth`, `@arki/db`). Their parsed variables merge
|
|
32
|
+
* into the result with full types; each was validated by its own `createEnv` at import time.
|
|
33
|
+
*/
|
|
34
|
+
extends?: TExtends;
|
|
35
|
+
/**
|
|
36
|
+
* Explicit runtime source. Omit it and `defineEnv` auto-builds one by reading `process.env` for
|
|
37
|
+
* every declared key — correct for any server-side env. Provide it only when the auto-build would
|
|
38
|
+
* be wrong: Vite/`start` apps whose client vars come from `import.meta.env` in the browser, or
|
|
39
|
+
* keys with a fallback/alias (e.g. `DB_URL: process.env.DB_URL ?? process.env.DATABASE_URL`).
|
|
40
|
+
*/
|
|
41
|
+
runtimeEnv?: Record<string, string | boolean | number | undefined>;
|
|
4
42
|
options?: {
|
|
5
43
|
skipValidation?: boolean;
|
|
6
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Treat empty-string env values as `undefined` so defaults apply (e.g. `PORT=` in `.env`, or a
|
|
46
|
+
* Docker arg baked as `X=""`). Defaults to `true` — set `false` only if an app intentionally
|
|
47
|
+
* accepts `""` as a valid required string.
|
|
48
|
+
*/
|
|
49
|
+
emptyStringAsUndefined?: boolean;
|
|
50
|
+
onValidationError?: (issues: ValidationIssues) => never;
|
|
7
51
|
};
|
|
8
|
-
}
|
|
9
|
-
export declare function defineEnv(config: CoreEnvConfig): Readonly<{
|
|
10
|
-
[x: string]: unknown;
|
|
11
|
-
}>;
|
|
52
|
+
}): CreateEnv<DefaultCombinedSchema<TServer, TClient, TShared>, TExtends>;
|
|
12
53
|
export {};
|
|
13
54
|
//# sourceMappingURL=define-env.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define-env.d.ts","sourceRoot":"","sources":["../../src/core/define-env.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"define-env.d.ts","sourceRoot":"","sources":["../../src/core/define-env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAE,SAAS,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAEnC,qGAAqG;AACrG,KAAK,gBAAgB,GAAG,UAAU,CAChC,WAAW,CAAC,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAClE,CAAC,CAAC,CAAC,CAAC;AAkBL;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CACvB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,EAC9D,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,EAC9D,KAAK,CAAC,OAAO,SAAS,MAAM,GAAG,EAAE,EACjC,KAAK,CAAC,QAAQ,SAAS,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,EAAE,EAC1D,MAAM,EAAE;IACR;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,8FAA8F;IAC9F,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,wFAAwF;IACxF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,qFAAqF;IACrF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,QAAQ,CAAC;IACnB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;IACnE,OAAO,CAAC,EAAE;QACR,cAAc,CAAC,EAAE,OAAO,CAAC;QACzB;;;;WAIG;QACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;QACjC,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,KAAK,CAAC;KACzD,CAAC;CACH,GAAG,SAAS,CAAC,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,CAwDxE"}
|
package/dist/core/define-env.js
CHANGED
|
@@ -1,18 +1,76 @@
|
|
|
1
1
|
import { createEnv } from '@t3-oss/env-core';
|
|
2
|
+
/**
|
|
3
|
+
* Fold validation issues into a human-legible, log-pipeline-safe string. The t3-oss default handler
|
|
4
|
+
* logs `issues` as a raw object, which platforms like Coolify collapse to `[Object]` — so the var
|
|
5
|
+
* that actually failed never reaches the logs. A flat ` path: message` list survives transport.
|
|
6
|
+
*/
|
|
7
|
+
function formatIssues(name, issues) {
|
|
8
|
+
const label = name ? `[${name}] ` : '';
|
|
9
|
+
const lines = issues.map(issue => {
|
|
10
|
+
const path = (issue.path ?? [])
|
|
11
|
+
.map(seg => (typeof seg === 'object' && seg !== null && 'key' in seg ? String(seg.key) : String(seg)))
|
|
12
|
+
.join('.');
|
|
13
|
+
return ` ${path || '(root)'}: ${issue.message}`;
|
|
14
|
+
});
|
|
15
|
+
return `❌ ${label}Invalid environment variables:\n${lines.join('\n')}`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Thin, type-preserving wrapper over `@t3-oss/env-core`'s `createEnv`. Generic over the server,
|
|
19
|
+
* client, shared and composed schemas so the parsed env keeps its exact inferred types — the wrapper
|
|
20
|
+
* adds the ARKI defaults (auto `runtimeEnv`, prod-safe `skipValidation`, a legible labelled error
|
|
21
|
+
* handler) without widening any value to `unknown`.
|
|
22
|
+
*
|
|
23
|
+
* `TPrefix` is kept as a string-literal generic on purpose: `createEnv` uses it to enforce that
|
|
24
|
+
* client keys (and only client keys) carry the prefix. Widening it to `string` would make every
|
|
25
|
+
* server key collapse to a type-level `ErrorMessage`, so the prefix must stay a literal.
|
|
26
|
+
*/
|
|
2
27
|
export function defineEnv(config) {
|
|
3
28
|
const skipValidation = config.options?.skipValidation ?? (!!process.env.CI || process.env.NODE_ENV !== 'development');
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
29
|
+
// Use the caller's explicit runtimeEnv when supplied (Vite client vars via import.meta.env, or
|
|
30
|
+
// fallback aliases). Otherwise auto-build from every declared key (server + client + shared);
|
|
31
|
+
// createEnv tolerates keys absent from process.env (they parse as undefined → optional/default).
|
|
32
|
+
let runtimeEnv;
|
|
33
|
+
if (config.runtimeEnv) {
|
|
34
|
+
runtimeEnv = config.runtimeEnv;
|
|
8
35
|
}
|
|
9
|
-
|
|
36
|
+
else {
|
|
37
|
+
runtimeEnv = {};
|
|
38
|
+
for (const key of Object.keys(config.server)) {
|
|
39
|
+
runtimeEnv[key] = process.env[key];
|
|
40
|
+
}
|
|
41
|
+
for (const key of Object.keys(config.client ?? {})) {
|
|
42
|
+
runtimeEnv[key] = process.env[key];
|
|
43
|
+
}
|
|
44
|
+
for (const key of Object.keys(config.shared ?? {})) {
|
|
45
|
+
runtimeEnv[key] = process.env[key];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const onValidationError = config.options?.onValidationError ??
|
|
49
|
+
((issues) => {
|
|
50
|
+
const message = formatIssues(config.name, issues);
|
|
51
|
+
console.error(message);
|
|
52
|
+
throw new Error(message);
|
|
53
|
+
});
|
|
54
|
+
// `createEnv`'s `server`/`client` params are conditional on `clientPrefix` being a *concrete*
|
|
55
|
+
// string literal; inside a generic wrapper `TPrefix` is abstract, so the body cannot be verified
|
|
56
|
+
// for all prefixes and the call argument must be asserted once. The PUBLIC contract stays exact:
|
|
57
|
+
// the explicit return type above reconstructs `createEnv`'s own result from the generics, and the
|
|
58
|
+
// caller's `server`/`client`/`extends` are fully type-checked against `TServer`/`TClient`/`TExtends`
|
|
59
|
+
// at the call site. This is the only seam where types are asserted — value types are never widened.
|
|
60
|
+
const options = {
|
|
61
|
+
shared: config.shared ?? {},
|
|
62
|
+
extends: config.extends,
|
|
10
63
|
server: config.server,
|
|
11
|
-
client: {},
|
|
12
|
-
clientPrefix: '',
|
|
64
|
+
client: config.client ?? {},
|
|
65
|
+
clientPrefix: config.clientPrefix ?? '',
|
|
13
66
|
runtimeEnv,
|
|
14
67
|
skipValidation,
|
|
15
|
-
|
|
16
|
-
|
|
68
|
+
// Default ON: in containerized deploys an unset var is baked as "" (e.g. Docker `ENV X=${X}`
|
|
69
|
+
// with no build arg), not left absent. Treating "" as undefined lets `.default()`/`.optional()`
|
|
70
|
+
// apply as intended instead of failing format checks like `z.url()` on an empty string.
|
|
71
|
+
emptyStringAsUndefined: config.options?.emptyStringAsUndefined ?? true,
|
|
72
|
+
onValidationError,
|
|
73
|
+
};
|
|
74
|
+
return createEnv(options);
|
|
17
75
|
}
|
|
18
76
|
//# sourceMappingURL=define-env.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define-env.js","sourceRoot":"","sources":["../../src/core/define-env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"define-env.js","sourceRoot":"","sources":["../../src/core/define-env.ts"],"names":[],"mappings":"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"}
|
package/dist/dot.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DOT adapter for `@arki/env`.
|
|
3
3
|
*
|
|
4
|
-
* Wraps `defineEnv()` as a
|
|
4
|
+
* Wraps `defineEnv()` as a DOT pip so a DOT app can validate its
|
|
5
5
|
* environment alongside any other services. The pip is sync (env
|
|
6
6
|
* validation is sync), so the `boot` hook returns the validated env
|
|
7
|
-
* object synchronously through the standard
|
|
7
|
+
* object synchronously through the standard provides channel.
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
10
|
* ```ts
|
|
@@ -19,11 +19,21 @@
|
|
|
19
19
|
* console.log(app.services.env.PORT); // number, typed.
|
|
20
20
|
* ```
|
|
21
21
|
*
|
|
22
|
+
* To mount a second env scope in the same app, rename the published wire
|
|
23
|
+
* key at the mount site:
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { rename } from '@arki/dot';
|
|
27
|
+
*
|
|
28
|
+
* .use(env({ schema: appSchema }))
|
|
29
|
+
* .use(rename(env({ schema: publicSchema }), { env: 'publicEnv' }, 'public-env'))
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
22
32
|
* The `@arki/dot` package is an OPTIONAL peer of `@arki/env`. Importing
|
|
23
33
|
* this adapter without `@arki/dot` installed will fail at module load —
|
|
24
34
|
* that is intentional: the adapter only makes sense in a DOT app.
|
|
25
35
|
*/
|
|
26
|
-
import { type
|
|
36
|
+
import { type EmptyShape, type Pip } from '@arki/dot/pip';
|
|
27
37
|
import type { ZodType } from 'zod';
|
|
28
38
|
/**
|
|
29
39
|
* Options for the env DOT adapter.
|
|
@@ -34,11 +44,6 @@ import type { ZodType } from 'zod';
|
|
|
34
44
|
export type EnvDotOptions<TSchema extends Record<string, ZodType>> = {
|
|
35
45
|
/** Zod schemas for each env var the pip validates. */
|
|
36
46
|
readonly schema: TSchema;
|
|
37
|
-
/**
|
|
38
|
-
* Pip name override. Defaults to `'env'`. Use this only when
|
|
39
|
-
* composing multiple env scopes inside the same app (rare).
|
|
40
|
-
*/
|
|
41
|
-
readonly name?: string;
|
|
42
47
|
/**
|
|
43
48
|
* Forwarded to `defineEnv`. If `true`, the validator is skipped
|
|
44
49
|
* entirely. By default, validation is skipped when `CI` is set or
|
|
@@ -46,14 +51,7 @@ export type EnvDotOptions<TSchema extends Record<string, ZodType>> = {
|
|
|
46
51
|
*/
|
|
47
52
|
readonly skipValidation?: boolean;
|
|
48
53
|
};
|
|
49
|
-
/**
|
|
50
|
-
* The shape of services published by the env adapter.
|
|
51
|
-
*
|
|
52
|
-
* The key is configurable via `options.name` (defaults to `'env'`) so
|
|
53
|
-
* downstream pips can pick the right env when multiple env scopes
|
|
54
|
-
* exist. For the default single-env case, `app.services.env` is the
|
|
55
|
-
* validated record.
|
|
56
|
-
*/
|
|
54
|
+
/** The shape of services published by the env adapter. */
|
|
57
55
|
export type EnvServices<TSchema extends Record<string, ZodType>> = {
|
|
58
56
|
readonly env: {
|
|
59
57
|
readonly [K in keyof TSchema]: TSchema[K] extends ZodType<infer U> ? U : never;
|
|
@@ -63,7 +61,7 @@ export type EnvServices<TSchema extends Record<string, ZodType>> = {
|
|
|
63
61
|
* Build a DOT pip that validates and publishes a typed `env` service.
|
|
64
62
|
*
|
|
65
63
|
* @param options - Schema + optional config.
|
|
66
|
-
* @returns A
|
|
64
|
+
* @returns A pip that publishes `services.env`.
|
|
67
65
|
*/
|
|
68
|
-
export declare function env<TSchema extends Record<string, ZodType>>(options: EnvDotOptions<TSchema>):
|
|
66
|
+
export declare function env<TSchema extends Record<string, ZodType>>(options: EnvDotOptions<TSchema>): Pip<EmptyShape, EnvServices<TSchema>>;
|
|
69
67
|
//# sourceMappingURL=dot.d.ts.map
|
package/dist/dot.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dot.d.ts","sourceRoot":"","sources":["../src/dot.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"dot.d.ts","sourceRoot":"","sources":["../src/dot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAO,KAAK,UAAU,EAAE,KAAK,GAAG,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAInC;;;;;GAKG;AACH,MAAM,MAAM,aAAa,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI;IACnE,sDAAsD;IACtD,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;CACnC,CAAC;AAEF,0DAA0D;AAC1D,MAAM,MAAM,WAAW,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI;IACjE,QAAQ,CAAC,GAAG,EAAE;QAAE,QAAQ,EAAE,CAAC,IAAI,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,SAAS,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK;KAAE,CAAC;CAClG,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,GAAG,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACzD,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,GAC9B,GAAG,CAAC,UAAU,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC,CAuBvC"}
|
package/dist/dot.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DOT adapter for `@arki/env`.
|
|
3
3
|
*
|
|
4
|
-
* Wraps `defineEnv()` as a
|
|
4
|
+
* Wraps `defineEnv()` as a DOT pip so a DOT app can validate its
|
|
5
5
|
* environment alongside any other services. The pip is sync (env
|
|
6
6
|
* validation is sync), so the `boot` hook returns the validated env
|
|
7
|
-
* object synchronously through the standard
|
|
7
|
+
* object synchronously through the standard provides channel.
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
10
|
* ```ts
|
|
@@ -19,24 +19,32 @@
|
|
|
19
19
|
* console.log(app.services.env.PORT); // number, typed.
|
|
20
20
|
* ```
|
|
21
21
|
*
|
|
22
|
+
* To mount a second env scope in the same app, rename the published wire
|
|
23
|
+
* key at the mount site:
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { rename } from '@arki/dot';
|
|
27
|
+
*
|
|
28
|
+
* .use(env({ schema: appSchema }))
|
|
29
|
+
* .use(rename(env({ schema: publicSchema }), { env: 'publicEnv' }, 'public-env'))
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
22
32
|
* The `@arki/dot` package is an OPTIONAL peer of `@arki/env`. Importing
|
|
23
33
|
* this adapter without `@arki/dot` installed will fail at module load —
|
|
24
34
|
* that is intentional: the adapter only makes sense in a DOT app.
|
|
25
35
|
*/
|
|
26
|
-
import {
|
|
36
|
+
import { pip } from '@arki/dot/pip';
|
|
27
37
|
import { defineEnv } from './core/define-env.js';
|
|
28
38
|
/**
|
|
29
39
|
* Build a DOT pip that validates and publishes a typed `env` service.
|
|
30
40
|
*
|
|
31
41
|
* @param options - Schema + optional config.
|
|
32
|
-
* @returns A
|
|
42
|
+
* @returns A pip that publishes `services.env`.
|
|
33
43
|
*/
|
|
34
44
|
export function env(options) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
name,
|
|
45
|
+
return pip({
|
|
46
|
+
name: 'env',
|
|
38
47
|
version: '0.1.0',
|
|
39
|
-
provides: ['env'],
|
|
40
48
|
configure(ctx) {
|
|
41
49
|
ctx.registerService('env', 'env');
|
|
42
50
|
},
|
|
@@ -52,7 +60,7 @@ export function env(options) {
|
|
|
52
60
|
// The runtime guarantee comes from Zod-side validation inside
|
|
53
61
|
// `defineEnv`. No `any` involved — the cast is type-only.
|
|
54
62
|
const envObject = validated;
|
|
55
|
-
return {
|
|
63
|
+
return { env: envObject };
|
|
56
64
|
},
|
|
57
65
|
});
|
|
58
66
|
}
|
package/dist/dot.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dot.js","sourceRoot":"","sources":["../src/dot.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"dot.js","sourceRoot":"","sources":["../src/dot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,GAAG,EAA6B,MAAM,eAAe,CAAC;AAG/D,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAwBjD;;;;;GAKG;AACH,MAAM,UAAU,GAAG,CACjB,OAA+B;IAE/B,OAAO,GAAG,CAAC;QACT,IAAI,EAAE,KAAK;QACX,OAAO,EAAE,OAAO;QAChB,SAAS,CAAC,GAAG;YACX,GAAG,CAAC,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACpC,CAAC;QACD,IAAI;YACF,MAAM,SAAS,GAAG,SAAS,CAAC;gBAC1B,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,OAAO,EACL,OAAO,CAAC,cAAc,KAAK,SAAS;oBAClC,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,CAAC,cAAc,EAAE;oBAC5C,CAAC,CAAC,SAAS;aAChB,CAAC,CAAC;YACH,oEAAoE;YACpE,kEAAkE;YAClE,8DAA8D;YAC9D,0DAA0D;YAC1D,MAAM,SAAS,GAAG,SAAmD,CAAC;YACtE,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC;QAC5B,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arki/env",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Type-safe environment variable parsing for ARKI — defineEnv() built on @t3-oss/env-core with Zod contracts for common variable shapes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -47,6 +47,12 @@
|
|
|
47
47
|
},
|
|
48
48
|
"files": [
|
|
49
49
|
"dist/**",
|
|
50
|
+
"src/**",
|
|
51
|
+
"!src/**/*.test.*",
|
|
52
|
+
"!src/**/*.spec.*",
|
|
53
|
+
"!src/**/tests/**",
|
|
54
|
+
"!src/**/test/**",
|
|
55
|
+
"!src/**/__tests__/**",
|
|
50
56
|
"README.md",
|
|
51
57
|
"LICENSE",
|
|
52
58
|
"package.json"
|
|
@@ -56,7 +62,7 @@
|
|
|
56
62
|
"zod": "4.3.5"
|
|
57
63
|
},
|
|
58
64
|
"peerDependencies": {
|
|
59
|
-
"@arki/dot": "^0.1.
|
|
65
|
+
"@arki/dot": "^0.1.2"
|
|
60
66
|
},
|
|
61
67
|
"peerDependenciesMeta": {
|
|
62
68
|
"@arki/dot": {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const commonContracts = {
|
|
4
|
+
string: (defaultValue?: string) => {
|
|
5
|
+
const schema = z.string();
|
|
6
|
+
return defaultValue ? schema.default(defaultValue) : schema;
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
number: (defaultValue?: number) => {
|
|
10
|
+
const schema = z.coerce.number();
|
|
11
|
+
return defaultValue ? schema.default(defaultValue) : schema;
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
port: (defaultValue = 3000) =>
|
|
15
|
+
z
|
|
16
|
+
.coerce.number()
|
|
17
|
+
.min(1, 'Port must be at least 1')
|
|
18
|
+
.max(65_535, 'Port must be at most 65535')
|
|
19
|
+
.default(defaultValue),
|
|
20
|
+
|
|
21
|
+
nodeEnv: () => z.enum(['development', 'production', 'test']).default('development'),
|
|
22
|
+
|
|
23
|
+
boolean: (defaultValue = false) =>
|
|
24
|
+
z
|
|
25
|
+
.enum(['true', 'false', '1', '0'])
|
|
26
|
+
.default(defaultValue ? 'true' : 'false')
|
|
27
|
+
.transform(val => val === 'true' || val === '1'),
|
|
28
|
+
|
|
29
|
+
logLevel: (defaultValue: 'debug' | 'info' | 'warn' | 'error' = 'info') =>
|
|
30
|
+
z.enum(['debug', 'info', 'warn', 'error']).default(defaultValue),
|
|
31
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { commonContracts } from './common.js';
|
|
2
|
+
import { parseContracts } from './parsing.js';
|
|
3
|
+
import { urlContracts } from './urls.js';
|
|
4
|
+
|
|
5
|
+
// Unified contracts export
|
|
6
|
+
export const contracts = {
|
|
7
|
+
...commonContracts,
|
|
8
|
+
...urlContracts,
|
|
9
|
+
...parseContracts,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Individual category exports
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export {commonContracts} from './common.js';
|
|
16
|
+
export {urlContracts} from './urls.js';
|
|
17
|
+
export {parseContracts} from './parsing.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const parseContracts = {
|
|
4
|
+
json: <T extends z.ZodType>(schema: T) =>
|
|
5
|
+
z.string().transform((str) => {
|
|
6
|
+
const parsed = JSON.parse(str);
|
|
7
|
+
return schema.parse(parsed) as z.infer<T>;
|
|
8
|
+
}),
|
|
9
|
+
|
|
10
|
+
csvArray: <T extends z.ZodType>(itemSchema: T) =>
|
|
11
|
+
z.preprocess(
|
|
12
|
+
(val) => {
|
|
13
|
+
if (typeof val !== 'string') return val;
|
|
14
|
+
return val.split(',').map((s) => s.trim());
|
|
15
|
+
},
|
|
16
|
+
z.array(itemSchema)
|
|
17
|
+
),
|
|
18
|
+
|
|
19
|
+
csvSet: <T extends z.ZodType>(itemSchema: T) =>
|
|
20
|
+
z.preprocess(
|
|
21
|
+
(val) => {
|
|
22
|
+
if (typeof val !== 'string') return val;
|
|
23
|
+
return [...new Set(val.split(',').map((s) => s.trim()))];
|
|
24
|
+
},
|
|
25
|
+
z.array(itemSchema)
|
|
26
|
+
),
|
|
27
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const urlContracts = {
|
|
4
|
+
url: (defaultValue?: string) => {
|
|
5
|
+
const schema = z.url();
|
|
6
|
+
return defaultValue ? schema.default(defaultValue) : schema;
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
databaseUrl: () =>
|
|
10
|
+
z
|
|
11
|
+
.url()
|
|
12
|
+
.refine(
|
|
13
|
+
url => url.startsWith('postgres://') || url.startsWith('postgresql://'),
|
|
14
|
+
'Database URL must start with postgres:// or postgresql://',
|
|
15
|
+
),
|
|
16
|
+
|
|
17
|
+
redisUrl: () =>
|
|
18
|
+
z
|
|
19
|
+
.url()
|
|
20
|
+
.refine(
|
|
21
|
+
url => url.startsWith('redis://') || url.startsWith('rediss://'),
|
|
22
|
+
'Redis URL must start with redis:// or rediss://',
|
|
23
|
+
),
|
|
24
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createEnv } from '@t3-oss/env-core';
|
|
2
|
+
import type { CreateEnv, DefaultCombinedSchema } from '@t3-oss/env-core';
|
|
3
|
+
import type { ZodType } from 'zod';
|
|
4
|
+
|
|
5
|
+
/** The issue shape t3-oss hands to `onValidationError`, derived so it stays in sync with the lib. */
|
|
6
|
+
type ValidationIssues = Parameters<
|
|
7
|
+
NonNullable<Parameters<typeof createEnv>[0]['onValidationError']>
|
|
8
|
+
>[0];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Fold validation issues into a human-legible, log-pipeline-safe string. The t3-oss default handler
|
|
12
|
+
* logs `issues` as a raw object, which platforms like Coolify collapse to `[Object]` — so the var
|
|
13
|
+
* that actually failed never reaches the logs. A flat ` path: message` list survives transport.
|
|
14
|
+
*/
|
|
15
|
+
function formatIssues(name: string | undefined, issues: ValidationIssues): string {
|
|
16
|
+
const label = name ? `[${name}] ` : '';
|
|
17
|
+
const lines = issues.map(issue => {
|
|
18
|
+
const path = (issue.path ?? [])
|
|
19
|
+
.map(seg => (typeof seg === 'object' && seg !== null && 'key' in seg ? String(seg.key) : String(seg)))
|
|
20
|
+
.join('.');
|
|
21
|
+
return ` ${path || '(root)'}: ${issue.message}`;
|
|
22
|
+
});
|
|
23
|
+
return `❌ ${label}Invalid environment variables:\n${lines.join('\n')}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Thin, type-preserving wrapper over `@t3-oss/env-core`'s `createEnv`. Generic over the server,
|
|
28
|
+
* client, shared and composed schemas so the parsed env keeps its exact inferred types — the wrapper
|
|
29
|
+
* adds the ARKI defaults (auto `runtimeEnv`, prod-safe `skipValidation`, a legible labelled error
|
|
30
|
+
* handler) without widening any value to `unknown`.
|
|
31
|
+
*
|
|
32
|
+
* `TPrefix` is kept as a string-literal generic on purpose: `createEnv` uses it to enforce that
|
|
33
|
+
* client keys (and only client keys) carry the prefix. Widening it to `string` would make every
|
|
34
|
+
* server key collapse to a type-level `ErrorMessage`, so the prefix must stay a literal.
|
|
35
|
+
*/
|
|
36
|
+
export function defineEnv<
|
|
37
|
+
TServer extends Record<string, ZodType>,
|
|
38
|
+
TClient extends Record<string, ZodType> = NonNullable<unknown>,
|
|
39
|
+
TShared extends Record<string, ZodType> = NonNullable<unknown>,
|
|
40
|
+
const TPrefix extends string = '',
|
|
41
|
+
const TExtends extends Array<Record<string, unknown>> = [],
|
|
42
|
+
>(config: {
|
|
43
|
+
/**
|
|
44
|
+
* Label shown in validation-error logs, e.g. `'@acme/backend'`. Without it a misconfigured
|
|
45
|
+
* deploy logs a bare "Invalid environment variables" with no hint which env failed — exactly the
|
|
46
|
+
* blind failure this wrapper exists to prevent.
|
|
47
|
+
*/
|
|
48
|
+
name?: string;
|
|
49
|
+
server: TServer;
|
|
50
|
+
/** Client-side schema. Every key must start with `clientPrefix` (enforced by `createEnv`). */
|
|
51
|
+
client?: TClient;
|
|
52
|
+
/** Prefix client keys must carry, e.g. `'VITE_'`. Defaults to `''` (no client vars). */
|
|
53
|
+
clientPrefix?: TPrefix;
|
|
54
|
+
/** Shared vars available to both client and server, unprefixed (e.g. `NODE_ENV`). */
|
|
55
|
+
shared?: TShared;
|
|
56
|
+
/**
|
|
57
|
+
* Already-built envs to compose in (e.g. `@arki/auth`, `@arki/db`). Their parsed variables merge
|
|
58
|
+
* into the result with full types; each was validated by its own `createEnv` at import time.
|
|
59
|
+
*/
|
|
60
|
+
extends?: TExtends;
|
|
61
|
+
/**
|
|
62
|
+
* Explicit runtime source. Omit it and `defineEnv` auto-builds one by reading `process.env` for
|
|
63
|
+
* every declared key — correct for any server-side env. Provide it only when the auto-build would
|
|
64
|
+
* be wrong: Vite/`start` apps whose client vars come from `import.meta.env` in the browser, or
|
|
65
|
+
* keys with a fallback/alias (e.g. `DB_URL: process.env.DB_URL ?? process.env.DATABASE_URL`).
|
|
66
|
+
*/
|
|
67
|
+
runtimeEnv?: Record<string, string | boolean | number | undefined>;
|
|
68
|
+
options?: {
|
|
69
|
+
skipValidation?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Treat empty-string env values as `undefined` so defaults apply (e.g. `PORT=` in `.env`, or a
|
|
72
|
+
* Docker arg baked as `X=""`). Defaults to `true` — set `false` only if an app intentionally
|
|
73
|
+
* accepts `""` as a valid required string.
|
|
74
|
+
*/
|
|
75
|
+
emptyStringAsUndefined?: boolean;
|
|
76
|
+
onValidationError?: (issues: ValidationIssues) => never;
|
|
77
|
+
};
|
|
78
|
+
}): CreateEnv<DefaultCombinedSchema<TServer, TClient, TShared>, TExtends> {
|
|
79
|
+
const skipValidation =
|
|
80
|
+
config.options?.skipValidation ?? (!!process.env.CI || process.env.NODE_ENV !== 'development');
|
|
81
|
+
|
|
82
|
+
// Use the caller's explicit runtimeEnv when supplied (Vite client vars via import.meta.env, or
|
|
83
|
+
// fallback aliases). Otherwise auto-build from every declared key (server + client + shared);
|
|
84
|
+
// createEnv tolerates keys absent from process.env (they parse as undefined → optional/default).
|
|
85
|
+
let runtimeEnv: Record<string, string | boolean | number | undefined>;
|
|
86
|
+
if (config.runtimeEnv) {
|
|
87
|
+
runtimeEnv = config.runtimeEnv;
|
|
88
|
+
} else {
|
|
89
|
+
runtimeEnv = {};
|
|
90
|
+
for (const key of Object.keys(config.server)) {
|
|
91
|
+
runtimeEnv[key] = process.env[key];
|
|
92
|
+
}
|
|
93
|
+
for (const key of Object.keys(config.client ?? {})) {
|
|
94
|
+
runtimeEnv[key] = process.env[key];
|
|
95
|
+
}
|
|
96
|
+
for (const key of Object.keys(config.shared ?? {})) {
|
|
97
|
+
runtimeEnv[key] = process.env[key];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const onValidationError =
|
|
102
|
+
config.options?.onValidationError ??
|
|
103
|
+
((issues: ValidationIssues): never => {
|
|
104
|
+
const message = formatIssues(config.name, issues);
|
|
105
|
+
console.error(message);
|
|
106
|
+
throw new Error(message);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// `createEnv`'s `server`/`client` params are conditional on `clientPrefix` being a *concrete*
|
|
110
|
+
// string literal; inside a generic wrapper `TPrefix` is abstract, so the body cannot be verified
|
|
111
|
+
// for all prefixes and the call argument must be asserted once. The PUBLIC contract stays exact:
|
|
112
|
+
// the explicit return type above reconstructs `createEnv`'s own result from the generics, and the
|
|
113
|
+
// caller's `server`/`client`/`extends` are fully type-checked against `TServer`/`TClient`/`TExtends`
|
|
114
|
+
// at the call site. This is the only seam where types are asserted — value types are never widened.
|
|
115
|
+
const options = {
|
|
116
|
+
shared: config.shared ?? {},
|
|
117
|
+
extends: config.extends,
|
|
118
|
+
server: config.server,
|
|
119
|
+
client: config.client ?? {},
|
|
120
|
+
clientPrefix: config.clientPrefix ?? '',
|
|
121
|
+
runtimeEnv,
|
|
122
|
+
skipValidation,
|
|
123
|
+
// Default ON: in containerized deploys an unset var is baked as "" (e.g. Docker `ENV X=${X}`
|
|
124
|
+
// with no build arg), not left absent. Treating "" as undefined lets `.default()`/`.optional()`
|
|
125
|
+
// apply as intended instead of failing format checks like `z.url()` on an empty string.
|
|
126
|
+
emptyStringAsUndefined: config.options?.emptyStringAsUndefined ?? true,
|
|
127
|
+
onValidationError,
|
|
128
|
+
} satisfies Record<string, unknown>;
|
|
129
|
+
|
|
130
|
+
return createEnv(options as Parameters<typeof createEnv>[0]) as CreateEnv<
|
|
131
|
+
DefaultCombinedSchema<TServer, TClient, TShared>,
|
|
132
|
+
TExtends
|
|
133
|
+
>;
|
|
134
|
+
}
|
package/src/dot.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOT adapter for `@arki/env`.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `defineEnv()` as a DOT pip so a DOT app can validate its
|
|
5
|
+
* environment alongside any other services. The pip is sync (env
|
|
6
|
+
* validation is sync), so the `boot` hook returns the validated env
|
|
7
|
+
* object synchronously through the standard provides channel.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { defineApp } from '@arki/dot';
|
|
12
|
+
* import { env } from '@arki/env/dot';
|
|
13
|
+
* import { z } from 'zod';
|
|
14
|
+
*
|
|
15
|
+
* const app = await defineApp('my-app')
|
|
16
|
+
* .use(env({ schema: { PORT: z.coerce.number().default(3000) } }))
|
|
17
|
+
* .boot();
|
|
18
|
+
*
|
|
19
|
+
* console.log(app.services.env.PORT); // number, typed.
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* To mount a second env scope in the same app, rename the published wire
|
|
23
|
+
* key at the mount site:
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { rename } from '@arki/dot';
|
|
27
|
+
*
|
|
28
|
+
* .use(env({ schema: appSchema }))
|
|
29
|
+
* .use(rename(env({ schema: publicSchema }), { env: 'publicEnv' }, 'public-env'))
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* The `@arki/dot` package is an OPTIONAL peer of `@arki/env`. Importing
|
|
33
|
+
* this adapter without `@arki/dot` installed will fail at module load —
|
|
34
|
+
* that is intentional: the adapter only makes sense in a DOT app.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { pip, type EmptyShape, type Pip } from '@arki/dot/pip';
|
|
38
|
+
import type { ZodType } from 'zod';
|
|
39
|
+
|
|
40
|
+
import { defineEnv } from './core/define-env.js';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Options for the env DOT adapter.
|
|
44
|
+
*
|
|
45
|
+
* @typeParam TSchema - The shape of the `server` schema record. The
|
|
46
|
+
* resulting `app.services.env` is typed from this.
|
|
47
|
+
*/
|
|
48
|
+
export type EnvDotOptions<TSchema extends Record<string, ZodType>> = {
|
|
49
|
+
/** Zod schemas for each env var the pip validates. */
|
|
50
|
+
readonly schema: TSchema;
|
|
51
|
+
/**
|
|
52
|
+
* Forwarded to `defineEnv`. If `true`, the validator is skipped
|
|
53
|
+
* entirely. By default, validation is skipped when `CI` is set or
|
|
54
|
+
* `NODE_ENV !== 'development'`.
|
|
55
|
+
*/
|
|
56
|
+
readonly skipValidation?: boolean;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** The shape of services published by the env adapter. */
|
|
60
|
+
export type EnvServices<TSchema extends Record<string, ZodType>> = {
|
|
61
|
+
readonly env: { readonly [K in keyof TSchema]: TSchema[K] extends ZodType<infer U> ? U : never };
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build a DOT pip that validates and publishes a typed `env` service.
|
|
66
|
+
*
|
|
67
|
+
* @param options - Schema + optional config.
|
|
68
|
+
* @returns A pip that publishes `services.env`.
|
|
69
|
+
*/
|
|
70
|
+
export function env<TSchema extends Record<string, ZodType>>(
|
|
71
|
+
options: EnvDotOptions<TSchema>,
|
|
72
|
+
): Pip<EmptyShape, EnvServices<TSchema>> {
|
|
73
|
+
return pip({
|
|
74
|
+
name: 'env',
|
|
75
|
+
version: '0.1.0',
|
|
76
|
+
configure(ctx) {
|
|
77
|
+
ctx.registerService('env', 'env');
|
|
78
|
+
},
|
|
79
|
+
boot() {
|
|
80
|
+
const validated = defineEnv({
|
|
81
|
+
server: options.schema,
|
|
82
|
+
options:
|
|
83
|
+
options.skipValidation !== undefined
|
|
84
|
+
? { skipValidation: options.skipValidation }
|
|
85
|
+
: undefined,
|
|
86
|
+
});
|
|
87
|
+
// `defineEnv` returns a Proxy from `@t3-oss/env-core`; we narrow it
|
|
88
|
+
// to the schema-derived shape so downstream typing flows through.
|
|
89
|
+
// The runtime guarantee comes from Zod-side validation inside
|
|
90
|
+
// `defineEnv`. No `any` involved — the cast is type-only.
|
|
91
|
+
const envObject = validated as unknown as EnvServices<TSchema>['env'];
|
|
92
|
+
return { env: envObject };
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
package/src/index.ts
ADDED