configorama 0.9.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.
- package/package.json +1 -1
- package/src/main.js +31 -20
- package/src/parsers/yaml.js +4 -4
- package/src/parsers/yaml.test.js +52 -0
- package/src/resolvers/valueFromFile.js +15 -2
- package/src/utils/encoders/unknown-values.js +1 -1
- package/src/utils/encoders/unknown-values.test.js +146 -0
- package/src/utils/lodash.js +5 -4
- package/src/utils/lodash.test.js +172 -0
- package/src/utils/parsing/cloudformationSchema.js +24 -2
- package/src/utils/parsing/cloudformationSchema.test.js +236 -0
- package/src/utils/parsing/mergeByKeys.js +9 -8
- package/src/utils/parsing/mergeByKeys.test.js +189 -0
- package/src/utils/parsing/parse.js +5 -2
- package/src/utils/paths/getFullFilePath.js +2 -2
- package/src/utils/paths/getFullFilePath.test.js +152 -0
- package/src/utils/regex/index.js +65 -1
- package/src/utils/regex/index.test.js +195 -0
- package/src/utils/strings/formatFunctionArgs.js +4 -0
- package/src/utils/strings/splitCsv.js +46 -19
- package/types/src/main.d.ts.map +1 -1
- package/types/src/resolvers/valueFromFile.d.ts.map +1 -1
- package/types/src/utils/lodash.d.ts.map +1 -1
- package/types/src/utils/parsing/mergeByKeys.d.ts.map +1 -1
- package/types/src/utils/parsing/parse.d.ts.map +1 -1
- package/types/src/utils/regex/index.d.ts +15 -1
- package/types/src/utils/regex/index.d.ts.map +1 -1
- package/types/src/utils/strings/formatFunctionArgs.d.ts.map +1 -1
- package/types/src/utils/strings/splitCsv.d.ts +1 -1
- package/types/src/utils/strings/splitCsv.d.ts.map +1 -1
|
@@ -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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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] =
|
|
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)) {
|
|
@@ -17,8 +17,8 @@ function resolveFilePath(pathToResolve, basePath) {
|
|
|
17
17
|
if (fs.existsSync(fullFilePath)) {
|
|
18
18
|
// Get real path to handle potential symlinks (but don't fatal error)
|
|
19
19
|
fullFilePath = fs.realpathSync(fullFilePath)
|
|
20
|
-
// Only
|
|
21
|
-
} else if (
|
|
20
|
+
// Only use findUp for relative paths (not absolute paths)
|
|
21
|
+
} else if (!path.isAbsolute(pathToResolve)) {
|
|
22
22
|
const cleanName = path.basename(pathToResolve)
|
|
23
23
|
const findUpResult = findUp.sync(cleanName, { cwd: basePath })
|
|
24
24
|
if (findUpResult) {
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for getFullFilePath.js - file path resolution with findUp support
|
|
3
|
+
*/
|
|
4
|
+
const { test } = require('uvu')
|
|
5
|
+
const assert = require('uvu/assert')
|
|
6
|
+
const path = require('path')
|
|
7
|
+
const fs = require('fs')
|
|
8
|
+
const getFullPath = require('./getFullFilePath')
|
|
9
|
+
const { resolveFilePath, resolveFilePathFromMatch } = require('./getFullFilePath')
|
|
10
|
+
|
|
11
|
+
// ==========================================
|
|
12
|
+
// Test directory setup/teardown
|
|
13
|
+
// ==========================================
|
|
14
|
+
|
|
15
|
+
const testDir = path.join(__dirname, '_test-getFullFilePath')
|
|
16
|
+
const subDir = path.join(testDir, 'subdir')
|
|
17
|
+
const deepDir = path.join(subDir, 'deepdir')
|
|
18
|
+
|
|
19
|
+
test.before(() => {
|
|
20
|
+
// Cleanup any existing test directory
|
|
21
|
+
if (fs.existsSync(testDir)) {
|
|
22
|
+
fs.rmSync(testDir, { recursive: true })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Create directory structure:
|
|
26
|
+
// _test-getFullFilePath/
|
|
27
|
+
// config.yml <- file at root
|
|
28
|
+
// subdir/
|
|
29
|
+
// local.yml <- file in subdir
|
|
30
|
+
// deepdir/
|
|
31
|
+
// (empty - for testing findUp from here)
|
|
32
|
+
fs.mkdirSync(testDir, { recursive: true })
|
|
33
|
+
fs.mkdirSync(subDir, { recursive: true })
|
|
34
|
+
fs.mkdirSync(deepDir, { recursive: true })
|
|
35
|
+
|
|
36
|
+
// Create test files
|
|
37
|
+
fs.writeFileSync(path.join(testDir, 'config.yml'), 'root: true')
|
|
38
|
+
fs.writeFileSync(path.join(subDir, 'local.yml'), 'local: true')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test.after(() => {
|
|
42
|
+
// Cleanup
|
|
43
|
+
if (fs.existsSync(testDir)) {
|
|
44
|
+
fs.rmSync(testDir, { recursive: true })
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// ==========================================
|
|
49
|
+
// resolveFilePath - basic functionality
|
|
50
|
+
// ==========================================
|
|
51
|
+
|
|
52
|
+
test('resolveFilePath - returns file when it exists at computed path', () => {
|
|
53
|
+
const result = resolveFilePath('./config.yml', testDir)
|
|
54
|
+
assert.is(result, path.join(testDir, 'config.yml'))
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('resolveFilePath - handles absolute paths', () => {
|
|
58
|
+
const absolutePath = path.join(testDir, 'config.yml')
|
|
59
|
+
const result = resolveFilePath(absolutePath, '/some/other/path')
|
|
60
|
+
assert.is(result, absolutePath)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('resolveFilePath - returns computed path when file does not exist', () => {
|
|
64
|
+
const result = resolveFilePath('./nonexistent.yml', testDir)
|
|
65
|
+
assert.is(result, path.join(testDir, 'nonexistent.yml'))
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// ==========================================
|
|
69
|
+
// resolveFilePath - findUp with "./" prefix
|
|
70
|
+
// ==========================================
|
|
71
|
+
|
|
72
|
+
test('resolveFilePath - with "./" prefix triggers findUp and finds file in parent directory', () => {
|
|
73
|
+
// From deepDir, look for config.yml which is in testDir (grandparent)
|
|
74
|
+
const result = resolveFilePath('./config.yml', deepDir)
|
|
75
|
+
const expected = path.join(testDir, 'config.yml')
|
|
76
|
+
assert.is(result, expected)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('resolveFilePath - with "./" prefix finds file in immediate parent', () => {
|
|
80
|
+
// From deepDir, look for local.yml which is in subDir (parent)
|
|
81
|
+
const result = resolveFilePath('./local.yml', deepDir)
|
|
82
|
+
const expected = path.join(subDir, 'local.yml')
|
|
83
|
+
assert.is(result, expected)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// ==========================================
|
|
87
|
+
// resolveFilePath - findUp WITHOUT "./" prefix (bug fix tests)
|
|
88
|
+
// ==========================================
|
|
89
|
+
|
|
90
|
+
test('resolveFilePath - bare filename should also trigger findUp', () => {
|
|
91
|
+
// From deepDir, look for config.yml (bare filename) which is in testDir
|
|
92
|
+
const result = resolveFilePath('config.yml', deepDir)
|
|
93
|
+
const expected = path.join(testDir, 'config.yml')
|
|
94
|
+
assert.is(result, expected,
|
|
95
|
+
`Bare filename should trigger findUp. Got ${result} instead of ${expected}`)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('resolveFilePath - bare filename finds file in immediate parent', () => {
|
|
99
|
+
// From deepDir, look for local.yml (bare filename) which is in subDir
|
|
100
|
+
const result = resolveFilePath('local.yml', deepDir)
|
|
101
|
+
const expected = path.join(subDir, 'local.yml')
|
|
102
|
+
assert.is(result, expected)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('resolveFilePath - relative path without ./ prefix triggers findUp', () => {
|
|
106
|
+
// Path like "subdir/file.yml" should also trigger findUp if not found
|
|
107
|
+
const result = resolveFilePath('config.yml', deepDir)
|
|
108
|
+
const expected = path.join(testDir, 'config.yml')
|
|
109
|
+
assert.is(result, expected)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// ==========================================
|
|
113
|
+
// getFullPath - wrapper function
|
|
114
|
+
// ==========================================
|
|
115
|
+
|
|
116
|
+
test('getFullPath - resolves file path using cwd', () => {
|
|
117
|
+
const result = getFullPath('./config.yml', testDir)
|
|
118
|
+
assert.is(result, path.join(testDir, 'config.yml'))
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('getFullPath - expands ~ to home directory', () => {
|
|
122
|
+
const os = require('os')
|
|
123
|
+
const result = getFullPath('~/somefile.yml', testDir)
|
|
124
|
+
assert.ok(result.startsWith(os.homedir()))
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// ==========================================
|
|
128
|
+
// resolveFilePathFromMatch - file() syntax parsing
|
|
129
|
+
// ==========================================
|
|
130
|
+
|
|
131
|
+
const fileRefSyntax = /^file\((~?[@\{\}\:\$a-zA-Z0-9._\-\/,'" =+]+?)\)/g
|
|
132
|
+
|
|
133
|
+
test('resolveFilePathFromMatch - extracts path from file() syntax', () => {
|
|
134
|
+
const result = resolveFilePathFromMatch('file(./config.yml)', fileRefSyntax, testDir)
|
|
135
|
+
assert.is(result.relativePath, './config.yml')
|
|
136
|
+
assert.is(result.fullFilePath, path.join(testDir, 'config.yml'))
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('resolveFilePathFromMatch - handles quoted paths', () => {
|
|
140
|
+
const result = resolveFilePathFromMatch("file('./config.yml')", fileRefSyntax, testDir)
|
|
141
|
+
assert.is(result.relativePath, './config.yml')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('resolveFilePathFromMatch - handles bare filename in file() syntax', () => {
|
|
145
|
+
// This tests the bug fix - bare filename should work with findUp
|
|
146
|
+
const result = resolveFilePathFromMatch('file(config.yml)', fileRefSyntax, deepDir)
|
|
147
|
+
const expected = path.join(testDir, 'config.yml')
|
|
148
|
+
assert.is(result.fullFilePath, expected,
|
|
149
|
+
`file(config.yml) should find file via findUp. Got ${result.fullFilePath}`)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test.run()
|