Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions packages/varlock/src/env-graph/lib/config-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,18 +269,31 @@ export class ConfigItem {
* special early resolution helper
* currently used to resolve the envFlag before everything else has been loaded
* */
async earlyResolve() {
async earlyResolve(resolving?: Set<string>) {
if (this.isResolved) return;

// Cycle detection: track which items are currently in the resolution chain
// (mirrors the recursionStack pattern in findGraphCycles)
const resolvingSet = resolving ?? new Set<string>();
if (resolvingSet.has(this.key)) {
throw new SchemaError(
`Circular dependency detected during early resolution: ${this.key}`,
);
}
resolvingSet.add(this.key);

await this.process();

// process and resolve any other items our env flag depends on
for (const depKey of this.dependencyKeys) {
const depItem = this.envGraph.configSchema[depKey];
if (!depItem) {
throw new Error(`eager resolution eror - non-existant dependency: ${depKey}`);
throw new Error(`eager resolution error - non-existent dependency: ${depKey}`);
}
await depItem.earlyResolve();
await depItem.earlyResolve(resolvingSet);
}
await this.resolve();
resolvingSet.delete(this.key);
}

_isRequired: boolean = true;
Expand Down
9 changes: 8 additions & 1 deletion packages/varlock/src/env-graph/lib/data-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,14 @@ export class DirectoryDataSource extends EnvGraphDataSource {
}
const envFlagItem = this.graph.configSchema[envFlagKey];
if (envFlagItem) {
if (!envFlagItem.resolvedValue) await envFlagItem.earlyResolve();
if (!envFlagItem.resolvedValue) {
try {
await envFlagItem.earlyResolve();
} catch (err) {
this._loadingError = err instanceof Error ? err : new Error(String(err));
return;
}
}
currentEnv = envFlagItem.resolvedValue?.toString();
}
}
Expand Down
42 changes: 42 additions & 0 deletions packages/varlock/src/env-graph/test/environments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,48 @@ describe('@currentEnv and .env.* file loading logic', () => {
});
});

describe('earlyResolve cycle detection', () => {
test('self-referencing env flag triggers loading error', envFilesTest({
files: {
'.env.schema': outdent`
# @currentEnv=$APP_ENV
# ---
APP_ENV=$APP_ENV
`,
},
loadingError: true,
}));

test('indirect cycle via env flag triggers loading error', envFilesTest({
files: {
'.env.schema': outdent`
# @currentEnv=$APP_ENV
# ---
APP_ENV=$OTHER
OTHER=$APP_ENV
`,
},
loadingError: true,
}));

test('diamond dependency (no cycle) in env flag resolves correctly', envFilesTest({
files: {
'.env.schema': outdent`
# @currentEnv=$APP_ENV
# ---
APP_ENV=fallback($A, $B)
A=fallback($SHARED, a-default)
B=fallback($SHARED, b-default)
SHARED=dev
`,
},
expectValues: {
APP_ENV: 'dev',
SHARED: 'dev',
},
}));
});

describe('multiple data-source handling', () => {
test('undefined handling for overriding values', envFilesTest({
files: {
Expand Down
36 changes: 36 additions & 0 deletions varlock-env-info.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
=== Environment ===
Date: Mon 16 Mar 2026 05:32:31 AM UTC
Node: v22.22.1
npm: 10.9.4
pnpm: 10.28.1
OS: Linux 6.12.73+deb13-amd64 x86_64
Memory: total used free shared buff/cache available
Mem: 7.8Gi 5.7Gi 1.0Gi 1.1Gi 2.4Gi 2.1Gi

=== Varlock version ===
0.4.1

=== Varlock package.json ===
not found

=== .env.schema head ===
# @currentEnv=$APP_ENV
# @defaultSensitive=inferFromPrefix(VITE_)
# @defaultRequired=infer
# @generateTypes(lang=ts, path=./src/varlock-env.d.ts)
# ---

# === Environment ===

# Runtime environment selector for the demo app.
# Falls back to varlock's auto-detected env (Vercel, CI, branch signals).
# @public @type=enum(development, staging, preview, production, test)
APP_ENV=fallback($APP_ENV, $VARLOCK_ENV, "development")

# === Supabase ===

# Browser-safe Supabase project URL.
# @public @required @type=url
# @docs("Supabase JS client", https://supabase.com/docs/reference/javascript/initializing)
VITE_SUPABASE_URL=https://uwxumjrejpkwwzgvrjbv.supabase.co

20 changes: 20 additions & 0 deletions varlock-oom-trace-1.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

<--- Last few GCs --->

[1040829:0x7b60000] 8214 ms: Scavenge 505.6 (519.1) -> 502.4 (519.6) MB, pooled: 0 MB, 3.17 / 0.00 ms (average mu = 0.260, current mu = 0.251) allocation failure;
[1040829:0x7b60000] 8600 ms: Mark-Compact (reduce) 504.4 (519.6) -> 502.9 (513.6) MB, pooled: 0 MB, 376.38 / 0.00 ms (+ 5.5 ms in 0 steps since start of marking, biggest step 0.0 ms, walltime since start of marking 386 ms) (average mu = 0.206, curren

<--- JS stacktrace --->

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
----- Native stack trace -----

1: 0xe42d60 node::OOMErrorHandler(char const*, v8::OOMDetails const&) [node]
2: 0x121ddd0 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [node]
3: 0x121e0a7 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [node]
4: 0x144ba05 [node]
5: 0x144ba33 [node]
6: 0x1464b0a [node]
7: 0x1467cd8 [node]
8: 0x1ccd051 [node]
Aborted
20 changes: 20 additions & 0 deletions varlock-oom-trace-2.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

<--- Last few GCs --->

[1041128:0x16156000] 4025 ms: Scavenge (reduce) (interleaved) 254.2 (263.4) -> 250.7 (256.6) MB, pooled: 0 MB, 5.17 / 0.00 ms (average mu = 0.204, current mu = 0.177) allocation failure;
[1041128:0x16156000] 4185 ms: Mark-Compact (reduce) 250.9 (256.6) -> 250.8 (256.9) MB, pooled: 0 MB, 159.91 / 0.00 ms (+ 1.7 ms in 0 steps since start of marking, biggest step 0.0 ms, walltime since start of marking 173 ms) (average mu = 0.192, curre

<--- JS stacktrace --->

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
----- Native stack trace -----

1: 0xe42d60 node::OOMErrorHandler(char const*, v8::OOMDetails const&) [node]
2: 0x121ddd0 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [node]
3: 0x121e0a7 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [node]
4: 0x144ba05 [node]
5: 0x144ba33 [node]
6: 0x1464b0a [node]
7: 0x1467cd8 [node]
8: 0x1ccd051 [node]
Aborted
3 changes: 3 additions & 0 deletions varlock-post-fix-trace.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
🚨 Error encountered while loading directory - /home/claude/code/goalserve-poc/apps/demo-pwa

Circular dependency detected during early resolution: APP_ENV