configorama 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -870,14 +870,49 @@ const config = await configorama(configFile, {
870
870
  allowUnresolvedVariables: ['param', 'file'], // only these pass through
871
871
  options: { stage: 'prod' }
872
872
  })
873
- // Unresolved ${param:x} and ${file(missing.yml)} pass through
874
- // Unresolved ${env:MISSING} throws an error
873
+ // Input: { paramKey: '${param:x}', fileKey: '${file(missing.yml)}' }
874
+ // Output: { paramKey: '${param:x}', fileKey: '${file(missing.yml)}' }
875
+
876
+ // Allow only SPECIFIC types to be unresolved
877
+ const config = await configorama(configFile, {
878
+ allowUnresolvedVariables: ['param', 'file'], // only these pass through
879
+ options: { stage: 'prod' }
880
+ })
881
+ // Input: { key: '${env:MISSING_VAR}', paramKey: '${param:x}', fileKey: '${file(missing.yml)}' }
882
+ // Unresolved ${param:x} and ${file(missing.yml)} pass through but
883
+ // Output error thrown because ${env:MISSING_VAR} throws an error
875
884
  ```
876
885
 
877
- This is useful for multi-stage resolution (e.g., Serverless Dashboard resolves params after local resolution).
886
+ This is useful for multi-stage resolution (e.g., Downstream Serverless Dashboard resolves params after local resolution).
887
+
888
+ > **Note:** This option does NOT apply to `self:` or dotProp variables (e.g., `${foo.bar.baz}`). These are local references that configorama fully owns—if they can't be resolved, it's a config error, not something to defer to another system.
878
889
 
879
890
  ## FAQ
880
891
 
892
+ **Q: What happens with circular variable dependencies?**
893
+
894
+ Configorama detects circular dependencies and throws a helpful error instead of hanging forever.
895
+
896
+ ```yml
897
+ # Direct cycle - throws error
898
+ a: ${self:b}
899
+ b: ${self:a}
900
+ # Error: Circular variable dependency detected: b → a → b
901
+
902
+ # Indirect cycle - also detected
903
+ a: ${self:b}
904
+ b: ${self:c}
905
+ c: ${self:a}
906
+ # Error: Circular variable dependency detected: c → a → b → c
907
+
908
+ # Works with shorthand syntax too
909
+ foo:
910
+ bar: ${baz.qux}
911
+ baz:
912
+ qux: ${foo.bar}
913
+ # Error: Circular variable dependency detected: baz.qux → foo.bar → baz.qux
914
+ ```
915
+
881
916
  **Q: Why should I use this?**
882
917
 
883
918
  Never rendering a stale configuration file again!
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "configorama",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Variable support for configuration files",
5
5
  "main": "src/index.js",
6
6
  "types": "index.d.ts",
package/src/main.js CHANGED
@@ -121,6 +121,7 @@ class Configorama {
121
121
  allowUndefinedValues: false,
122
122
  // Allow known variable types that can't be resolved to pass through
123
123
  // Can be: false | true | ['param', 'file', 'env', ...]
124
+ // Note: Does not apply to self: or dotprop refs - those always error
124
125
  allowUnresolvedVariables: false,
125
126
  // Return metadata
126
127
  returnMetadata: false,
@@ -2767,6 +2768,25 @@ Missing Value ${missingValue} - ${matchedString}
2767
2768
 
2768
2769
  // console.log('getValueFromSrc propertyString', propertyString)
2769
2770
  // console.log(`tracker contains ${variableString}`, this.tracker.contains(variableString))
2771
+
2772
+ // Cycle detection: track dependencies and check for cycles
2773
+ const fromPath = valueObject.path ? valueObject.path.join('.') : null
2774
+ // Extract target path from variableString (e.g., 'self:b' → 'b', 'b.c' → 'b.c')
2775
+ let toPath = variableString
2776
+ if (variableString.startsWith('self:')) {
2777
+ toPath = variableString.slice(5)
2778
+ }
2779
+ // For cycle detection, only track self-references
2780
+ if (fromPath && (variableString.startsWith('self:') || !variableString.includes(':'))) {
2781
+ if (this.tracker.wouldCreateCycle(fromPath, toPath)) {
2782
+ const cyclePath = this.tracker.getCyclePath(fromPath, toPath)
2783
+ return Promise.reject(new Error(
2784
+ `Circular variable dependency detected: ${cyclePath.join(' → ')}`
2785
+ ))
2786
+ }
2787
+ this.tracker.addDependency(fromPath, toPath)
2788
+ }
2789
+
2770
2790
  if (this.tracker.contains(variableString)) {
2771
2791
  // console.log('try to get', variableString)
2772
2792
  return this.tracker.get(variableString, propertyString)
@@ -3253,6 +3273,7 @@ Missing Value ${missingValue} - ${matchedString}
3253
3273
  // console.log('self fixed deepProperties', deepProperties)
3254
3274
  }
3255
3275
  }
3276
+
3256
3277
  return this.getDeeperValue(deepProperties, valueToPopulate).then((res) => {
3257
3278
  /*
3258
3279
  console.log('self getDeeperValue variableString', variableString)
@@ -8,6 +8,8 @@ class PromiseTracker {
8
8
  reset() {
9
9
  this.promiseList = []
10
10
  this.promiseMap = {}
11
+ // Track which variables depend on which (for cycle detection)
12
+ this.dependencyGraph = {}
11
13
  this.startTime = Date.now()
12
14
  this.cursor = 0
13
15
  }
@@ -81,6 +83,58 @@ class PromiseTracker {
81
83
  promise.waitList += ` ${specifier}`
82
84
  return promise
83
85
  }
86
+ // Add a dependency edge: "from" depends on "to"
87
+ addDependency(from, to) {
88
+ if (!this.dependencyGraph[from]) {
89
+ this.dependencyGraph[from] = new Set()
90
+ }
91
+ this.dependencyGraph[from].add(to)
92
+ }
93
+ // Check if adding dependency from → to would create a cycle
94
+ wouldCreateCycle(from, to) {
95
+ // Check if "to" can reach "from" (meaning from → to would close a cycle)
96
+ const visited = new Set()
97
+ const stack = [to]
98
+ while (stack.length > 0) {
99
+ const current = stack.pop()
100
+ if (current === from) {
101
+ return true
102
+ }
103
+ if (visited.has(current)) {
104
+ continue
105
+ }
106
+ visited.add(current)
107
+ const deps = this.dependencyGraph[current]
108
+ if (deps) {
109
+ for (const dep of deps) {
110
+ stack.push(dep)
111
+ }
112
+ }
113
+ }
114
+ return false
115
+ }
116
+ // Get the cycle path for error reporting
117
+ getCyclePath(from, to) {
118
+ const path = [from, to]
119
+ const visited = new Set([from, to])
120
+ let current = to
121
+ while (current !== from) {
122
+ const deps = this.dependencyGraph[current]
123
+ if (!deps) break
124
+ let found = false
125
+ for (const dep of deps) {
126
+ if (dep === from || !visited.has(dep)) {
127
+ path.push(dep)
128
+ visited.add(dep)
129
+ current = dep
130
+ found = true
131
+ break
132
+ }
133
+ }
134
+ if (!found) break
135
+ }
136
+ return path
137
+ }
84
138
  getPending() {
85
139
  return this.promiseList.filter(p => (p.state === 'pending'))
86
140
  }
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/main.js"],"names":[],"mappings":";AA0GA;IACE,0CAwbC;IAhbC,cAaW;IAuBX,gBAAqB;IAErB,sBAAwB;IACxB,qBAAuB;IAGvB,uBAA4B;IAc5B,uBAAoC;IAIpC,kBAAqC;IACrC,kBAAqC;IAErC,yBAA+F;IAC/F,yBAAuD;IACvD,kCAAyE;IAKvE,YAA0B;IAE1B,oBAA6C;IAE7C,gBAAoD;IAOpD,uBAAkC;IAElC,uBAA8B;IAE9B,uBAAkC;IASpC,wBAAmC;IAGnC,mBA8GC;IAoED,4BAA8C;IAO9C,aA2EC;IAUD,oBAEC;IAGD,eAkDC;IAOD,YAAc;IACd,cAAgB;IAChB,kBAAkB;IAGpB;;;;OAIG;IACH,0BAHW,MAAM,GACJ,OAAO,CAQnB;IAED;;;;OAIG;IACH,6BAHW,MAAM,GACJ,MAAM,GAAC,IAAI,CAOvB;IAED;;;;OAIG;IACH,gCAHW,MAAM,GACJ,OAAO,CAoBnB;IAKD;;;;;OAKG;IACH,oBAFa,OAAO,CAAC,GAAG,CAAC,CAqsBxB;IAlsBC,aAA4B;IAc1B,2BAA4B;IAQ5B,uBAAgD;IA8qBpD;;;OAGG;IACH,2BAFa,MAAM,CA8alB;IAvBC;;;;;;;;;;;;;;;MAoBC;IAIH;;;;OAIG;IACH,uCAFa,OAAO,CAAC,GAAG,CAAC,CAIxB;IACD,+CAsBC;IAKD;;;;;;;;;;;;;;;;;;;OAmBG;IACH;;;;;;;;;;;OAWG;IACH,mFAHa;;;;cAZC,QAAQ;;;;eACR,IAAI,GAAC,MAAM,SAAO;OAWD,CA+D9B;IACD;;;OAGG;IACH;;;;;OAKG;IACH,oCAHa,OAAO,CAAC;;;;cAnFP,QAAQ;;;;eACR,IAAI,GAAC,MAAM,SAAO;OAkFgB,CAAC,EAAE,CA6BlD;IACD;;;;;OAKG;IACH,iDAFa,OAAO,CAAC,IAAI,CAAC,CAWzB;IAID;;;;;OAKG;IACH;;;;OAIG;IACH,2BAFa,eAAc;;;;;;;;;;OAAa,CAavC;IACD;;;;;OAKG;IACH,yBAHW;;;;;;;;;;OAAa,gCACX,cAAS,CAOrB;IACD;;;;;;OAMG;IACH,6DAFa,GAAC,CA+Kb;IAKD;;;;;;;OAOG;IACH,yDAHa,OAAO,CAAC,GAAG,CAAC,CAiCxB;IACD;;;;OAIG;IAOH;;;;;;OAMG;IACH,wFA2BC;IACD;;;;;;;;;;;OAWG;IACH,8BARG;QAAyB,KAAK,EAAtB,GAAG;QACoB,IAAI,GAA3B,MAAM,EAAE;QACa,cAAc,GAAnC,MAAM;QACc,iBAAiB;KAC7C,6CAEU;QAAC,KAAK,EAAE,GAAG,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,iBAAiB,CAAC,QAAQ;QAAC,oBAAoB,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAC,CAmY9J;IAID;;;;;;;;OAQG;IACH,qEAHa,OAAO,CAAC,GAAG,CAAC,CAoExB;IAKD;;;;;;;OAOG;IACH,0FAFa,OAAO,CAAC,GAAG,CAAC,CA0fxB;IACD,+EA8BC;IACD,yDAeC;IACD,oEA6BC;IAKD,8CAQC;IACD,kDAyBC;IACD;;;;;;;;;;;;;OAaG;IACH,wEAoDC;IAKD,4BAOC;IACD,sCAoDC;CACF"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/main.js"],"names":[],"mappings":";AA0GA;IACE,0CAybC;IAjbC,cAcW;IAuBX,gBAAqB;IAErB,sBAAwB;IACxB,qBAAuB;IAGvB,uBAA4B;IAc5B,uBAAoC;IAIpC,kBAAqC;IACrC,kBAAqC;IAErC,yBAA+F;IAC/F,yBAAuD;IACvD,kCAAyE;IAKvE,YAA0B;IAE1B,oBAA6C;IAE7C,gBAAoD;IAOpD,uBAAkC;IAElC,uBAA8B;IAE9B,uBAAkC;IASpC,wBAAmC;IAGnC,mBA8GC;IAoED,4BAA8C;IAO9C,aA2EC;IAUD,oBAEC;IAGD,eAkDC;IAOD,YAAc;IACd,cAAgB;IAChB,kBAAkB;IAGpB;;;;OAIG;IACH,0BAHW,MAAM,GACJ,OAAO,CAQnB;IAED;;;;OAIG;IACH,6BAHW,MAAM,GACJ,MAAM,GAAC,IAAI,CAOvB;IAED;;;;OAIG;IACH,gCAHW,MAAM,GACJ,OAAO,CAoBnB;IAKD;;;;;OAKG;IACH,oBAFa,OAAO,CAAC,GAAG,CAAC,CAqsBxB;IAlsBC,aAA4B;IAc1B,2BAA4B;IAQ5B,uBAAgD;IA8qBpD;;;OAGG;IACH,2BAFa,MAAM,CA8alB;IAvBC;;;;;;;;;;;;;;;MAoBC;IAIH;;;;OAIG;IACH,uCAFa,OAAO,CAAC,GAAG,CAAC,CAIxB;IACD,+CAsBC;IAKD;;;;;;;;;;;;;;;;;;;OAmBG;IACH;;;;;;;;;;;OAWG;IACH,mFAHa;;;;cAZC,QAAQ;;;;eACR,IAAI,GAAC,MAAM,SAAO;OAWD,CA+D9B;IACD;;;OAGG;IACH;;;;;OAKG;IACH,oCAHa,OAAO,CAAC;;;;cAnFP,QAAQ;;;;eACR,IAAI,GAAC,MAAM,SAAO;OAkFgB,CAAC,EAAE,CA6BlD;IACD;;;;;OAKG;IACH,iDAFa,OAAO,CAAC,IAAI,CAAC,CAWzB;IAID;;;;;OAKG;IACH;;;;OAIG;IACH,2BAFa,eAAc;;;;;;;;;;OAAa,CAavC;IACD;;;;;OAKG;IACH,yBAHW;;;;;;;;;;OAAa,gCACX,cAAS,CAOrB;IACD;;;;;;OAMG;IACH,6DAFa,GAAC,CA+Kb;IAKD;;;;;;;OAOG;IACH,yDAHa,OAAO,CAAC,GAAG,CAAC,CAiCxB;IACD;;;;OAIG;IAOH;;;;;;OAMG;IACH,wFA2BC;IACD;;;;;;;;;;;OAWG;IACH,8BARG;QAAyB,KAAK,EAAtB,GAAG;QACoB,IAAI,GAA3B,MAAM,EAAE;QACa,cAAc,GAAnC,MAAM;QACc,iBAAiB;KAC7C,6CAEU;QAAC,KAAK,EAAE,GAAG,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,iBAAiB,CAAC,QAAQ;QAAC,oBAAoB,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAC,CAmY9J;IAID;;;;;;;;OAQG;IACH,qEAHa,OAAO,CAAC,GAAG,CAAC,CAoExB;IAKD;;;;;;;OAOG;IACH,0FAFa,OAAO,CAAC,GAAG,CAAC,CA6gBxB;IACD,+EA+BC;IACD,yDAeC;IACD,oEA6BC;IAKD,8CAQC;IACD,kDAyBC;IACD;;;;;;;;;;;;;OAaG;IACH,wEAoDC;IAKD,4BAOC;IACD,sCAoDC;CACF"}
@@ -3,6 +3,7 @@ declare class PromiseTracker {
3
3
  reset(): void;
4
4
  promiseList: any[];
5
5
  promiseMap: {};
6
+ dependencyGraph: {};
6
7
  startTime: number;
7
8
  cursor: number;
8
9
  start(): void;
@@ -12,6 +13,9 @@ declare class PromiseTracker {
12
13
  add(variable: any, promise: any, specifier: any, hasFilter: any, promiseKey: any): any;
13
14
  contains(variable: any): boolean;
14
15
  get(variable: any, specifier: any): any;
16
+ addDependency(from: any, to: any): void;
17
+ wouldCreateCycle(from: any, to: any): boolean;
18
+ getCyclePath(from: any, to: any): any[];
15
19
  getPending(): any[];
16
20
  getSettled(): any[];
17
21
  getAll(): any[];
@@ -1 +1 @@
1
- {"version":3,"file":"PromiseTracker.d.ts","sourceRoot":"","sources":["../../../src/utils/PromiseTracker.js"],"names":[],"mappings":";AAGA;IAIE,cAKC;IAJC,mBAAqB;IACrB,eAAoB;IACpB,kBAA2B;IAC3B,eAAe;IAEjB,cAGC;IADC,yBAAyD;IAE3D,eAqBC;IACD,aAGC;IACD,uFA+BC;IACD,iCAEC;IACD,wCAIC;IACD,oBAEC;IACD,oBAEC;IACD,gBAEC;CACF"}
1
+ {"version":3,"file":"PromiseTracker.d.ts","sourceRoot":"","sources":["../../../src/utils/PromiseTracker.js"],"names":[],"mappings":";AAGA;IAIE,cAOC;IANC,mBAAqB;IACrB,eAAoB;IAEpB,oBAAyB;IACzB,kBAA2B;IAC3B,eAAe;IAEjB,cAGC;IADC,yBAAyD;IAE3D,eAqBC;IACD,aAGC;IACD,uFA+BC;IACD,iCAEC;IACD,wCAIC;IAED,wCAKC;IAED,8CAqBC;IAED,wCAoBC;IACD,oBAEC;IACD,oBAEC;IACD,gBAEC;CACF"}