celery-env 0.1.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/docs/SCHEMA.md ADDED
@@ -0,0 +1,149 @@
1
+ # Schema API
2
+
3
+ Schemas are plain JavaScript modules. Each key describes one env var.
4
+
5
+ ```js
6
+ import { defineEnv, int, str } from "celery-env";
7
+
8
+ export default defineEnv({
9
+ DATABASE_URL: str({ min: 1 }),
10
+ PORT: int({ default: 3000 })
11
+ });
12
+ ```
13
+
14
+ Think of the schema as executable documentation for your configuration. The key
15
+ is the env var name, the validator describes the accepted string format, and
16
+ options describe defaults, examples, and environment-specific behavior.
17
+
18
+ ## Validators
19
+
20
+ | Validator | Output | Use For |
21
+ | --- | --- | --- |
22
+ | `str(options)` | `string` | Text, secrets, tokens. |
23
+ | `int(options)` | `number` | Whole numbers like ports and limits. |
24
+ | `num(options)` | `number` | Decimal numbers. |
25
+ | `bool(options)` | `boolean` | Feature flags. |
26
+ | `oneOf(values, options)` | union | Enums like `NODE_ENV`. |
27
+ | `url(options)` | `string` | URLs with optional protocols. |
28
+ | `json(options)` | `unknown` | JSON strings parsed with `JSON.parse`. |
29
+ | `list(item, options)` | `readonly T[]` | Comma-separated lists. |
30
+
31
+ ## Common Options
32
+
33
+ | Option | Meaning |
34
+ | --- | --- |
35
+ | `default` | Value used when the env var is missing. |
36
+ | `devDefault` | Value used when `NODE_ENV` is not `production`. |
37
+ | `testDefault` | Value used when `NODE_ENV` is `test`. |
38
+ | `optional` | Allows the value to be missing. |
39
+ | `requiredWhen` | Function that can make a value required. |
40
+ | `desc` | Description used in generated `.env.example`. |
41
+ | `example` | Example value used in generated `.env.example`. |
42
+ | `docs` | Longer documentation text for generated metadata. |
43
+
44
+ `testDefault` wins over `devDefault`, and `default` applies in every
45
+ environment.
46
+
47
+ ## Missing Values
48
+
49
+ Empty strings are treated as missing. If a value is missing, Celery checks
50
+ options in this order:
51
+
52
+ 1. `testDefault` when `NODE_ENV` is `test`.
53
+ 2. `devDefault` when `NODE_ENV` is not `production`.
54
+ 3. `default`.
55
+ 4. `optional`.
56
+ 5. Otherwise, the variable is required.
57
+
58
+ ## Strings
59
+
60
+ ```js
61
+ str({ min: 8, max: 128 })
62
+ str({ startsWith: "sk_" })
63
+ str({ includes: "@" })
64
+ ```
65
+
66
+ ## Numbers
67
+
68
+ ```js
69
+ int({ min: 1, max: 65535 })
70
+ num({ min: 0, max: 1 })
71
+ ```
72
+
73
+ By default, numeric parsing follows JavaScript `Number()`. Use `strict: true`
74
+ to reject values such as hex and exponent notation:
75
+
76
+ ```js
77
+ int({ strict: true })
78
+ num({ strict: true })
79
+ ```
80
+
81
+ ## Booleans
82
+
83
+ Accepted true values:
84
+
85
+ ```text
86
+ true, 1, yes, on
87
+ ```
88
+
89
+ Accepted false values:
90
+
91
+ ```text
92
+ false, 0, no, off
93
+ ```
94
+
95
+ ## Enums
96
+
97
+ ```js
98
+ oneOf(["development", "test", "production"], {
99
+ default: "development"
100
+ })
101
+ ```
102
+
103
+ Values can be strings, numbers, or booleans.
104
+
105
+ ## URLs
106
+
107
+ ```js
108
+ url({ protocols: ["https"] })
109
+ url({ protocols: ["postgres", "postgresql"] })
110
+ ```
111
+
112
+ Write protocols without the colon. Use `postgres`, not `postgres:`.
113
+
114
+ ## JSON
115
+
116
+ ```js
117
+ json()
118
+ ```
119
+
120
+ Celery validates that the value is valid JSON. It does not validate the object
121
+ shape inside that JSON.
122
+
123
+ ## Lists
124
+
125
+ ```js
126
+ list(str())
127
+ list(int({ strict: true }))
128
+ list(url({ protocols: ["https"] }))
129
+ ```
130
+
131
+ Options:
132
+
133
+ ```js
134
+ list(str(), { separator: ",", trim: true })
135
+ ```
136
+
137
+ `separator` defaults to `","`. `trim` defaults to `true`.
138
+
139
+ ## Conditional Required Values
140
+
141
+ ```js
142
+ SESSION_SECRET: str({
143
+ optional: true,
144
+ requiredWhen: (env) => env.NODE_ENV === "production"
145
+ })
146
+ ```
147
+
148
+ Generated validators serialize `requiredWhen` with `Function#toString()`.
149
+ Keep the function self-contained and do not close over local variables.
@@ -0,0 +1,74 @@
1
+ # Troubleshooting
2
+
3
+ ## The CLI Refuses To Overwrite A File
4
+
5
+ Generation does not overwrite existing files unless you pass `--force`.
6
+
7
+ ```sh
8
+ npx celery-env generate \
9
+ --schema env.schema.mjs \
10
+ --out src/env.mjs \
11
+ --types src/env.d.ts \
12
+ --force
13
+ ```
14
+
15
+ ## TypeScript Cannot Find The Generated Types
16
+
17
+ Generate declarations with `--types` and import the generated module path:
18
+
19
+ ```sh
20
+ npx celery-env generate --schema env.schema.mjs --out src/env.mjs --types src/env.d.ts
21
+ ```
22
+
23
+ ```ts
24
+ import { loadEnv } from "./env.mjs";
25
+ ```
26
+
27
+ The `.d.ts` file must sit next to the generated `.mjs` file with the same base
28
+ name.
29
+
30
+ ## A URL Protocol Is Rejected
31
+
32
+ Write protocols without the colon:
33
+
34
+ ```js
35
+ url({ protocols: ["postgres"] })
36
+ ```
37
+
38
+ Use `postgres`, not `postgres:`.
39
+
40
+ ## A Production Secret Is Missing
41
+
42
+ Use `requiredWhen` for values that are optional in development but required in
43
+ production:
44
+
45
+ ```js
46
+ SESSION_SECRET: str({
47
+ optional: true,
48
+ requiredWhen: (env) => env.NODE_ENV === "production"
49
+ })
50
+ ```
51
+
52
+ Keep `requiredWhen` self-contained. Generated validators serialize the function
53
+ source, so it should not close over local variables.
54
+
55
+ ## `json()` Is Typed As `unknown`
56
+
57
+ Celery only checks that the env value is valid JSON. It does not validate the
58
+ object shape inside the JSON string. Narrow or validate the parsed value in your
59
+ app before using fields from it.
60
+
61
+ ## Generated Mode Feels Like Too Much
62
+
63
+ Use runtime mode:
64
+
65
+ ```js
66
+ import { defineEnv, int, parseEnv, str } from "celery-env";
67
+
68
+ const schema = defineEnv({
69
+ DATABASE_URL: str({ min: 1 }),
70
+ PORT: int({ default: 3000 })
71
+ });
72
+
73
+ export const env = parseEnv(schema, process.env);
74
+ ```
@@ -0,0 +1,105 @@
1
+ # TypeScript
2
+
3
+ You do not need to be a TypeScript expert to use Celery. The main idea is:
4
+
5
+ 1. Write a schema.
6
+ 2. Generate `src/env.d.ts`.
7
+ 3. Import `env` or `loadEnv` and let your editor infer the types.
8
+
9
+ ## Generated Types
10
+
11
+ Generate both JavaScript and declarations:
12
+
13
+ ```sh
14
+ npx celery-env generate \
15
+ --schema env.schema.mjs \
16
+ --out src/env.mjs \
17
+ --types src/env.d.ts
18
+ ```
19
+
20
+ Then import the generated module:
21
+
22
+ ```ts
23
+ import { loadEnv } from "./env.mjs";
24
+
25
+ const env = loadEnv(process.env);
26
+
27
+ env.PORT;
28
+ // ^ number
29
+
30
+ env.DATABASE_URL;
31
+ // ^ string
32
+ ```
33
+
34
+ ## Optional Values
35
+
36
+ ```js
37
+ import { defineEnv, url } from "celery-env";
38
+
39
+ export default defineEnv({
40
+ SENTRY_DSN: url({ optional: true })
41
+ });
42
+ ```
43
+
44
+ TypeScript sees:
45
+
46
+ ```ts
47
+ env.SENTRY_DSN;
48
+ // string | undefined
49
+ ```
50
+
51
+ ## Defaults Remove Undefined
52
+
53
+ ```js
54
+ import { defineEnv, int } from "celery-env";
55
+
56
+ export default defineEnv({
57
+ PORT: int({ default: 3000 })
58
+ });
59
+ ```
60
+
61
+ TypeScript sees:
62
+
63
+ ```ts
64
+ env.PORT;
65
+ // number
66
+ ```
67
+
68
+ ## Inferring From A Schema
69
+
70
+ If you use runtime mode or want a named type:
71
+
72
+ ```ts
73
+ import type { InferEnv } from "celery-env";
74
+ import schema from "../env.schema.mjs";
75
+
76
+ export type Env = InferEnv<typeof schema>;
77
+ ```
78
+
79
+ ## JSON Types
80
+
81
+ Generated declarations type `json()` values as `unknown`, because Celery only
82
+ validates JSON syntax.
83
+
84
+ ```js
85
+ import { defineEnv, json } from "celery-env";
86
+
87
+ const schema = defineEnv({
88
+ RATE_LIMIT_JSON: json()
89
+ });
90
+ ```
91
+
92
+ Narrow the value in your app after parsing:
93
+
94
+ ```ts
95
+ const rateLimit = env.RATE_LIMIT_JSON;
96
+
97
+ if (
98
+ rateLimit &&
99
+ typeof rateLimit === "object" &&
100
+ "windowMs" in rateLimit &&
101
+ "max" in rateLimit
102
+ ) {
103
+ // rateLimit has the fields you checked for here.
104
+ }
105
+ ```
@@ -0,0 +1,7 @@
1
+ <svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc">
2
+ <title id="title">celery-env mark</title>
3
+ <desc id="desc">A compact green celery-env leaf mark.</desc>
4
+ <rect width="128" height="128" rx="28" fill="#0F766E"/>
5
+ <path d="M71 19c-7 9-11 18-12 27 12-3 23-11 34-24 3 21-9 37-31 45 12 6 24 7 38 3-13 18-30 25-50 22-8-1-15-5-20-10 8-2 15-5 21-10-13-3-22-12-26-26 13 7 25 10 36 7-2-10 1-21 10-34Z" fill="#D9F99D"/>
6
+ <path d="M38 103c16-2 32-2 48 0" stroke="#ECFDF5" stroke-width="8" stroke-linecap="round"/>
7
+ </svg>
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "celery-env",
3
+ "version": "0.1.0",
4
+ "description": "Zero-dependency environment validation with generated standalone validators.",
5
+ "type": "module",
6
+ "types": "./src/index.d.ts",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.d.ts",
11
+ "import": "./src/index.js",
12
+ "default": "./src/index.js"
13
+ },
14
+ "./compiler": {
15
+ "types": "./src/compiler.d.ts",
16
+ "import": "./src/compiler.js",
17
+ "default": "./src/compiler.js"
18
+ }
19
+ },
20
+ "bin": {
21
+ "celery-env": "src/cli.js"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/theaaravagarwal/celery-env.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/theaaravagarwal/celery-env/issues"
29
+ },
30
+ "homepage": "https://github.com/theaaravagarwal/celery-env#readme",
31
+ "files": [
32
+ "src/index.js",
33
+ "src/compiler.js",
34
+ "src/index.d.ts",
35
+ "src/compiler.d.ts",
36
+ "src/cli.js",
37
+ "docs/assets/celery-mark.svg",
38
+ "docs/BENCHMARKS.md",
39
+ "docs/CLI.md",
40
+ "docs/COMPARISON.md",
41
+ "docs/GETTING_STARTED.md",
42
+ "docs/MIGRATION.md",
43
+ "docs/README.md",
44
+ "docs/RUNTIME.md",
45
+ "docs/SCHEMA.md",
46
+ "docs/TROUBLESHOOTING.md",
47
+ "docs/TYPESCRIPT.md",
48
+ "SECURITY.md",
49
+ "README.md",
50
+ "LICENSE"
51
+ ],
52
+ "scripts": {
53
+ "ci": "npm run security:scan",
54
+ "test": "node --test test/*.mjs",
55
+ "size": "node scripts/size.mjs",
56
+ "security:scan": "node scripts/security-scan.mjs && npm run prepublishOnly",
57
+ "validate:publish": "node scripts/validate-publish.mjs",
58
+ "prepublishOnly": "npm test && npm run size && npm run validate:publish",
59
+ "demo:generate": "node src/cli.js --schema examples/env.schema.mjs --out .tmp/generated/env.mjs --types .tmp/generated/env.d.ts"
60
+ },
61
+ "keywords": [
62
+ "env",
63
+ "validation",
64
+ "environment",
65
+ "config",
66
+ "schema",
67
+ "typescript",
68
+ "serverless",
69
+ "zod",
70
+ "dotenv",
71
+ "cli"
72
+ ],
73
+ "license": "MIT",
74
+ "engines": {
75
+ "node": ">=18"
76
+ }
77
+ }
package/src/cli.js ADDED
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ import { constants } from "node:fs";
3
+ import { mkdir, open, writeFile } from "node:fs/promises";
4
+ import { dirname, resolve } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ const NOFOLLOW = constants.O_NOFOLLOW || 0;
8
+
9
+ const args = parseArgs(process.argv.slice(2));
10
+
11
+ if (args.help) usage(0);
12
+
13
+ if (args.command === "init") {
14
+ await init(args);
15
+ } else {
16
+ await generate(args);
17
+ }
18
+
19
+ async function generate(args) {
20
+ if (!args.schema || !args.out) usage(1);
21
+
22
+ const schemaPath = resolve(args.schema);
23
+ const mod = await import(pathToFileURL(schemaPath).href);
24
+ const schema = mod.default || mod.schema;
25
+
26
+ if (!schema) {
27
+ throw new Error(`No default export or named "schema" export found in ${schemaPath}`);
28
+ }
29
+
30
+ const outPath = resolve(args.out);
31
+ const { generateExample, generateTypes, generateValidator } = await import("./compiler.js");
32
+ await mkdir(dirname(outPath), { recursive: true });
33
+ const options = { functionName: args.functionName, processDefault: args.processDefault, minify: args.minify, failFast: args.failFast, optimize: args.optimize };
34
+ await writeOutput(outPath, generateValidator(schema, options), args.force);
35
+
36
+ if (args.types) {
37
+ const typesPath = resolve(args.types);
38
+ await mkdir(dirname(typesPath), { recursive: true });
39
+ await writeOutput(typesPath, generateTypes(schema, options), args.force);
40
+ }
41
+
42
+ if (args.example) {
43
+ const examplePath = resolve(args.example);
44
+ await mkdir(dirname(examplePath), { recursive: true });
45
+ await writeOutput(examplePath, generateExample(schema), args.force);
46
+ }
47
+ }
48
+
49
+ async function init(args) {
50
+ if (!args.schema) usage(1);
51
+ const target = args.target || "node";
52
+ const source = template(target);
53
+ const schemaPath = resolve(args.schema);
54
+ await mkdir(dirname(schemaPath), { recursive: true });
55
+ await writeFile(schemaPath, source, { encoding: "utf8", flag: "wx" });
56
+ }
57
+
58
+ function parseArgs(argv) {
59
+ const out = { command: "generate", functionName: "loadEnv" };
60
+ if (argv[0] === "generate" || argv[0] === "init") out.command = argv.shift();
61
+ for (let i = 0; i < argv.length; i += 1) {
62
+ const arg = argv[i];
63
+ if (arg === "--help" || arg === "-h") out.help = true;
64
+ else if (arg === "--schema") out.schema = argv[++i];
65
+ else if (arg === "--target") out.target = argv[++i];
66
+ else if (arg === "--out") out.out = argv[++i];
67
+ else if (arg === "--types") out.types = argv[++i];
68
+ else if (arg === "--example") out.example = argv[++i];
69
+ else if (arg === "--function-name") out.functionName = argv[++i];
70
+ else if (arg === "--no-process-default") out.processDefault = false;
71
+ else if (arg === "--minify") out.minify = true;
72
+ else if (arg === "--fail-fast") out.failFast = true;
73
+ else if (arg === "--force") out.force = true;
74
+ else if (arg === "--optimize") out.optimize = argv[++i];
75
+ else throw new Error(`Unknown argument: ${arg}`);
76
+ }
77
+ return out;
78
+ }
79
+
80
+ async function writeOutput(path, source, force) {
81
+ const flags = constants.O_WRONLY | constants.O_CREAT | NOFOLLOW | (force ? constants.O_TRUNC : constants.O_EXCL);
82
+ let file;
83
+ try {
84
+ file = await open(path, flags, 0o666);
85
+ } catch (error) {
86
+ if (error.code === "EEXIST") throw new Error(`${path} already exists; pass --force to overwrite`);
87
+ if (error.code === "ELOOP") throw new Error(`${path} is a symlink; refusing to write`);
88
+ throw error;
89
+ }
90
+ try {
91
+ await file.writeFile(source, "utf8");
92
+ } finally {
93
+ await file.close();
94
+ }
95
+ }
96
+
97
+ function template(target) {
98
+ if (target === "node") return `import { bool, defineEnv, int, str } from "celery-env";
99
+
100
+ export default defineEnv({
101
+ NODE_ENV: str({ default: "development", desc: "Current runtime environment." }),
102
+ DATABASE_URL: str({ min: 1, desc: "Primary database connection string.", example: "postgres://user:pass@localhost:5432/app" }),
103
+ PORT: int({ default: 3000, min: 1, max: 65535 }),
104
+ DEBUG: bool({ default: false })
105
+ });
106
+ `;
107
+ if (target === "next") return `import { bool, defineEnv, str } from "celery-env";
108
+
109
+ export default defineEnv({
110
+ NODE_ENV: str({ default: "development" }),
111
+ DATABASE_URL: str({ min: 1, desc: "Server-only database connection string." }),
112
+ NEXT_PUBLIC_API_URL: str({ min: 1, startsWith: "https://", desc: "Browser-visible API origin.", example: "https://api.example.com" }),
113
+ NEXT_PUBLIC_ENABLE_ANALYTICS: bool({ default: false })
114
+ });
115
+ `;
116
+ if (target === "vite") return `import { bool, defineEnv, str } from "celery-env";
117
+
118
+ export default defineEnv({
119
+ MODE: str({ default: "development" }),
120
+ VITE_API_URL: str({ min: 1, startsWith: "https://", desc: "Browser-visible API origin.", example: "https://api.example.com" }),
121
+ VITE_ENABLE_SEARCH: bool({ default: false })
122
+ });
123
+ `;
124
+ throw new Error(`Unknown init target: ${target}`);
125
+ }
126
+
127
+ function usage(code) {
128
+ console.log(`Usage:
129
+ celery-env --schema env.schema.mjs --out src/env.mjs [--types src/env.d.ts]
130
+ celery-env generate --schema env.schema.mjs --out src/env.mjs [--types src/env.d.ts] [--example .env.example] [--force] [--optimize speed]
131
+ celery-env init --target node|next|vite --schema env.schema.mjs`);
132
+ process.exit(code);
133
+ }
@@ -0,0 +1,7 @@
1
+ import type { Spec } from "./index.js";
2
+ export type GenerateValidatorOptions = { functionName?: string; processDefault?: boolean; minify?: boolean; failFast?: boolean; optimize?: "default" | "speed"; splitLarge?: boolean; splitLargeThreshold?: number };
3
+ export type GenerateJsonSchemaOptions = { title?: string; additionalProperties?: boolean };
4
+ export function generateValidator(schema: Record<string, Spec<unknown>>, options?: GenerateValidatorOptions): string;
5
+ export function generateTypes(schema: Record<string, Spec<unknown>>, options?: { functionName?: string; processDefault?: boolean }): string;
6
+ export function generateExample(schema: Record<string, Spec<unknown>>): string;
7
+ export function generateJsonSchema(schema: Record<string, Spec<unknown>>, options?: GenerateJsonSchemaOptions): Record<string, unknown>;