Repository navigation
Conversation
`new EnvGraph({ mode: 'shape' })` builds a graph for tools that only
inspect a schema (type checkers, language servers, CI linters):
- only resolvers marked `pure: true` run, all others resolve to undefined
- an item whose value depends on a skipped resolver, directly or through
references, is unknown: it is left unset and is not coerced, validated or
reported as a missing or failed value
- a @disable or @import condition that is unknown keeps the file enabled
- root decorator `execute()` is skipped (the items they reference are still
resolved, so they keep their final validation state)
- no cache store is created
- plugins that are not installed or cached are never downloaded
- process.env overrides are ignored
Skipped work is recorded in `graph.shapeSkipped`. Default mode is
unchanged. The built-in resolvers that only read other values (concat, ref,
fallback, forEnv, if, ...) and the builtin VARLOCK_* vars are marked pure;
VARLOCK_BRANCH skips its local `git branch` lookup in shape mode.
Documents shape mode for tool authors, including the `pure` flag for plugin resolvers.
- builtin variables no longer read the real shell env (an env passed in explicitly is still used), so CI variables cannot change results - generated code is only written inside the root schema's directory; a path that leaves it (.., absolute, or through a symlink) is a schema error - an import that is not a regular file (a named pipe) is a loading error instead of a read that never returns - guide: documents these, that plugin pure flags are trusted like the plugin code that already runs, and that schema regexes have no timeout Tests for each fail on the previous commit.
…known parts - the named-pipe check moves from the direct-import branch to FileBasedDataSource, so a pipe found through a directory import, or the root schema itself, is refused too instead of hanging the load - an error is only treated as caused by a partial value when the resolver it was first thrown from had an unknown input, so an error from a fully known part of the same expression is still reported - the CI-branch test also checks that git is not run Tests for the pipe cases and the mixed expression fail on the previous commit.
- ConfigItem keeps the unknown flag computed when its value resolver ran, so each dependent read no longer walks the resolver tree - FileBasedDataSource does one stat instead of an access check plus a stat
…lk values A file kept only because its @import enabled= or @disable condition could not be evaluated might be skipped entirely by a normal load, so a @plugin in it (or in a file it imports) is no longer loaded. @setValuesBulk now runs in shape mode: it only resolves its args and edits the graph. Data written in the schema is applied as in a normal load. When the data or its enabled= condition comes from a skipped resolver, the items it could set are unknown while empty instead of failing the required check. A local plugin whose package.json is a named pipe or device is refused in both modes instead of blocking the read forever.
… project folder In shape mode the loader does not clear or open a cache store.
… on a skipped value as unknown A computed @required/@optional or @sensitive/@public built from a skipped resolver is now unknown instead of whatever the empty value happened to give: the item is treated as optional and sensitive, and isRequiredUnknownInShapeLoad / isSensitiveUnknownInShapeLoad say so. forEnv() with an unknown @currentEnv is unknown instead of an error, so a @disable or @required built on it no longer fails the load. Checks that only hold for a sensitive item are skipped while sensitivity is unknown. Default mode and generated types are unchanged.
finishLoad returned before the cycle check when any item had a schema error. An item on a cycle was then left unmarked, waited on itself when values were resolved, and the CLI gave up with "varlock stopped before the command finished" instead of showing either error.
…ith a generated soundness test A shape load now reports an item as unknown, instead of as valid, when one of its dependencies is unknown (a normal load fails an item whenever a dependency is invalid, read or not), when it is empty and its requiredness is unknown, or when its sensitivity is unknown and a sensitive-only check would fail. shape-soundness.test.ts generates 1,200 schemas and checks, for every item, that a shape load says unknown or gives exactly the value and errors of a normal load, and the same required and sensitive unless flagged unknown. It fails on every seed without these rules.
…d list; classify every built-in resolver A named pipe or device is skipped and recorded (kind nonRegularFile) instead of failing the load: 1Password mounts a .env that way. It could set any item, so every value is then unknown. shapeSkipped no longer lists root decorators that have no execute(). A test lists every built-in resolver as pure or skipped, so a new one fails until someone decides. Also: resolution-context is imported once, and errors from unknown inputs are tracked in a WeakSet instead of a property written onto the error.
packages/ts-content-mapper (@varlock/ts-content-mapper, private) types
`import { ENV } from 'varlock/env'` straight from .env.schema while
`tsc --runExternalCode` runs, with no generated env.d.ts. It loads the
schema with EnvGraph shape mode, so exec(), secret stores, root
decorator execute() and plugin downloads never run during a type check.
example/ is a small workspace app wired through `contentMappers` in its
tsconfig. `bunx turbo run check --filter=@varlock/ts-content-mapper-example`
builds varlock and the mapper and type-checks it.
The tests run the real tsc against the built mapper: correct types (with a
control that the assertions fail on a changed schema), wrong-typed app
code, schema errors located in .env.schema, exec() never run, no network
request, and sensitive values kept out of the virtual code.
A regex like ^(a+)+$ against a long value would stall every type check. The mapper process now turns on V8's fallback to its non-backtracking engine after excessive backtracking. Patterns with lookaround or backreferences are not covered. Process-wide, which is fine in a process that only loads schemas.
theoephraim
force-pushed
the
ts-content-mapper-v2
branch
from
November 13, 2026 00:47
f340857 to
9d957fd
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
stacked on #1289. only the last 2 commits are new, all under
packages/ts-content-mapper.TypeScript starts the mapper, the mapper does a shape load of
.env.schemaand hands back types.what that takes care of
no
env.d.ts: nothing to commit, forget to regenerate, or collide on in a monorepowatch mode ([Feature Request] Watch mode for type generation #925) for free. plain
tsc --watch, then edit the schema:the schema becomes part of the program: hover, go to definition, rename and find references go from
ENV.PORTin code to the line in.env.schemano command runs and no secret is fetched during a type check, because it's a shape load. that matters for the same reason as in the shape mode PR: a type checker runs on whatever folder you open
a schema regex written to backtrack forever can't stall the type check (the mapper turns on V8's non-backtracking fallback for its own process)
not for merging yet
7.1.0-betaand the nov 9 nightlytsc --runExternalCodecontentMappers(a fix is open on their side)ts-content-mapper0.1.0)env.d.tssilently wins over the mapperthe package is
private: true. it's here so you can try it, not to ship it.