configorama 0.9.0 → 0.9.5

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.
@@ -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)) {
@@ -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 match files that are relative
21
- } else if (pathToResolve.match(/\.\//)) {
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()