npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

varlock-effect-plugin

v0.5.0

Published

Generate Effect Config modules from Varlock schemas

Readme

varlock-effect-plugin

Generate an Effect 4 Config module from a Varlock environment schema.

Install

Install the plugin in the workspace that owns .env.schema. The generated module imports Effect modules, so that workspace must also depend on Effect.

This release requires effect@^4.0.0-rc.117. Generated modules use the PascalCase Config constructors (Config.String, Config.Literals, Config.mapEffect, ...) introduced in rc.114, so older release candidates are not supported.

bun add -d varlock-effect-plugin
bun add effect@^4.0.0-rc.117

Configure

# @plugin(varlock-effect-plugin)
# @generateEffectConfig(path=./src/env.generated.ts)
# ---
# @type=enum(development, staging, production) @public
APP_ENV=development

# @type=port @public
PORT=3000

# @type=array(string, format=json) @public
HOSTS='["localhost","example.com"]'

# @sensitive
API_TOKEN=

Generate the module explicitly:

bunx varlock codegen

Varlock also regenerates it during varlock load and varlock run unless the decorator uses auto=false.

Use

import { Effect } from "effect"

import { generated } from "./env.generated.js"

const program = Effect.gen(function* () {
  const env = yield* generated

  console.log(env.APP_ENV)
})

Effect.runPromise(program)

generated is an Effect that loads environment values when executed. Missing required values and decoding failures become defects, so consumers do not need to add Effect.orDie. Missing optional values remain Option.none(). Each execution reads the config again; yield it during startup to fail before starting your application.

The config export exposes the underlying Effect Config with typed Config.ConfigError failures for custom error handling, Config composition, or explicit providers via config.parse(provider).

Applications that want dependency injection can define their own service and layer:

import { Config, Context, Layer } from "effect"

import { config, generated } from "./env.generated.js"

export class Env extends Context.Service<Env, Config.Success<typeof config>>()("my-app/Env") {
  static readonly layer = Layer.effect(Env, generated)
}

Provide Env.layer at the application boundary and use const env = yield* Env in consumers. Use Layer.effect(Env, config) instead to retain typed failures during layer construction.

Run the application through Varlock so it validates and injects the environment first:

bunx varlock run -- bun run src/index.ts

The generator maps Varlock values to Effect as follows:

| Varlock schema | Generated Effect Config | | --- | --- | | string-like values | Config.String | | boolean | Config.Boolean | | int | Config.schema with Schema.Number checked by Number.isInteger | | number | Config.Number | | enum | Config.Literals([members], key) | | array, record, object | Config.schema(Schema.fromJsonString(Schema.Unknown), key), mapped to the generated type | | @sensitive | Config.map(config, Redacted.make) with sanitized errors | | optional | Config.option |

Composite values are parsed from Varlock's serialized JSON wire format. Varlock remains responsible for schema validation before it injects those values.

Arrays with scalar elements require format=json, as in @type=array(string, format=json). Varlock's default scalar-array format is separator-delimited. Arrays containing composite values and records already serialize as JSON.

Integer configs accept the same values as Number.isInteger, including integers outside the safe integer range, matching Effect 3 and Varlock. Fractional values, NaN, and infinities fail.

Sensitive values are wrapped after parsing. Optional sensitive values have type Option<Redacted<T>>. Sensitive failures retain the field name but replace their details with <redacted>, including enum literals and provider error causes. Missing optional secrets still become Option.none().

The config export reports loading and decoding failures as Config.ConfigError from effect/Config. The generated export converts those failures to defects after sanitizing sensitive errors. Invalid supplied values still fail even when the field is optional.

Schema values

The generated module preserves literal defaults and references from the schema, so config resolves them at runtime without Varlock having resolved the environment first:

APP_ORIGIN=http://localhost:3000
WORKOS_REDIRECT_URI=${APP_ORIGIN}/api/v1/auth/callback
  • Explicit values from the ConfigProvider take precedence over schema values.
  • Missing values use literal defaults declared in the schema.
  • ${VAR}, $VAR, ${VAR:-default}, ref(), concat(), and fallback() use the referenced key's effective runtime value: its explicit value when supplied, otherwise its own schema value. Chained references and imported schemas are followed.
  • With APP_ORIGIN=https://syncfast.dev injected, WORKOS_REDIRECT_URI derives https://syncfast.dev/api/v1/auth/callback. An explicit WORKOS_REDIRECT_URI overrides that.
  • Derived values are decoded with the field's own Config, so types, validation, Option, redaction, and error shapes are unchanged. Invalid explicit values still fail even when the schema declares a default.
  • Values are read through Effect's ConfigProvider, so custom providers work. Nothing reads process.env directly.

Generated modules declare one $KEY constant per key with a schema value, in reference order, and decode each field from it. Keys referenced by a generated field are declared even when a filter excludes them from the exported config.

Requires Varlock resolution

Only literals, references, interpolation, and fallback() are generated. Any schema value that uses another resolver keeps reading the injected value directly, and the generated module documents that with a comment on the field. This covers exec() and $(command) expansion, plugin resolvers such as awsSecret(), varlock(), remap(), if(), forEnv(), regex(), and object or array literals. A concat() or fallback() that contains such a value is not generated either. Run those schemas through varlock run or varlock load so Varlock resolves and injects the values.

A reference to a resolver-backed key is still generated: fallback(ref(DYNAMIC), "literal") reads the injected DYNAMIC value and falls back to the literal. Resolver expressions are never emitted as literal fallbacks.

Rules

  • Sensitive fields never receive literal defaults in the generated module, because that module is committed and bundled. References and interpolations that involve other keys are still generated, and literal options in a sensitive fallback() are dropped.
  • Generation only reads the parsed schema definitions that Varlock's own type generation uses: .env.schema and its imports. Values from .env, .env.local, environment-specific files, process.env, and resolver output are never read, so no secrets or developer environment values are captured.
  • Empty schema values (KEY= or KEY="") declare no default. A missing key inside an interpolation contributes an empty string, matching Varlock's concat(), and an interpolation that resolves to an empty string counts as missing. With preserveEmptyStrings, an explicit empty string wins over the schema value.
  • Single-quoted values are literal, matching Varlock.
  • Reference cycles, unresolvable references, and invalid expressions fail generation with an error that names only the keys involved.

Migrate existing generated modules

Regenerate with bunx varlock codegen. The generated export is now an Effect with no typed failures. Existing yield* generated calls keep working, and .pipe(Effect.orDie) is redundant. Use the new config export wherever you previously used generated.parse(...), Config combinators, Config.Success<typeof generated>, or typed config error handling. Use Effect.Success<typeof generated> to infer the loaded values from generated.

Migrate from Effect 3

This is a breaking change. Projects using Effect 3 should remain on [email protected]. To migrate, update the plugin, install the Effect 4 release shown above, then regenerate:

bunx varlock codegen

The @generateEffectConfig decorator and yield* generated syntax stay the same. The export and error-handling changes described above also apply. Regenerate existing modules before running them with Effect 4.

Effect 4's default environment provider treats empty strings as missing. A required empty value fails, and an optional empty value becomes Option.none(). Applications that need to preserve empty strings can supply a provider explicitly:

import { ConfigProvider, Effect } from "effect"

import { config } from "./env.generated.js"

Effect.runPromise(
  config.parse(ConfigProvider.fromEnv({ preserveEmptyStrings: true })).pipe(Effect.orDie),
)

Local development

bun install
bun run check

Publish

Sign in to npm once:

npm login
npm whoami

Then publish:

bun run release

The prepublishOnly hook runs typechecking, tests, and the production build before npm uploads the package. npm refuses to overwrite an existing version, so bump version in package.json before later releases.

License

MIT