From 1d982fc3ffd1775c08bf90a49479c2282915fc16 Mon Sep 17 00:00:00 2001 From: Alex Renoki Date: Sun, 19 Oct 2025 23:31:43 +0300 Subject: [PATCH 1/2] wip(fn): replace fn wip --- .cursor/rules/varlock.mdc | 1 + .../env-spec-parser/src/simple-resolver.ts | 10 ++++ .../env-spec-parser/test/functions.test.ts | 56 +++++++++++++++++++ packages/varlock/env-graph/lib/resolver.ts | 36 ++++++++++++ 4 files changed, 103 insertions(+) diff --git a/.cursor/rules/varlock.mdc b/.cursor/rules/varlock.mdc index c1953760..29237885 100644 --- a/.cursor/rules/varlock.mdc +++ b/.cursor/rules/varlock.mdc @@ -76,6 +76,7 @@ actions: - concat() - For concatenated values - exec() - For command output - ref() - For referencing other variables + - replace() - For ref()-ing a variable and find-and-replace examples: - input: | diff --git a/packages/env-spec-parser/src/simple-resolver.ts b/packages/env-spec-parser/src/simple-resolver.ts index fc28579e..62c8b89c 100644 --- a/packages/env-spec-parser/src/simple-resolver.ts +++ b/packages/env-spec-parser/src/simple-resolver.ts @@ -28,6 +28,16 @@ export function simpleResolver( } else { throw new Error('Invalid `ref` args'); } + } else if (valOrFn.name === 'replace') { + const args = valOrFn.simplifiedArgs; + if (Array.isArray(args)) { + const str = args[0]; + const search = args[1]; + const replace = args[2]; + return str.replace(search, replace); + } else { + throw new Error('Invalid `replace` args'); + } } else if (valOrFn.name === 'concat') { const args = valOrFn.data.args.values; const resolvedArgs = args.map((i) => { diff --git a/packages/env-spec-parser/test/functions.test.ts b/packages/env-spec-parser/test/functions.test.ts index a8917853..6e06b9b3 100644 --- a/packages/env-spec-parser/test/functions.test.ts +++ b/packages/env-spec-parser/test/functions.test.ts @@ -58,10 +58,21 @@ describe('function calls', functionValueTests({ ITEM: 'foo-val', }, }, + 'replace()': { + input: 'ITEM=replace("foo", "f", "b")', + expected: { ITEM: 'boo' }, + }, 'nested function calls - array': { input: 'OTHERVAL=d\nITEM=concat("a", fallback("", "b"), exec("echo c"), ref(OTHERVAL))', expected: { ITEM: 'abcd' }, }, + 'nested function calls - replace()': { + input: 'FOO=foo-val\nITEM=replace(ref("FOO"), "f", "b")', + expected: { + FOO: 'foo-val', + ITEM: 'boo-val', + }, + }, 'nested function calls - key/value': { input: 'ITEM=remap("foo", zzz=aaa, bar=fallback("", "foo"))', expected: { ITEM: 'bar' }, @@ -135,6 +146,51 @@ describe('ref expansion', functionValueTests({ }, })); +describe('replace expansion', functionValueTests({ + 'ref expansion - unquoted': { + input: 'OTHER=foo\nITEM=replace($OTHER, "f", "b")', + expected: { + OTHER: 'foo', + ITEM: 'boo', + }, + }, + 'ref expansion within quotes - double quotes': { + input: 'OTHER=foo\nITEM="${replace($OTHER, "f", "b")}"', + expected: { + OTHER: 'foo', + ITEM: 'boo', + }, + }, + 'ref expansion within quotes - backtick': { + input: 'OTHER=foo\nITEM=`${replace($OTHER, "f", "b")}`', + expected: { + OTHER: 'foo', + ITEM: 'boo', + }, + }, + 'ref expansion within quotes - single quotes (NOT EXPANDED)': { + input: "OTHER=foo\nITEM='${replace($OTHER, 'f', 'b')}'", + expected: { + OTHER: 'foo', + ITEM: '${replace($OTHER, "f", "b")}', + }, + }, + 'ref expansion - simple (no brackets)': { + input: 'OTHER=foo\nITEM=replace($OTHER, "f", "b")', + expected: { + OTHER: 'foo', + ITEM: 'boo', + }, + }, + 'ref expansion - with brackets': { + input: 'FOO=foo\nITEM=replace($FOO, "f", "b")', + expected: { + FOO: 'foo', + ITEM: 'boo', + }, + }, +})); + describe('complex cases', functionValueTests({ 'multiple expansions': { input: 'FOO=foo\nBAR=bar\nITEM=${FOO}-$BAR-$(echo baz)-${UNDEF:-qux}', diff --git a/packages/varlock/env-graph/lib/resolver.ts b/packages/varlock/env-graph/lib/resolver.ts index 88271a27..061be6f4 100644 --- a/packages/varlock/env-graph/lib/resolver.ts +++ b/packages/varlock/env-graph/lib/resolver.ts @@ -251,6 +251,42 @@ export class RefResolver extends Resolver { } } +export class ReplaceResolver extends Resolver { + static fnName = 'replace'; + label = 'replace'; + icon = 'mdi-light:content-duplicate'; + + private originalString?: string; + private searchString?: string; + private replacementString?: string; + + async _process() { + if (this.fnArgs.length !== 1) { + throw new SchemaError('replace() expects three child args'); + } + if (!(this.fnArgs[0] instanceof StaticValueResolver)) { + throw new SchemaError('replace() expects the first arg to be the original string'); + } + if (!(this.fnArgs[1] instanceof StaticValueResolver)) { + throw new SchemaError('replace() expects the second arg to be the string to search for'); + } + if (!(this.fnArgs[2] instanceof StaticValueResolver)) { + throw new SchemaError('replace() expects the third arg to be the replacement string'); + } + + this.originalString = String(await this.fnArgs[0].resolve()); + this.searchString = String(await this.fnArgs[1].resolve()); + this.replacementString = String(await this.fnArgs[2].resolve()); + } + + protected async _resolve() { + if (!this.originalString) throw new Error('expected originalString to be set'); + if (!this.searchString) throw new Error('expected searchString to be set'); + if (!this.replacementString) throw new Error('expected replacementString to be set'); + return this.originalString.replace(this.searchString, this.replacementString); + } +} + // regex() is only used internally as function args to be used by other functions // we will check final resoled values to make sure they are not regexes export class RegexResolver extends Resolver { From 49e798d7d6490581e846aad381dd72eef9ff3adc Mon Sep 17 00:00:00 2001 From: Alex Renoki Date: Sun, 19 Oct 2025 23:33:47 +0300 Subject: [PATCH 2/2] wip(fn): changeset --- .changeset/flat-turkeys-win.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/flat-turkeys-win.md diff --git a/.changeset/flat-turkeys-win.md b/.changeset/flat-turkeys-win.md new file mode 100644 index 00000000..1985f2a2 --- /dev/null +++ b/.changeset/flat-turkeys-win.md @@ -0,0 +1,6 @@ +--- +"@env-spec/parser": patch +"varlock": patch +--- + +added `replace()` function to replace a string with support for ref()