diff --git a/.changeset/fix-standard-vars-warning.md b/.changeset/fix-standard-vars-warning.md new file mode 100644 index 00000000..3b31ef34 --- /dev/null +++ b/.changeset/fix-standard-vars-warning.md @@ -0,0 +1,5 @@ +--- +"varlock": patch +--- + +Fix false warning 'found in environment but not connected to plugin' when standard vars are already wired via init decorator (e.g. `@initOp(token=$OP_SERVICE_ACCOUNT_TOKEN)`) diff --git a/packages/varlock/src/env-graph/lib/env-graph.ts b/packages/varlock/src/env-graph/lib/env-graph.ts index 2d4ca47e..62c27256 100644 --- a/packages/varlock/src/env-graph/lib/env-graph.ts +++ b/packages/varlock/src/env-graph/lib/env-graph.ts @@ -337,11 +337,6 @@ export class EnvGraph { if (plugin.loadingError) return; } - // check declared standardVars against the environment - for (const plugin of this.plugins) { - plugin._checkStandardVars(this); - } - // Attach builtin defs to any user-defined VARLOCK_* items // (they may have been defined directly without a $VARLOCK_* reference) for (const key in this.configSchema) { @@ -358,6 +353,12 @@ export class EnvGraph { } } + // check declared standardVars against the environment + // (runs after root decorator processing so decValueResolver.deps is available) + for (const plugin of this.plugins) { + plugin._checkStandardVars(this); + } + // process config items // checks decorators, sets data type, checks resolver args, adds deps for (const itemKey in this.configSchema) { diff --git a/packages/varlock/src/env-graph/lib/plugins.ts b/packages/varlock/src/env-graph/lib/plugins.ts index 6c79ffb7..89889f9c 100644 --- a/packages/varlock/src/env-graph/lib/plugins.ts +++ b/packages/varlock/src/env-graph/lib/plugins.ts @@ -247,7 +247,15 @@ export class VarlockPlugin { }; /** called by the loading infrastructure — checks declared standardVars against the graph */ - _checkStandardVars(graph: { overrideValues: Record, configSchema: Record }) { + _checkStandardVars(graph: { + overrideValues: Record, + configSchema: Record, + sortedDataSources: Iterable<{ + rootDecorators: Array<{ + name: string, isFunctionCall: boolean, decValueResolver?: { deps: Array }, + }>, + }>, + }) { if (!this.standardVars) return; const { initDecorator, params } = this.standardVars; @@ -260,7 +268,19 @@ export class VarlockPlugin { }; }); - const detected = resolved.filter((v) => v.matchedKey); + // collect config item keys already wired via init decorator instances + const initDecName = initDecorator.replace(/^@/, ''); + const wiredVarNames = new Set(); + for (const source of graph.sortedDataSources) { + for (const rootDec of source.rootDecorators) { + if (rootDec.name === initDecName && rootDec.isFunctionCall && rootDec.decValueResolver) { + for (const dep of rootDec.decValueResolver.deps) wiredVarNames.add(dep); + } + } + } + + // filter: only warn about vars detected in environment but NOT wired to the init decorator + const detected = resolved.filter((v) => v.matchedKey && !wiredVarNames.has(v.matchedKey)); if (detected.length === 0) return; const detectedKeys = detected.map((v) => v.matchedKey!); diff --git a/packages/varlock/src/env-graph/test/plugins.test.ts b/packages/varlock/src/env-graph/test/plugins.test.ts index 908cd397..bb1683c7 100644 --- a/packages/varlock/src/env-graph/test/plugins.test.ts +++ b/packages/varlock/src/env-graph/test/plugins.test.ts @@ -1,6 +1,10 @@ -import { describe, test } from 'vitest'; +import { + describe, test, expect, vi, +} from 'vitest'; +import path from 'node:path'; import outdent from 'outdent'; import { envFilesTest } from './helpers/generic-test'; +import { EnvGraph, DotEnvFileDataSource } from '../index'; describe('plugins ', () => { test('validate simple plugin works', envFilesTest({ @@ -94,4 +98,64 @@ describe('plugins ', () => { `, expectValues: { WARNED_ITEM: 'some_value', OTHER_ITEM: 'bar' }, })); + + describe('standardVars warnings', () => { + async function loadGraphWithPlugin(envFile: string, overrideValues: Record) { + const currentDir = path.dirname(expect.getState().testPath!); + vi.spyOn(process, 'cwd').mockReturnValue(currentDir); + const g = new EnvGraph(); + g.overrideValues = overrideValues; + const source = new DotEnvFileDataSource('.env.schema', { overrideContents: envFile }); + await g.setRootDataSource(source); + await g.finishLoad(); + return g; + } + + test('warns when standard var is in environment but not wired to init decorator', async () => { + const g = await loadGraphWithPlugin( + outdent` + # @plugin(./plugins/test-plugin-with-standard-vars/) + # @initTestStdVars() + # --- + MY_PLUGIN_TOKEN= + `, + { MY_PLUGIN_TOKEN: 'some-token-value' }, + ); + const plugin = g.plugins.find((p) => p.name === '@varlock/test-plugin-with-standard-vars'); + expect(plugin).toBeDefined(); + expect(plugin!.warnings.length).toBe(1); + expect(plugin!.warnings[0].message).toContain('MY_PLUGIN_TOKEN'); + expect(plugin!.warnings[0].message).toContain('not connected to plugin'); + }); + + test('no warning when standard var is wired via init decorator', async () => { + const g = await loadGraphWithPlugin( + outdent` + # @plugin(./plugins/test-plugin-with-standard-vars/) + # @initTestStdVars(token=$MY_PLUGIN_TOKEN) + # --- + MY_PLUGIN_TOKEN= + `, + { MY_PLUGIN_TOKEN: 'some-token-value' }, + ); + const plugin = g.plugins.find((p) => p.name === '@varlock/test-plugin-with-standard-vars'); + expect(plugin).toBeDefined(); + expect(plugin!.warnings.length).toBe(0); + }); + + test('no warning when standard var is not in environment', async () => { + const g = await loadGraphWithPlugin( + outdent` + # @plugin(./plugins/test-plugin-with-standard-vars/) + # @initTestStdVars() + # --- + MY_PLUGIN_TOKEN= + `, + {}, + ); + const plugin = g.plugins.find((p) => p.name === '@varlock/test-plugin-with-standard-vars'); + expect(plugin).toBeDefined(); + expect(plugin!.warnings.length).toBe(0); + }); + }); }); diff --git a/packages/varlock/src/env-graph/test/plugins/test-plugin-with-standard-vars/package.json b/packages/varlock/src/env-graph/test/plugins/test-plugin-with-standard-vars/package.json new file mode 100644 index 00000000..4e2aae61 --- /dev/null +++ b/packages/varlock/src/env-graph/test/plugins/test-plugin-with-standard-vars/package.json @@ -0,0 +1,10 @@ +{ + "name": "@varlock/test-plugin-with-standard-vars", + "private": true, + "description": "plugin used in automated tests - has standardVars and init decorator", + "version": "1.0.0", + "type": "module", + "exports": { + "./plugin": "./plugin.js" + } +} diff --git a/packages/varlock/src/env-graph/test/plugins/test-plugin-with-standard-vars/plugin.js b/packages/varlock/src/env-graph/test/plugins/test-plugin-with-standard-vars/plugin.js new file mode 100644 index 00000000..9cb9e372 --- /dev/null +++ b/packages/varlock/src/env-graph/test/plugins/test-plugin-with-standard-vars/plugin.js @@ -0,0 +1,27 @@ +const { plugin, SchemaError } = require('varlock/plugin-lib'); + +const instances = {}; + +plugin.name = 'test-plugin-with-standard-vars'; + +plugin.standardVars = { + initDecorator: '@initTestStdVars', + params: { + token: { key: 'MY_PLUGIN_TOKEN', dataType: 'myPluginToken' }, + }, +}; + +plugin.registerRootDecorator({ + name: 'initTestStdVars', + isFunction: true, + useFnArgsResolver: true, + process() { + const id = '_default'; + if (instances[id]) { + throw new SchemaError(`Instance with id "${id}" already initialized`); + } + instances[id] = true; + return { id }; + }, + execute() {}, +});