configorama 0.8.0 → 0.9.3

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.
Files changed (34) hide show
  1. package/README.md +38 -3
  2. package/package.json +1 -1
  3. package/src/main.js +53 -21
  4. package/src/parsers/yaml.js +4 -4
  5. package/src/parsers/yaml.test.js +52 -0
  6. package/src/resolvers/valueFromFile.js +15 -2
  7. package/src/utils/PromiseTracker.js +54 -0
  8. package/src/utils/encoders/unknown-values.js +1 -1
  9. package/src/utils/encoders/unknown-values.test.js +146 -0
  10. package/src/utils/lodash.js +5 -4
  11. package/src/utils/lodash.test.js +172 -0
  12. package/src/utils/parsing/cloudformationSchema.js +24 -2
  13. package/src/utils/parsing/cloudformationSchema.test.js +236 -0
  14. package/src/utils/parsing/mergeByKeys.js +9 -8
  15. package/src/utils/parsing/mergeByKeys.test.js +189 -0
  16. package/src/utils/parsing/parse.js +5 -2
  17. package/src/utils/paths/getFullFilePath.js +2 -2
  18. package/src/utils/paths/getFullFilePath.test.js +152 -0
  19. package/src/utils/regex/index.js +65 -1
  20. package/src/utils/regex/index.test.js +195 -0
  21. package/src/utils/strings/formatFunctionArgs.js +4 -0
  22. package/src/utils/strings/splitCsv.js +46 -19
  23. package/types/src/main.d.ts.map +1 -1
  24. package/types/src/resolvers/valueFromFile.d.ts.map +1 -1
  25. package/types/src/utils/PromiseTracker.d.ts +4 -0
  26. package/types/src/utils/PromiseTracker.d.ts.map +1 -1
  27. package/types/src/utils/lodash.d.ts.map +1 -1
  28. package/types/src/utils/parsing/mergeByKeys.d.ts.map +1 -1
  29. package/types/src/utils/parsing/parse.d.ts.map +1 -1
  30. package/types/src/utils/regex/index.d.ts +15 -1
  31. package/types/src/utils/regex/index.d.ts.map +1 -1
  32. package/types/src/utils/strings/formatFunctionArgs.d.ts.map +1 -1
  33. package/types/src/utils/strings/splitCsv.d.ts +1 -1
  34. package/types/src/utils/strings/splitCsv.d.ts.map +1 -1
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Tests for custom lodash utility implementations
3
+ */
4
+ const { test } = require('uvu')
5
+ const assert = require('uvu/assert')
6
+ const { set, trim } = require('./lodash')
7
+
8
+ // ==========================================
9
+ // set() - basic functionality
10
+ // ==========================================
11
+
12
+ test('set - sets value at simple path', () => {
13
+ const obj = {}
14
+ set(obj, 'a', 'value')
15
+ assert.is(obj.a, 'value')
16
+ })
17
+
18
+ test('set - sets value at nested path', () => {
19
+ const obj = {}
20
+ set(obj, 'a.b.c', 'value')
21
+ assert.is(obj.a.b.c, 'value')
22
+ })
23
+
24
+ test('set - creates intermediate objects', () => {
25
+ const obj = {}
26
+ set(obj, 'a.b.c', 'value')
27
+ assert.type(obj.a, 'object')
28
+ assert.type(obj.a.b, 'object')
29
+ })
30
+
31
+ test('set - handles array path', () => {
32
+ const obj = {}
33
+ set(obj, ['a', 'b', 'c'], 'value')
34
+ assert.is(obj.a.b.c, 'value')
35
+ })
36
+
37
+ test('set - creates arrays for numeric keys', () => {
38
+ const obj = {}
39
+ set(obj, 'items.0.name', 'first')
40
+ assert.ok(Array.isArray(obj.items))
41
+ assert.is(obj.items[0].name, 'first')
42
+ })
43
+
44
+ test('set - overwrites existing value', () => {
45
+ const obj = { a: { b: 'old' } }
46
+ set(obj, 'a.b', 'new')
47
+ assert.is(obj.a.b, 'new')
48
+ })
49
+
50
+ test('set - returns the object', () => {
51
+ const obj = {}
52
+ const result = set(obj, 'a', 'value')
53
+ assert.is(result, obj)
54
+ })
55
+
56
+ test('set - returns object unchanged if object is null', () => {
57
+ const result = set(null, 'a.b', 'value')
58
+ assert.is(result, null)
59
+ })
60
+
61
+ test('set - returns object unchanged if object is primitive', () => {
62
+ const result = set('string', 'a.b', 'value')
63
+ assert.is(result, 'string')
64
+ })
65
+
66
+ // ==========================================
67
+ // set() - null intermediate values (bug fix)
68
+ // ==========================================
69
+
70
+ test('set - overwrites null intermediate value with object', () => {
71
+ const obj = { a: null }
72
+ set(obj, 'a.b.c', 'value')
73
+ assert.type(obj.a, 'object')
74
+ assert.is(obj.a.b.c, 'value')
75
+ })
76
+
77
+ test('set - overwrites null at deeper level', () => {
78
+ const obj = { a: { b: null } }
79
+ set(obj, 'a.b.c', 'value')
80
+ assert.type(obj.a.b, 'object')
81
+ assert.is(obj.a.b.c, 'value')
82
+ })
83
+
84
+ // ==========================================
85
+ // set() - primitive intermediate values (bug fix)
86
+ // ==========================================
87
+
88
+ test('set - overwrites string intermediate value', () => {
89
+ const obj = { a: 'string' }
90
+ set(obj, 'a.b.c', 'value')
91
+ assert.type(obj.a, 'object')
92
+ assert.is(obj.a.b.c, 'value')
93
+ })
94
+
95
+ test('set - overwrites number intermediate value', () => {
96
+ const obj = { a: 42 }
97
+ set(obj, 'a.b.c', 'value')
98
+ assert.type(obj.a, 'object')
99
+ assert.is(obj.a.b.c, 'value')
100
+ })
101
+
102
+ test('set - overwrites boolean intermediate value', () => {
103
+ const obj = { a: true }
104
+ set(obj, 'a.b.c', 'value')
105
+ assert.type(obj.a, 'object')
106
+ assert.is(obj.a.b.c, 'value')
107
+ })
108
+
109
+ test('set - overwrites false intermediate value', () => {
110
+ const obj = { a: false }
111
+ set(obj, 'a.b.c', 'value')
112
+ assert.type(obj.a, 'object')
113
+ assert.is(obj.a.b.c, 'value')
114
+ })
115
+
116
+ test('set - overwrites zero intermediate value', () => {
117
+ const obj = { a: 0 }
118
+ set(obj, 'a.b.c', 'value')
119
+ assert.type(obj.a, 'object')
120
+ assert.is(obj.a.b.c, 'value')
121
+ })
122
+
123
+ test('set - overwrites empty string intermediate value', () => {
124
+ const obj = { a: '' }
125
+ set(obj, 'a.b.c', 'value')
126
+ assert.type(obj.a, 'object')
127
+ assert.is(obj.a.b.c, 'value')
128
+ })
129
+
130
+ // ==========================================
131
+ // set() - array handling with null/primitives
132
+ // ==========================================
133
+
134
+ test('set - creates array when overwriting null for numeric path', () => {
135
+ const obj = { items: null }
136
+ set(obj, 'items.0.name', 'value')
137
+ assert.ok(Array.isArray(obj.items))
138
+ assert.is(obj.items[0].name, 'value')
139
+ })
140
+
141
+ test('set - creates array when overwriting primitive for numeric path', () => {
142
+ const obj = { items: 'not an array' }
143
+ set(obj, 'items.0.name', 'value')
144
+ assert.ok(Array.isArray(obj.items))
145
+ assert.is(obj.items[0].name, 'value')
146
+ })
147
+
148
+ // ==========================================
149
+ // trim() - basic functionality
150
+ // ==========================================
151
+
152
+ test('trim - removes whitespace from both ends', () => {
153
+ assert.is(trim(' hello '), 'hello')
154
+ })
155
+
156
+ test('trim - handles null', () => {
157
+ assert.is(trim(null), '')
158
+ })
159
+
160
+ test('trim - handles undefined', () => {
161
+ assert.is(trim(undefined), '')
162
+ })
163
+
164
+ test('trim - removes custom characters', () => {
165
+ assert.is(trim('---hello---', '-'), 'hello')
166
+ })
167
+
168
+ test('trim - handles string with no trim needed', () => {
169
+ assert.is(trim('hello'), 'hello')
170
+ })
171
+
172
+ test.run()
@@ -6,6 +6,7 @@ const flatten = require('lodash.flatten');
6
6
  const map = require('lodash.map');
7
7
 
8
8
  const functionNames = [
9
+ // Standard intrinsic functions
9
10
  'And',
10
11
  'Base64',
11
12
  'Cidr',
@@ -23,6 +24,16 @@ const functionNames = [
23
24
  'Select',
24
25
  'Split',
25
26
  'Sub',
27
+ 'Transform',
28
+ // AWS::LanguageExtensions functions
29
+ 'ForEach',
30
+ 'Length',
31
+ 'ToJsonString',
32
+ // Rule-specific functions (valid in Rules section)
33
+ 'EachMemberEquals',
34
+ 'EachMemberIn',
35
+ 'ValueOf',
36
+ 'ValueOfAll',
26
37
  ];
27
38
 
28
39
  const yamlType = (name, kind) => {
@@ -31,8 +42,19 @@ const yamlType = (name, kind) => {
31
42
  kind,
32
43
  construct: data => {
33
44
  if (name === 'GetAtt') {
34
- // special GetAtt dot syntax
35
- return { [functionName]: isString(data) ? split(data, '.', 2) : data };
45
+ // special GetAtt dot syntax - split only at FIRST dot
46
+ // Attribute names can contain dots (e.g., Endpoint.Address, CertificateDetails.CAIdentifier)
47
+ if (isString(data)) {
48
+ const dotIndex = data.indexOf('.');
49
+ if (dotIndex === -1) {
50
+ return { [functionName]: [data] };
51
+ }
52
+ return { [functionName]: [
53
+ data.substring(0, dotIndex), // Resource name (before first dot)
54
+ data.substring(dotIndex + 1) // Attribute name (after first dot, may contain dots)
55
+ ]};
56
+ }
57
+ return { [functionName]: data };
36
58
  }
37
59
  return { [functionName]: data };
38
60
  },
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Tests for CloudFormation YAML schema intrinsic functions
3
+ */
4
+ const { test } = require('uvu')
5
+ const assert = require('uvu/assert')
6
+ const YAML = require('js-yaml')
7
+ const cloudFormationSchema = require('./cloudformationSchema')
8
+
9
+ // Helper to parse YAML with CloudFormation schema
10
+ function parseCfYaml(yamlString) {
11
+ return YAML.load(yamlString, { schema: cloudFormationSchema.schema })
12
+ }
13
+
14
+ // ==========================================
15
+ // !GetAtt - Simple attributes (single dot)
16
+ // ==========================================
17
+
18
+ test('!GetAtt - simple attribute (dot syntax)', () => {
19
+ const yaml = `Value: !GetAtt MyDBInstance.DBInstanceArn`
20
+ const parsed = parseCfYaml(yaml)
21
+ assert.equal(parsed.Value['Fn::GetAtt'], ['MyDBInstance', 'DBInstanceArn'])
22
+ })
23
+
24
+ test('!GetAtt - simple attribute (array syntax)', () => {
25
+ const yaml = `Value: !GetAtt [MyDBInstance, DBInstanceArn]`
26
+ const parsed = parseCfYaml(yaml)
27
+ assert.equal(parsed.Value['Fn::GetAtt'], ['MyDBInstance', 'DBInstanceArn'])
28
+ })
29
+
30
+ // ==========================================
31
+ // !GetAtt - Nested attributes (bug fix)
32
+ // AWS resources have attributes like Endpoint.Address that contain dots
33
+ // ==========================================
34
+
35
+ test('!GetAtt - nested attribute Endpoint.Address (dot syntax)', () => {
36
+ const yaml = `Value: !GetAtt MyDBInstance.Endpoint.Address`
37
+ const parsed = parseCfYaml(yaml)
38
+ assert.equal(parsed.Value['Fn::GetAtt'], ['MyDBInstance', 'Endpoint.Address'])
39
+ })
40
+
41
+ test('!GetAtt - nested attribute Endpoint.Port (dot syntax)', () => {
42
+ const yaml = `Value: !GetAtt MyDBInstance.Endpoint.Port`
43
+ const parsed = parseCfYaml(yaml)
44
+ assert.equal(parsed.Value['Fn::GetAtt'], ['MyDBInstance', 'Endpoint.Port'])
45
+ })
46
+
47
+ test('!GetAtt - nested attribute CertificateDetails.CAIdentifier (dot syntax)', () => {
48
+ const yaml = `Value: !GetAtt MyDBInstance.CertificateDetails.CAIdentifier`
49
+ const parsed = parseCfYaml(yaml)
50
+ assert.equal(parsed.Value['Fn::GetAtt'], ['MyDBInstance', 'CertificateDetails.CAIdentifier'])
51
+ })
52
+
53
+ test('!GetAtt - nested attribute ReadEndpoint.Address (dot syntax)', () => {
54
+ const yaml = `Value: !GetAtt MyDBCluster.ReadEndpoint.Address`
55
+ const parsed = parseCfYaml(yaml)
56
+ assert.equal(parsed.Value['Fn::GetAtt'], ['MyDBCluster', 'ReadEndpoint.Address'])
57
+ })
58
+
59
+ test('!GetAtt - nested attribute MasterUserSecret.SecretArn (dot syntax)', () => {
60
+ const yaml = `Value: !GetAtt MyDBCluster.MasterUserSecret.SecretArn`
61
+ const parsed = parseCfYaml(yaml)
62
+ assert.equal(parsed.Value['Fn::GetAtt'], ['MyDBCluster', 'MasterUserSecret.SecretArn'])
63
+ })
64
+
65
+ test('!GetAtt - nested attribute (array syntax - should still work)', () => {
66
+ const yaml = `Value: !GetAtt [MyDBInstance, Endpoint.Address]`
67
+ const parsed = parseCfYaml(yaml)
68
+ assert.equal(parsed.Value['Fn::GetAtt'], ['MyDBInstance', 'Endpoint.Address'])
69
+ })
70
+
71
+ // ==========================================
72
+ // !GetAtt - Edge cases
73
+ // ==========================================
74
+
75
+ test('!GetAtt - resource name only (no attribute)', () => {
76
+ const yaml = `Value: !GetAtt MyResource`
77
+ const parsed = parseCfYaml(yaml)
78
+ assert.equal(parsed.Value['Fn::GetAtt'], ['MyResource'])
79
+ })
80
+
81
+ test('!GetAtt - three-level nested attribute', () => {
82
+ const yaml = `Value: !GetAtt MyResource.Level1.Level2.Level3`
83
+ const parsed = parseCfYaml(yaml)
84
+ assert.equal(parsed.Value['Fn::GetAtt'], ['MyResource', 'Level1.Level2.Level3'])
85
+ })
86
+
87
+ // ==========================================
88
+ // !Ref - Basic reference
89
+ // ==========================================
90
+
91
+ test('!Ref - basic reference', () => {
92
+ const yaml = `Value: !Ref MyResource`
93
+ const parsed = parseCfYaml(yaml)
94
+ assert.equal(parsed.Value['Ref'], 'MyResource')
95
+ })
96
+
97
+ // ==========================================
98
+ // !Sub - String substitution
99
+ // ==========================================
100
+
101
+ test('!Sub - simple substitution', () => {
102
+ const yaml = `Value: !Sub 'Hello \${Name}'`
103
+ const parsed = parseCfYaml(yaml)
104
+ assert.is(parsed.Value['Fn::Sub'], 'Hello ${Name}')
105
+ })
106
+
107
+ test('!Sub - with array form', () => {
108
+ const yaml = `Value: !Sub ['Hello \${Name}', {Name: World}]`
109
+ const parsed = parseCfYaml(yaml)
110
+ assert.equal(parsed.Value['Fn::Sub'], ['Hello ${Name}', { Name: 'World' }])
111
+ })
112
+
113
+ // ==========================================
114
+ // !Join - Join values
115
+ // ==========================================
116
+
117
+ test('!Join - basic join', () => {
118
+ const yaml = `Value: !Join [',', [a, b, c]]`
119
+ const parsed = parseCfYaml(yaml)
120
+ assert.equal(parsed.Value['Fn::Join'], [',', ['a', 'b', 'c']])
121
+ })
122
+
123
+ // ==========================================
124
+ // !If - Conditional
125
+ // ==========================================
126
+
127
+ test('!If - basic conditional', () => {
128
+ const yaml = `Value: !If [IsProduction, prod-value, dev-value]`
129
+ const parsed = parseCfYaml(yaml)
130
+ assert.equal(parsed.Value['Fn::If'], ['IsProduction', 'prod-value', 'dev-value'])
131
+ })
132
+
133
+ // ==========================================
134
+ // !FindInMap - Map lookup
135
+ // ==========================================
136
+
137
+ test('!FindInMap - basic lookup', () => {
138
+ const yaml = `Value: !FindInMap [MapName, TopKey, SecondKey]`
139
+ const parsed = parseCfYaml(yaml)
140
+ assert.equal(parsed.Value['Fn::FindInMap'], ['MapName', 'TopKey', 'SecondKey'])
141
+ })
142
+
143
+ // ==========================================
144
+ // !Select - Select from list
145
+ // ==========================================
146
+
147
+ test('!Select - basic select', () => {
148
+ const yaml = `Value: !Select [0, [a, b, c]]`
149
+ const parsed = parseCfYaml(yaml)
150
+ assert.equal(parsed.Value['Fn::Select'], [0, ['a', 'b', 'c']])
151
+ })
152
+
153
+ // ==========================================
154
+ // !Base64 - Base64 encoding
155
+ // ==========================================
156
+
157
+ test('!Base64 - basic encoding', () => {
158
+ const yaml = `Value: !Base64 'Hello World'`
159
+ const parsed = parseCfYaml(yaml)
160
+ assert.is(parsed.Value['Fn::Base64'], 'Hello World')
161
+ })
162
+
163
+ // ==========================================
164
+ // !Condition - Condition reference
165
+ // ==========================================
166
+
167
+ test('!Condition - basic condition', () => {
168
+ const yaml = `Value: !Condition IsProduction`
169
+ const parsed = parseCfYaml(yaml)
170
+ assert.is(parsed.Value['Condition'], 'IsProduction')
171
+ })
172
+
173
+ // ==========================================
174
+ // !Transform - Macro processing
175
+ // ==========================================
176
+
177
+ test('!Transform - basic transform', () => {
178
+ const yaml = `Value: !Transform { Name: 'AWS::Include', Parameters: { Location: 's3://bucket/file.yaml' } }`
179
+ const parsed = parseCfYaml(yaml)
180
+ assert.equal(parsed.Value['Fn::Transform'], {
181
+ Name: 'AWS::Include',
182
+ Parameters: { Location: 's3://bucket/file.yaml' }
183
+ })
184
+ })
185
+
186
+ // ==========================================
187
+ // AWS::LanguageExtensions functions
188
+ // ==========================================
189
+
190
+ test('!Length - array length', () => {
191
+ const yaml = `Value: !Length [a, b, c]`
192
+ const parsed = parseCfYaml(yaml)
193
+ assert.equal(parsed.Value['Fn::Length'], ['a', 'b', 'c'])
194
+ })
195
+
196
+ test('!ToJsonString - convert to JSON', () => {
197
+ const yaml = `Value: !ToJsonString { key: value }`
198
+ const parsed = parseCfYaml(yaml)
199
+ assert.equal(parsed.Value['Fn::ToJsonString'], { key: 'value' })
200
+ })
201
+
202
+ test('!ForEach - iteration', () => {
203
+ const yaml = `Value: !ForEach [Identifier, [a, b, c], Content]`
204
+ const parsed = parseCfYaml(yaml)
205
+ assert.equal(parsed.Value['Fn::ForEach'], ['Identifier', ['a', 'b', 'c'], 'Content'])
206
+ })
207
+
208
+ // ==========================================
209
+ // Rule-specific functions
210
+ // ==========================================
211
+
212
+ test('!ValueOf - rule value lookup', () => {
213
+ const yaml = `Value: !ValueOf [ParameterLogicalId, AttributeKey]`
214
+ const parsed = parseCfYaml(yaml)
215
+ assert.equal(parsed.Value['Fn::ValueOf'], ['ParameterLogicalId', 'AttributeKey'])
216
+ })
217
+
218
+ test('!ValueOfAll - all values lookup', () => {
219
+ const yaml = `Value: !ValueOfAll ['AWS::EC2::VPC::Id', Tags.Name]`
220
+ const parsed = parseCfYaml(yaml)
221
+ assert.equal(parsed.Value['Fn::ValueOfAll'], ['AWS::EC2::VPC::Id', 'Tags.Name'])
222
+ })
223
+
224
+ test('!EachMemberEquals - member equality check', () => {
225
+ const yaml = `Value: !EachMemberEquals [[a, b], value]`
226
+ const parsed = parseCfYaml(yaml)
227
+ assert.equal(parsed.Value['Fn::EachMemberEquals'], [['a', 'b'], 'value'])
228
+ })
229
+
230
+ test('!EachMemberIn - member inclusion check', () => {
231
+ const yaml = `Value: !EachMemberIn [[a, b], [a, b, c]]`
232
+ const parsed = parseCfYaml(yaml)
233
+ assert.equal(parsed.Value['Fn::EachMemberIn'], [['a', 'b'], ['a', 'b', 'c']])
234
+ })
235
+
236
+ test.run()
@@ -12,16 +12,17 @@ function mergeByKeys(data, path, keysToMerge) {
12
12
  const mergeAll = !keysToMerge || !Array.isArray(keysToMerge) || keysToMerge.length === 0
13
13
 
14
14
  for (const item of items) {
15
- const key = Object.keys(item)[0]
16
-
17
- if (mergeAll || keysToMerge.includes(key)) {
18
- if (!result[key]) {
19
- result[key] = Object.assign({}, item[key])
15
+ const keys = Object.keys(item)
16
+ for (const key of keys) {
17
+ if (mergeAll || keysToMerge.includes(key)) {
18
+ if (!result[key]) {
19
+ result[key] = Object.assign({}, item[key])
20
+ } else {
21
+ result[key] = Object.assign({}, result[key], item[key])
22
+ }
20
23
  } else {
21
- result[key] = Object.assign({}, result[key], item[key])
24
+ result[key] = item[key]
22
25
  }
23
- } else {
24
- result[key] = item[key]
25
26
  }
26
27
  }
27
28
  return result
@@ -0,0 +1,189 @@
1
+ const { test } = require('uvu')
2
+ const assert = require('uvu/assert')
3
+ const { mergeByKeys } = require('./mergeByKeys')
4
+
5
+ // ==========================================
6
+ // Basic functionality tests
7
+ // ==========================================
8
+
9
+ test('mergeByKeys - should merge objects with matching keys', () => {
10
+ const data = {
11
+ items: [
12
+ { Resources: { A: { type: 'a' } } },
13
+ { Resources: { B: { type: 'b' } } }
14
+ ]
15
+ }
16
+ const result = mergeByKeys(data, 'items', ['Resources'])
17
+ assert.equal(result, {
18
+ Resources: { A: { type: 'a' }, B: { type: 'b' } }
19
+ })
20
+ })
21
+
22
+ test('mergeByKeys - should pass through non-matching keys unchanged', () => {
23
+ const data = {
24
+ items: [
25
+ { Resources: { A: { type: 'a' } } },
26
+ { Outputs: { Out1: { value: 'v1' } } }
27
+ ]
28
+ }
29
+ const result = mergeByKeys(data, 'items', ['Resources'])
30
+ assert.equal(result, {
31
+ Resources: { A: { type: 'a' } },
32
+ Outputs: { Out1: { value: 'v1' } }
33
+ })
34
+ })
35
+
36
+ test('mergeByKeys - should return empty object for null/undefined data', () => {
37
+ assert.equal(mergeByKeys(null, 'items', ['Resources']), {})
38
+ assert.equal(mergeByKeys(undefined, 'items', ['Resources']), {})
39
+ })
40
+
41
+ test('mergeByKeys - should return data unchanged if path is not an array', () => {
42
+ const data = { items: 'not an array' }
43
+ assert.equal(mergeByKeys(data, 'items', ['Resources']), data)
44
+ })
45
+
46
+ // ==========================================
47
+ // Bug fix tests: Multiple keys per item
48
+ // ==========================================
49
+
50
+ test('mergeByKeys - should process ALL keys in each item, not just the first', () => {
51
+ // Simulating a CloudFormation/Serverless pattern where a config file
52
+ // might define both Resources and Outputs in the same section
53
+ const data = {
54
+ cloudformation: [
55
+ {
56
+ Resources: {
57
+ MyAPI: { Type: 'AWS::ApiGateway::RestApi' }
58
+ },
59
+ Outputs: {
60
+ ApiEndpoint: { Value: 'https://api.example.com' }
61
+ }
62
+ },
63
+ {
64
+ Resources: {
65
+ MyBucket: { Type: 'AWS::S3::Bucket' }
66
+ }
67
+ }
68
+ ]
69
+ }
70
+
71
+ const result = mergeByKeys(data, 'cloudformation', ['Resources'])
72
+
73
+ // Expected behavior:
74
+ // - Resources should be merged (both MyAPI and MyBucket)
75
+ // - Outputs should be preserved (not merged, just passed through)
76
+ const expected = {
77
+ Resources: {
78
+ MyAPI: { Type: 'AWS::ApiGateway::RestApi' },
79
+ MyBucket: { Type: 'AWS::S3::Bucket' }
80
+ },
81
+ Outputs: {
82
+ ApiEndpoint: { Value: 'https://api.example.com' }
83
+ }
84
+ }
85
+
86
+ assert.equal(result, expected)
87
+ })
88
+
89
+ test('mergeByKeys - should handle items with multiple keys when none match mergeKeys', () => {
90
+ const data = {
91
+ items: [
92
+ {
93
+ Foo: { a: 1 },
94
+ Bar: { b: 2 }
95
+ },
96
+ {
97
+ Baz: { c: 3 }
98
+ }
99
+ ]
100
+ }
101
+
102
+ // We want to merge 'Baz' only, so Foo and Bar should pass through unchanged
103
+ const result = mergeByKeys(data, 'items', ['Baz'])
104
+
105
+ const expected = {
106
+ Foo: { a: 1 },
107
+ Bar: { b: 2 },
108
+ Baz: { c: 3 }
109
+ }
110
+
111
+ assert.equal(result, expected)
112
+ })
113
+
114
+ test('mergeByKeys - should preserve all keys when mergeAll is true (empty array)', () => {
115
+ const data = {
116
+ items: [
117
+ {
118
+ Resources: { API: { Type: 'API' } },
119
+ Outputs: { Out1: { Value: 'v1' } }
120
+ },
121
+ {
122
+ Resources: { S3: { Type: 'S3' } }
123
+ }
124
+ ]
125
+ }
126
+
127
+ // Empty array means merge all keys
128
+ const result = mergeByKeys(data, 'items', [])
129
+
130
+ const expected = {
131
+ Resources: {
132
+ API: { Type: 'API' },
133
+ S3: { Type: 'S3' }
134
+ },
135
+ Outputs: {
136
+ Out1: { Value: 'v1' }
137
+ }
138
+ }
139
+
140
+ assert.equal(result, expected)
141
+ })
142
+
143
+ test('mergeByKeys - should merge multiple keys from same item when both are in mergeKeys', () => {
144
+ const data = {
145
+ items: [
146
+ {
147
+ Resources: { A: { type: 'a' } },
148
+ Outputs: { O1: { value: 'v1' } }
149
+ },
150
+ {
151
+ Resources: { B: { type: 'b' } },
152
+ Outputs: { O2: { value: 'v2' } }
153
+ }
154
+ ]
155
+ }
156
+
157
+ const result = mergeByKeys(data, 'items', ['Resources', 'Outputs'])
158
+
159
+ const expected = {
160
+ Resources: { A: { type: 'a' }, B: { type: 'b' } },
161
+ Outputs: { O1: { value: 'v1' }, O2: { value: 'v2' } }
162
+ }
163
+
164
+ assert.equal(result, expected)
165
+ })
166
+
167
+ test('mergeByKeys - should handle three keys in same item', () => {
168
+ const data = {
169
+ items: [
170
+ {
171
+ Resources: { R1: {} },
172
+ Outputs: { O1: {} },
173
+ Parameters: { P1: {} }
174
+ }
175
+ ]
176
+ }
177
+
178
+ const result = mergeByKeys(data, 'items', ['Resources'])
179
+
180
+ const expected = {
181
+ Resources: { R1: {} },
182
+ Outputs: { O1: {} },
183
+ Parameters: { P1: {} }
184
+ }
185
+
186
+ assert.equal(result, expected)
187
+ })
188
+
189
+ test.run()
@@ -34,8 +34,8 @@ function parseFileContents({ contents, filePath, varRegex, dynamicArgs }) {
34
34
  const ymlText = YAML.preProcess(contents)
35
35
  configObject = YAML.parse(ymlText)
36
36
  } catch (err) {
37
- // Attempt to fix cloudformation refs
38
- if (err.message.match(/YAMLException/)) {
37
+ // Attempt to fix cloudformation refs for YAML syntax errors
38
+ if (err.message && err.message.match(/YAMLException/)) {
39
39
  const ymlText = YAML.preProcess(contents)
40
40
  const result = YAML.load(ymlText, {
41
41
  filename: filePath,
@@ -45,6 +45,9 @@ function parseFileContents({ contents, filePath, varRegex, dynamicArgs }) {
45
45
  throw result.error
46
46
  }
47
47
  configObject = result.data
48
+ } else {
49
+ // Re-throw non-YAML errors (e.g., TypeError from null/undefined contents)
50
+ throw err
48
51
  }
49
52
  }
50
53
  } else if (fileType.match(/\.(toml|tml)/i)) {