Skip to content

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:

.env.schema
# @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.

@generateTsTypes is the default for JavaScript/TypeScript projects. It makes import { ENV } from 'varlock/env' fully typed, and augments process.env / import.meta.env:

.env.schema
# @generateTsTypes(path=./env.d.ts)

You can control what it emits:

  • exposeEnv: where the coerced ENV object lives: global (default, augments varlock/env), local (export a package-local ENV from the generated file, ideal for monorepos), or none.
  • processEnv / importMetaEnv: strict (only defined keys), loose (also allow extra keys), or none (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.

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). No hand-rolled parsing required:

LanguageDecoratorLoaderGuide
Python@generatePythonEnvload_env()Python
Rust@generateRustEnvenv::load()Rust
Go@generateGoEnvenv.Load()Go
PHP@generatePhpEnvEnv::load()PHP

For a language without a generated module, see Other languages.

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:

.env.schema
# @generateTsTypes(path=./env.d.ts, auto=false)

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:

my-plugin.ts
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:

.env.schema
# @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, so 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:

my-plugin.ts
plugin.registerDataType({
name: 'retryCount',
coercedType: 'int',
coerce: (val) => parseInt(val, 10),
});

Data types that don’t declare a coercedType are treated as strings.