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).
  • icons: set false to leave item icons out of the generated JSDoc. Icons for built-in data types and official plugins ship with varlock, but custom @icon values are fetched from Iconify during generation, so turn this off if the generated file must not depend on network access (for example, a committed file checked for drift in CI).

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.

The Effect plugin generates an Effect Config module with typed values, optional fields, and redacted secrets. Effect 3 and Effect 4 are both supported; the plugin detects which one is installed:

.env.schema
# @plugin(@varlock/effect-plugin)
# @generateEffectConfig(path=./src/env.generated.ts)

Install @varlock/effect-plugin and effect first. See the Effect guide for setup, usage, and migration from varlock-effect-plugin.

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
Java@generateJavaEnvEnv.load()Java
C#@generateCsharpEnvEnv.Load()C#

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.

TypeScript generation embeds each data type’s icon in the JSDoc for matching items. Data types (and resolvers) that don’t set an icon use your plugin’s plugin.icon. Icons varlock doesn’t have locally are fetched from Iconify at generation time. To avoid that network request, ship the SVGs with your plugin, keyed by Iconify name:

my-plugin.ts
plugin.icon = 'mdi:shield-key';
// no `icon` here, so it uses the plugin icon
plugin.registerDataType({
name: 'myServiceToken',
// ...
});
plugin.bundledIcons = {
'mdi:shield-key': '<svg xmlns="http://www.w3.org/2000/svg" ...>...</svg>',
};

Use the SVG Iconify serves at a height of 20 (https://api.iconify.design/mdi/shield-key.svg?height=20) so the output matches a fetched icon, and use currentColor for fills and strokes so the icon follows the editor theme like the built-in ones. You can also bundle icons that are not on Iconify under any name your data types use. Older varlock versions ignore bundledIcons, so setting it is safe.

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.