@@ -68,44 +68,6 @@ const getSearchOptions = (options: FileLoaderOptions) => {
6868 } ;
6969} ;
7070
71- /**
72- * Will fill in some placeholders.
73- * @param template - Text with placeholders for `data` properties.
74- * @param data - Data to interpolate into `template`.
75- *
76- * @example
77- ```
78- placeholderResolver('Hello ${name}', {
79- name: 'John',
80- });
81- //=> 'Hello John'
82- ```
83- */
84- const placeholderResolver = (
85- template : string ,
86- data : Record < string , any > ,
87- disallowUndefinedEnvironmentVariables : boolean ,
88- ) : string => {
89- const replace = ( placeholder : any , key : string ) => {
90- let value = data ;
91- for ( const property of key . split ( '.' ) ) {
92- value = value [ property ] ;
93- }
94-
95- if ( ! value && disallowUndefinedEnvironmentVariables ) {
96- throw new Error (
97- `Environment variable is not set for variable name: '${ key } '` ,
98- ) ;
99- }
100- return String ( value ) ;
101- } ;
102-
103- // The regex tries to match either a number inside `${{ }}` or a valid JS identifier or key path.
104- const braceRegex = / \$ { ( \d + | [ a - z $ _ ] [ \w \- $ ] * ?(?: \. [ \w \- $ ] * ?) * ?) } / gi;
105-
106- return template . replace ( braceRegex , replace ) ;
107- } ;
108-
10971/**
11072 * File loader loads configuration with `cosmiconfig` from file system.
11173 *
@@ -126,7 +88,7 @@ export const fileLoader = (
12688 cosmiconfig = loadPackage ( 'cosmiconfig' , 'fileLoader' ) ;
12789
12890 const { cosmiconfigSync } = cosmiconfig ;
129- return ( ) : Record < string , any > => {
91+ return ( additionalContext : Record < string , any > = { } ) : Record < string , any > => {
13092 const { searchPlaces, searchFrom } = getSearchOptions ( options ) ;
13193 const loaders = {
13294 '.toml' : loadToml ,
@@ -136,6 +98,31 @@ export const fileLoader = (
13698 searchPlaces,
13799 ...options ,
138100 loaders,
101+ transform : ( result : Record < string , any > ) => {
102+ if ( options . ignoreEnvironmentVariableSubstitution ?? true ) {
103+ return result ;
104+ }
105+
106+ try {
107+ const updatedResult = transformFileLoaderResult (
108+ result . config ,
109+ {
110+ // additionalContext must be first so the actual config will override it
111+ ...additionalContext ,
112+ ...result . config ,
113+ ...process . env ,
114+ } ,
115+ options . disallowUndefinedEnvironmentVariables ?? true ,
116+ ) ;
117+
118+ result . config = updatedResult ;
119+ return result ;
120+ } catch ( error : any ) {
121+ // enrich error with options information
122+ error . details = options ;
123+ throw error ;
124+ }
125+ } ,
139126 } ) ;
140127 const result = explorer . search ( searchFrom ) ;
141128
@@ -147,17 +134,132 @@ export const fileLoader = (
147134 `File-loader has loaded a configuration file from ${ result . filepath } ` ,
148135 ) ;
149136
150- let config = result . config ;
137+ return result . config ;
138+ } ;
139+ } ;
140+
141+ function resolveReference (
142+ reference : string ,
143+ currentContext : Record < string , any > ,
144+ ) : Record < string , any > | string | number | null {
145+ const parts = reference . split ( '.' ) ;
146+ let value = currentContext ;
151147
152- if ( ! ( options . ignoreEnvironmentVariableSubstitution ?? true ) ) {
153- const replacedConfig = placeholderResolver (
154- JSON . stringify ( result . config ) ,
155- process . env ,
156- options . disallowUndefinedEnvironmentVariables ?? true ,
157- ) ;
158- config = JSON . parse ( replacedConfig ) ;
148+ for ( const part of parts ) {
149+ value = value [ part ] ;
150+ if ( value === undefined ) {
151+ return null ;
159152 }
153+ }
160154
161- return config ;
162- } ;
163- } ;
155+ return value ;
156+ }
157+
158+ function transformFileLoaderResult (
159+ obj : Record < string , any > | string | number ,
160+ context : Record < string , any > ,
161+ disallowUndefinedEnvironmentVariables : boolean ,
162+ visited = new Set < Record < string , any > | string | number > ( ) ,
163+ ) : Record < string , any > | string | number {
164+ if ( typeof obj === 'string' ) {
165+ const match = obj . match ( / \$ \{ ( .+ ?) \} / g) ;
166+ if ( match ) {
167+ for ( const placeholder of match ) {
168+ const variable = placeholder . slice ( 2 , - 1 ) ;
169+ let resolvedValue = resolveReference ( variable , context ) ;
170+
171+ if ( obj === resolvedValue ) {
172+ throw new Error (
173+ `Circular self reference detected: ${ obj } -> ${ resolvedValue } ` ,
174+ ) ;
175+ }
176+
177+ if ( resolvedValue !== null ) {
178+ if ( typeof resolvedValue === 'string' ) {
179+ // resolve reference first
180+ if ( resolvedValue . match ( / \$ \{ ( .+ ?) \} / ) ) {
181+ try {
182+ resolvedValue = transformFileLoaderResult (
183+ resolvedValue ,
184+ context ,
185+ disallowUndefinedEnvironmentVariables ,
186+ visited ,
187+ ) ;
188+ } catch ( error ) {
189+ if ( error instanceof RangeError ) {
190+ debug (
191+ `Can not resolve a circular reference in ${ obj } -> ${ resolvedValue } -> ${ error . message } ` ,
192+ error ,
193+ ) ;
194+ }
195+
196+ throw error ;
197+ }
198+ }
199+
200+ obj = obj . toString ( ) . replace ( placeholder , resolvedValue . toString ( ) ) ;
201+ } else if ( typeof resolvedValue === 'object' ) {
202+ obj = transformFileLoaderResult (
203+ obj ,
204+ resolvedValue ,
205+ disallowUndefinedEnvironmentVariables ,
206+ visited ,
207+ ) ;
208+ } else if (
209+ typeof resolvedValue === 'number' ||
210+ typeof resolvedValue === 'boolean'
211+ ) {
212+ // if it's one to one reference, just return it as a number
213+ if ( obj === placeholder ) {
214+ obj = resolvedValue ;
215+ } else {
216+ // this means that we're embedding some number into string
217+ obj = obj
218+ . toString ( )
219+ . replace ( placeholder , resolvedValue . toString ( ) ) ;
220+ }
221+ }
222+ } else if ( disallowUndefinedEnvironmentVariables ) {
223+ throw new Error (
224+ `Environment variable is not set for variable name: '${ variable } '` ,
225+ ) ;
226+ }
227+ }
228+ }
229+ } else if ( typeof obj === 'number' || typeof obj === 'boolean' ) {
230+ return obj ;
231+ } else if ( typeof obj === 'object' && obj !== null ) {
232+ /**
233+ * it's not really possible to have circular reference in JSON and yaml like this
234+ * but probably one day there will be more complex scenarios for this function
235+ */
236+ /* istanbul ignore next */
237+ if ( visited . has ( obj ) ) {
238+ return obj ; // Avoid infinite loops on circular references
239+ }
240+ visited . add ( obj ) ;
241+
242+ if ( Array . isArray ( obj ) ) {
243+ for ( let i = 0 ; i < obj . length ; i ++ ) {
244+ obj [ i ] = transformFileLoaderResult (
245+ obj [ i ] ,
246+ context ,
247+ disallowUndefinedEnvironmentVariables ,
248+ visited ,
249+ ) ;
250+ }
251+ } else {
252+ for ( const key in obj ) {
253+ obj [ key ] = transformFileLoaderResult (
254+ obj [ key ] ,
255+ context ,
256+ disallowUndefinedEnvironmentVariables ,
257+ visited ,
258+ ) ;
259+ }
260+ }
261+
262+ visited . delete ( obj ) ;
263+ }
264+ return obj ;
265+ }
0 commit comments