Code generation
Your .env.schema is the single source of truth for your environment variables — their names, types, and metadata. Varlock can turn that schema into generated code: strongly-typed definitions for your language, and, via plugins, anything else you can derive from a schema.
Generation is driven by per-language root decorators in your schema. Each one writes its own output file:
# @generateTsTypes(path=./env.d.ts)# @generatePythonEnv(path=./env_types.py)Output is regenerated automatically on varlock load and varlock run, or explicitly with varlock codegen. It’s derived only from non-environment-specific schema definitions, so the result is deterministic regardless of which environment is active.
TypeScript
Section titled “TypeScript”@generateTsTypes is the default for JavaScript/TypeScript projects. Out of the box it makes import { ENV } from 'varlock/env' fully typed, and augments process.env / import.meta.env:
# @generateTsTypes(path=./env.d.ts)You can control what it emits:
exposeEnv— where the coercedENVobject lives:global(default, augmentsvarlock/env),local(export a package-localENVfrom the generated file, ideal for monorepos), ornone.processEnv/importMetaEnv—strict(only defined keys),loose(also allow extra keys), ornone(don’t augment).
See the @generateTsTypes reference for details. In monorepos where multiple packages have different schemas, prefer exposeEnv=local to avoid global augmentation clashes — see Typed ENV across packages.
Other languages
Section titled “Other languages”Varlock ships generators for several languages. Each emits a self-contained, idiomatic module — a typed value object, a loader that reads the injected __VARLOCK_ENV blob, and a SENSITIVE_KEYS constant (so you can build your own redaction or leak-scanning) — usable out of the box, no hand-rolled parsing:
| Language | Decorator | Loader | Guide |
|---|---|---|---|
| Python | @generatePythonEnv | load_env() | Python |
| Rust | @generateRustEnv | env::load() | Rust |
| Go | @generateGoEnv | env.Load() | Go |
| PHP | @generatePhpEnv | Env::load() | PHP |
For a language without a generated module, see Other languages.
Disabling automatic generation
Section titled “Disabling automatic generation”Set auto=false on any generator to skip it during load/run, then regenerate on demand with varlock codegen — useful for CI or a dedicated build step:
# @generateTsTypes(path=./env.d.ts, auto=false)Extending with plugins
Section titled “Extending with plugins”Code generation is contributed through a registry, and plugins can register their own generators using the same mechanism the built-in ones use. A generator is a root decorator name plus a generate function that receives the resolved schema and returns file contents:
import { plugin, type CodeGeneratorDef } from 'varlock/plugin-lib';
const generateZodSchema: CodeGeneratorDef = { decoratorName: 'generateZodSchema', generate: ({ fields, outputPath }) => { // `fields` is the language-agnostic resolved schema (keys, coerced/raw types, // required/sensitive flags, docs). Return the source to write to `outputPath`. const lines = fields.map((f) => ` ${f.key}: z.string(),`); return `import { z } from 'zod';\n\nexport const envSchema = z.object({\n${lines.join('\n')}\n});\n`; },};
plugin.registerCodeGenerator(generateZodSchema);Once registered, users trigger it from their schema like any other generator:
# @plugin(my-plugin)# @generateZodSchema(path=./env.schema.ts)The decoratorName must start with generate (e.g. generateZodSchema) — a shared convention that keeps code-generation decorators recognizable and separate from behavior decorators. Generators also get the common path, auto, and executeWhenImported args automatically — your generate function only handles turning the schema into code.
Plugins that register custom data types can also declare coercedType — what their coerce() function outputs ('string', 'int', 'number', 'boolean', 'object', or { enum: [...] }) — so generated code types those fields correctly in every language:
plugin.registerDataType({ name: 'retryCount', coercedType: 'int', coerce: (val) => parseInt(val, 10),});Data types that don’t declare a coercedType are treated as strings.