configorama 0.6.18 → 0.7.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.
Files changed (143) hide show
  1. package/README.md +78 -2
  2. package/cli.js +1 -0
  3. package/index.d.ts +90 -37
  4. package/package.json +8 -2
  5. package/src/index.js +42 -14
  6. package/src/main.js +135 -62
  7. package/src/parsers/index.js +10 -0
  8. package/src/parsers/typescript.js +20 -43
  9. package/src/parsers/yaml.js +35 -2
  10. package/src/resolvers/valueFromFile.js +87 -24
  11. package/src/resolvers/valueFromGit.js +11 -3
  12. package/src/utils/encoders/js-fixes.js +44 -0
  13. package/src/utils/parsing/mergeByKeys.js +4 -3
  14. package/src/utils/parsing/parse.js +4 -2
  15. package/src/utils/parsing/preProcess.js +42 -25
  16. package/src/utils/parsing/preProcess.test.js +214 -0
  17. package/src/utils/paths/getFullFilePath.js +1 -1
  18. package/src/utils/resolution/preResolveVariable.js +1 -0
  19. package/src/utils/strings/quoteUtils.js +2 -2
  20. package/src/utils/strings/splitCsv.js +1 -1
  21. package/src/utils/ui/logs.js +4 -4
  22. package/src/utils/validation/warnIfNotFound.js +3 -3
  23. package/src/utils/variables/findNestedVariables.js +2 -2
  24. package/src/utils/variables/variableUtils.js +43 -0
  25. package/src/utils/variables/variableUtils.test.js +38 -1
  26. package/types/cli.d.ts +3 -0
  27. package/types/cli.d.ts.map +1 -0
  28. package/types/src/functions/md5.d.ts +3 -0
  29. package/types/src/functions/md5.d.ts.map +1 -0
  30. package/types/src/index.d.ts +100 -0
  31. package/types/src/index.d.ts.map +1 -0
  32. package/types/src/main.d.ts +275 -0
  33. package/types/src/main.d.ts.map +1 -0
  34. package/types/src/parsers/esm.d.ts +15 -0
  35. package/types/src/parsers/esm.d.ts.map +1 -0
  36. package/types/src/parsers/hcl.d.ts +1 -0
  37. package/types/src/parsers/hcl.d.ts.map +1 -0
  38. package/types/src/parsers/index.d.ts +18 -0
  39. package/types/src/parsers/index.d.ts.map +1 -0
  40. package/types/src/parsers/ini.d.ts +6 -0
  41. package/types/src/parsers/ini.d.ts.map +1 -0
  42. package/types/src/parsers/json5.d.ts +10 -0
  43. package/types/src/parsers/json5.d.ts.map +1 -0
  44. package/types/src/parsers/toml.d.ts +7 -0
  45. package/types/src/parsers/toml.d.ts.map +1 -0
  46. package/types/src/parsers/typescript.d.ts +15 -0
  47. package/types/src/parsers/typescript.d.ts.map +1 -0
  48. package/types/src/parsers/yaml.d.ts +45 -0
  49. package/types/src/parsers/yaml.d.ts.map +1 -0
  50. package/types/src/resolvers/valueFromCron.d.ts +14 -0
  51. package/types/src/resolvers/valueFromCron.d.ts.map +1 -0
  52. package/types/src/resolvers/valueFromEnv.d.ts +8 -0
  53. package/types/src/resolvers/valueFromEnv.d.ts.map +1 -0
  54. package/types/src/resolvers/valueFromEval.d.ts +7 -0
  55. package/types/src/resolvers/valueFromEval.d.ts.map +1 -0
  56. package/types/src/resolvers/valueFromFile.d.ts +58 -0
  57. package/types/src/resolvers/valueFromFile.d.ts.map +1 -0
  58. package/types/src/resolvers/valueFromGit.d.ts +11 -0
  59. package/types/src/resolvers/valueFromGit.d.ts.map +1 -0
  60. package/types/src/resolvers/valueFromNumber.d.ts +6 -0
  61. package/types/src/resolvers/valueFromNumber.d.ts.map +1 -0
  62. package/types/src/resolvers/valueFromOptions.d.ts +9 -0
  63. package/types/src/resolvers/valueFromOptions.d.ts.map +1 -0
  64. package/types/src/resolvers/valueFromSelf.d.ts +1 -0
  65. package/types/src/resolvers/valueFromSelf.d.ts.map +1 -0
  66. package/types/src/resolvers/valueFromString.d.ts +6 -0
  67. package/types/src/resolvers/valueFromString.d.ts.map +1 -0
  68. package/types/src/sync.d.ts +3 -0
  69. package/types/src/sync.d.ts.map +1 -0
  70. package/types/src/utils/PromiseTracker.d.ts +19 -0
  71. package/types/src/utils/PromiseTracker.d.ts.map +1 -0
  72. package/types/src/utils/encoders/index.d.ts +2 -0
  73. package/types/src/utils/encoders/index.d.ts.map +1 -0
  74. package/types/src/utils/encoders/js-fixes.d.ts +23 -0
  75. package/types/src/utils/encoders/js-fixes.d.ts.map +1 -0
  76. package/types/src/utils/encoders/unknown-values.d.ts +18 -0
  77. package/types/src/utils/encoders/unknown-values.d.ts.map +1 -0
  78. package/types/src/utils/handleSignalEvents.d.ts +3 -0
  79. package/types/src/utils/handleSignalEvents.d.ts.map +1 -0
  80. package/types/src/utils/lodash.d.ts +4 -0
  81. package/types/src/utils/lodash.d.ts.map +1 -0
  82. package/types/src/utils/parsing/arrayToJsonPath.d.ts +5 -0
  83. package/types/src/utils/parsing/arrayToJsonPath.d.ts.map +1 -0
  84. package/types/src/utils/parsing/cloudformationSchema.d.ts +2 -0
  85. package/types/src/utils/parsing/cloudformationSchema.d.ts.map +1 -0
  86. package/types/src/utils/parsing/enrichMetadata.d.ts +17 -0
  87. package/types/src/utils/parsing/enrichMetadata.d.ts.map +1 -0
  88. package/types/src/utils/parsing/mergeByKeys.d.ts +5 -0
  89. package/types/src/utils/parsing/mergeByKeys.d.ts.map +1 -0
  90. package/types/src/utils/parsing/parse.d.ts +54 -0
  91. package/types/src/utils/parsing/parse.d.ts.map +1 -0
  92. package/types/src/utils/parsing/preProcess.d.ts +10 -0
  93. package/types/src/utils/parsing/preProcess.d.ts.map +1 -0
  94. package/types/src/utils/paths/filePathUtils.d.ts +32 -0
  95. package/types/src/utils/paths/filePathUtils.d.ts.map +1 -0
  96. package/types/src/utils/paths/findLineForKey.d.ts +12 -0
  97. package/types/src/utils/paths/findLineForKey.d.ts.map +1 -0
  98. package/types/src/utils/paths/findProjectRoot.d.ts +2 -0
  99. package/types/src/utils/paths/findProjectRoot.d.ts.map +1 -0
  100. package/types/src/utils/paths/getFullFilePath.d.ts +25 -0
  101. package/types/src/utils/paths/getFullFilePath.d.ts.map +1 -0
  102. package/types/src/utils/paths/resolveAlias.d.ts +14 -0
  103. package/types/src/utils/paths/resolveAlias.d.ts.map +1 -0
  104. package/types/src/utils/regex/index.d.ts +14 -0
  105. package/types/src/utils/regex/index.d.ts.map +1 -0
  106. package/types/src/utils/resolution/preResolveVariable.d.ts +51 -0
  107. package/types/src/utils/resolution/preResolveVariable.d.ts.map +1 -0
  108. package/types/src/utils/strings/bracketMatcher.d.ts +25 -0
  109. package/types/src/utils/strings/bracketMatcher.d.ts.map +1 -0
  110. package/types/src/utils/strings/formatFunctionArgs.d.ts +3 -0
  111. package/types/src/utils/strings/formatFunctionArgs.d.ts.map +1 -0
  112. package/types/src/utils/strings/quoteUtils.d.ts +31 -0
  113. package/types/src/utils/strings/quoteUtils.d.ts.map +1 -0
  114. package/types/src/utils/strings/replaceAll.d.ts +9 -0
  115. package/types/src/utils/strings/replaceAll.d.ts.map +1 -0
  116. package/types/src/utils/strings/splitByComma.d.ts +2 -0
  117. package/types/src/utils/strings/splitByComma.d.ts.map +1 -0
  118. package/types/src/utils/strings/splitCsv.d.ts +10 -0
  119. package/types/src/utils/strings/splitCsv.d.ts.map +1 -0
  120. package/types/src/utils/strings/textUtils.d.ts +15 -0
  121. package/types/src/utils/strings/textUtils.d.ts.map +1 -0
  122. package/types/src/utils/ui/chalk.d.ts +70 -0
  123. package/types/src/utils/ui/chalk.d.ts.map +1 -0
  124. package/types/src/utils/ui/configWizard.d.ts +67 -0
  125. package/types/src/utils/ui/configWizard.d.ts.map +1 -0
  126. package/types/src/utils/ui/createEditorLink.d.ts +11 -0
  127. package/types/src/utils/ui/createEditorLink.d.ts.map +1 -0
  128. package/types/src/utils/ui/deep-log.d.ts +3 -0
  129. package/types/src/utils/ui/deep-log.d.ts.map +1 -0
  130. package/types/src/utils/ui/logs.d.ts +2 -0
  131. package/types/src/utils/ui/logs.d.ts.map +1 -0
  132. package/types/src/utils/validation/warnIfNotFound.d.ts +15 -0
  133. package/types/src/utils/validation/warnIfNotFound.d.ts.map +1 -0
  134. package/types/src/utils/variables/appendDeepVariable.d.ts +3 -0
  135. package/types/src/utils/variables/appendDeepVariable.d.ts.map +1 -0
  136. package/types/src/utils/variables/cleanVariable.d.ts +3 -0
  137. package/types/src/utils/variables/cleanVariable.d.ts.map +1 -0
  138. package/types/src/utils/variables/findNestedVariables.d.ts +23 -0
  139. package/types/src/utils/variables/findNestedVariables.d.ts.map +1 -0
  140. package/types/src/utils/variables/getVariableType.d.ts +8 -0
  141. package/types/src/utils/variables/getVariableType.d.ts.map +1 -0
  142. package/types/src/utils/variables/variableUtils.d.ts +21 -0
  143. package/types/src/utils/variables/variableUtils.d.ts.map +1 -0
@@ -2,10 +2,10 @@ const path = require('path')
2
2
  const fs = require('fs')
3
3
 
4
4
  /**
5
- * Execute TypeScript file and return its export
5
+ * Load TypeScript file and return its export (without executing)
6
6
  * @param {string} filePath - Full path to the TypeScript file
7
- * @param {Object} opts - Additional options including dynamicArgs
8
- * @returns {Promise<*>} The result of executing the TypeScript file
7
+ * @param {Object} opts - Additional options (unused, kept for API compat)
8
+ * @returns {Promise<*>} The exported module from the TypeScript file
9
9
  */
10
10
  async function executeTypeScriptFile(filePath, opts = {}) {
11
11
  // Check if tsx is available first (preferred)
@@ -34,6 +34,7 @@ async function executeTypeScriptFile(filePath, opts = {}) {
34
34
  let tsFile
35
35
  if (useTsx) {
36
36
  // Use tsx for modern, fast TypeScript execution
37
+ // @ts-ignore - tsx doesn't have type declarations
37
38
  const { register } = require('tsx/cjs/api')
38
39
  const restore = register()
39
40
  try {
@@ -46,6 +47,7 @@ async function executeTypeScriptFile(filePath, opts = {}) {
46
47
  } else {
47
48
  // Fallback to ts-node
48
49
  try {
50
+ // @ts-ignore - ts-node is optional peer dependency
49
51
  require('ts-node/register')
50
52
  tsFile = require(filePath)
51
53
  } catch (err) {
@@ -53,34 +55,19 @@ async function executeTypeScriptFile(filePath, opts = {}) {
53
55
  }
54
56
  }
55
57
 
56
- if (typeof tsFile !== 'function') {
57
- return tsFile
58
- } else {
59
- let tsArgs = opts.dynamicArgs || {}
60
- if (tsArgs && typeof tsArgs === 'function') {
61
- tsArgs = tsArgs()
62
- }
63
-
64
- try {
65
- const result = tsFile(tsArgs)
66
-
67
- // Handle promises
68
- if (result && typeof result.then === 'function') {
69
- return await result
70
- }
71
-
72
- return result
73
- } catch (err) {
74
- throw new Error(`Error executing TypeScript function: ${err.message}`)
75
- }
58
+ // Handle ES module default exports
59
+ if (tsFile && typeof tsFile === 'object' && 'default' in tsFile) {
60
+ tsFile = tsFile.default
76
61
  }
62
+
63
+ return tsFile
77
64
  }
78
65
 
79
66
  /**
80
- * Synchronous TypeScript file execution (using tsx with sync API)
67
+ * Load TypeScript file synchronously and return its export
81
68
  * @param {string} filePath - Full path to the TypeScript file
82
- * @param {Object} opts - Additional options including dynamicArgs
83
- * @returns {*} The result of executing the TypeScript file
69
+ * @param {Object} opts - Additional options (unused, kept for API compat)
70
+ * @returns {*} The exported module from the TypeScript file
84
71
  */
85
72
  function executeTypeScriptFileSync(filePath, opts = {}) {
86
73
  // Check if tsx is available first (preferred)
@@ -109,6 +96,7 @@ function executeTypeScriptFileSync(filePath, opts = {}) {
109
96
  let tsFile
110
97
  if (useTsx) {
111
98
  // Use tsx for modern, fast TypeScript execution
99
+ // @ts-ignore - tsx doesn't have type declarations
112
100
  const { register } = require('tsx/cjs/api')
113
101
  const restore = register()
114
102
  try {
@@ -121,6 +109,7 @@ function executeTypeScriptFileSync(filePath, opts = {}) {
121
109
  } else {
122
110
  // Fallback to ts-node
123
111
  try {
112
+ // @ts-ignore - ts-node is optional peer dependency
124
113
  require('ts-node/register')
125
114
  tsFile = require(filePath)
126
115
  } catch (err) {
@@ -128,24 +117,12 @@ function executeTypeScriptFileSync(filePath, opts = {}) {
128
117
  }
129
118
  }
130
119
 
131
- if (typeof tsFile !== 'function') {
132
- return tsFile
133
- } else {
134
- let tsArgs = opts.dynamicArgs || {}
135
- if (tsArgs && typeof tsArgs === 'function') {
136
- tsArgs = tsArgs()
137
- }
138
-
139
- try {
140
- const result = tsFile(tsArgs)
141
-
142
- // Note: For sync execution, we don't await promises
143
- // If the function returns a promise, it will be resolved by the calling code
144
- return result
145
- } catch (err) {
146
- throw new Error(`Error executing TypeScript function: ${err.message}`)
147
- }
120
+ // Handle ES module default exports
121
+ if (tsFile && typeof tsFile === 'object' && 'default' in tsFile) {
122
+ tsFile = tsFile.default
148
123
  }
124
+
125
+ return tsFile
149
126
  }
150
127
 
151
128
  module.exports = {
@@ -3,7 +3,12 @@ const TOML = require('./toml')
3
3
  const JSON = require('./json5')
4
4
  const { findOutermostVariables, findOutermostBracesDepthFirst } = require('../utils/strings/bracketMatcher')
5
5
 
6
- // Loader for custom CF syntax
6
+ /**
7
+ * Loader for custom CF syntax
8
+ * @param {string|Buffer} contents - YAML content to load
9
+ * @param {Object} [options] - YAML load options
10
+ * @returns {{data: Object|null, error: Error|null}} Parsed data and error if any
11
+ */
7
12
  function load(contents, options) {
8
13
  let data
9
14
  let error
@@ -15,6 +20,12 @@ function load(contents, options) {
15
20
  return { data, error }
16
21
  }
17
22
 
23
+ /**
24
+ * Parse YAML content into JavaScript object
25
+ * @param {string} ymlContents - YAML string to parse
26
+ * @returns {Object} Parsed YAML object
27
+ * @throws {Error} If YAML parsing fails
28
+ */
18
29
  function parse(ymlContents) {
19
30
  // Get document, or throw exception on error
20
31
  let ymlObject = {}
@@ -26,6 +37,12 @@ function parse(ymlContents) {
26
37
  return ymlObject
27
38
  }
28
39
 
40
+ /**
41
+ * Convert JavaScript object to YAML string
42
+ * @param {Object} object - Object to convert to YAML
43
+ * @returns {string} YAML string representation
44
+ * @throws {Error} If conversion fails
45
+ */
29
46
  function dump(object) {
30
47
  let yml
31
48
  try {
@@ -38,6 +55,12 @@ function dump(object) {
38
55
  return yml
39
56
  }
40
57
 
58
+ /**
59
+ * Convert YAML content to TOML format
60
+ * @param {string} ymlContents - YAML string to convert
61
+ * @returns {string} TOML string representation
62
+ * @throws {Error} If conversion fails
63
+ */
41
64
  function toToml(ymlContents) {
42
65
  let toml
43
66
  try {
@@ -48,6 +71,12 @@ function toToml(ymlContents) {
48
71
  return toml
49
72
  }
50
73
 
74
+ /**
75
+ * Convert YAML content to JSON format
76
+ * @param {string} ymlContents - YAML string to convert
77
+ * @returns {string} JSON string representation
78
+ * @throws {Error} If conversion fails
79
+ */
51
80
  function toJson(ymlContents) {
52
81
  let json
53
82
  try {
@@ -61,12 +90,16 @@ function toJson(ymlContents) {
61
90
  // Alias for backward compatibility
62
91
  const matchOutermostBraces = findOutermostBracesDepthFirst
63
92
 
64
-
65
93
  // https://regex101.com/r/XIltbc/1
66
94
  const KEY_OBJECT = /^[ \t]*[^":\s]*:\s+\{/gm
67
95
 
68
96
  const INNER_ARRAY = /\[(?:[^\[\]])*\]/g
69
97
 
98
+ /**
99
+ * Pre-process YAML string to handle nested variables and CloudFormation syntax
100
+ * @param {string} [ymlStr=''] - YAML string to pre-process
101
+ * @returns {string} Pre-processed YAML string
102
+ */
70
103
  function preProcess(ymlStr = '') {
71
104
  /*
72
105
  return ymlStr
@@ -7,7 +7,7 @@ const { splitCsv } = require('../utils/strings/splitCsv')
7
7
  const { resolveFilePathFromMatch, resolveFilePath } = require('../utils/paths/getFullFilePath')
8
8
  const { findNestedVariables } = require('../utils/variables/findNestedVariables')
9
9
  const { makeBox } = require('@davidwells/box-logger')
10
- const { encodeJsSyntax } = require('../utils/encoders/js-fixes')
10
+ const { encodeJsSyntax, decodeJsonInVariable, hasEncodedJson } = require('../utils/encoders/js-fixes')
11
11
 
12
12
  /* File Parsers */
13
13
  const YAML = require('../parsers/yaml')
@@ -15,6 +15,29 @@ const TOML = require('../parsers/toml')
15
15
  const INI = require('../parsers/ini')
16
16
  const JSON5 = require('../parsers/json5')
17
17
 
18
+ /**
19
+ * Recursively clean encoded JSON from an object
20
+ * @param {*} obj - Object to clean
21
+ * @returns {*} Cleaned object
22
+ */
23
+ function cleanEncodedJson(obj) {
24
+ if (!obj) return obj
25
+ if (typeof obj === 'string') {
26
+ return decodeJsonInVariable(obj)
27
+ }
28
+ if (Array.isArray(obj)) {
29
+ return obj.map(cleanEncodedJson)
30
+ }
31
+ if (typeof obj === 'object') {
32
+ const cleaned = {}
33
+ for (const key of Object.keys(obj)) {
34
+ cleaned[key] = cleanEncodedJson(obj[key])
35
+ }
36
+ return cleaned
37
+ }
38
+ return obj
39
+ }
40
+
18
41
  /**
19
42
  * Parse file contents based on file extension
20
43
  * @param {string} content - Raw file contents
@@ -86,6 +109,17 @@ async function getValueFromFile(ctx, variableString, options) {
86
109
  matchedFileString = argsFound[0]
87
110
  argsToPass = argsFound.filter((arg, i) => {
88
111
  return i !== 0
112
+ }).map((arg) => {
113
+ // Decode base64-encoded JSON objects passed as args
114
+ if (hasEncodedJson(arg)) {
115
+ const decoded = decodeJsonInVariable(arg)
116
+ try {
117
+ return JSON.parse(decoded)
118
+ } catch (e) {
119
+ return decoded
120
+ }
121
+ }
122
+ return arg
89
123
  })
90
124
  }
91
125
  }
@@ -137,9 +171,8 @@ async function getValueFromFile(ctx, variableString, options) {
137
171
 
138
172
  ctx.fileRefsFound.push(fileRefEntry)
139
173
 
140
- let fileExtension = resolvedPath.split('.')
141
-
142
- fileExtension = fileExtension[fileExtension.length - 1].toLowerCase()
174
+ const fileExtParts = resolvedPath.split('.')
175
+ const fileExtension = fileExtParts[fileExtParts.length - 1].toLowerCase()
143
176
 
144
177
  // Validate file exists
145
178
  if (!exists) {
@@ -168,14 +201,14 @@ async function getValueFromFile(ctx, variableString, options) {
168
201
  const errorMsg = makeBox({
169
202
  title: `File Not Found in ${originalVar}`,
170
203
  minWidth: '100%',
171
- text: `Variable ${variableString} cannot resolve due to missing file.
204
+ content: `Variable ${variableString} cannot resolve due to missing file.
172
205
 
173
206
  File not found ${fullFilePath}
174
207
 
175
208
  Default fallback value will be used if provided.
176
209
 
177
210
  ${JSON.stringify(options.context, null, 2)}`,
178
- })
211
+ })
179
212
  console.log(errorMsg)
180
213
  }
181
214
  // TODO maybe reject. YAML does not allow for null/undefined values
@@ -197,11 +230,17 @@ ${JSON.stringify(options.context, null, 2)}`,
197
230
  return Promise.resolve(valueToPopulate)
198
231
  }
199
232
 
233
+ // Clean encoded JSON from currentConfig for cleaner context
234
+ const cleanedCurrentConfig = cleanEncodedJson(ctx.config)
235
+
200
236
  // Build context for executable files
201
237
  const valueForFunction = {
238
+ options: ctx.opts.options || {},
202
239
  originalConfig: ctx.originalConfig,
203
- config: ctx.config,
204
- opts: ctx.opts,
240
+ currentConfig: cleanedCurrentConfig,
241
+ argsToPass,
242
+ // maybe helper fns
243
+ // maybe the lib instance itself for nested lookups
205
244
  }
206
245
 
207
246
  // Process JS files
@@ -230,8 +269,14 @@ ${JSON.stringify(options.context, null, 2)}`,
230
269
  try {
231
270
  const tsFile = await executeTypeScriptFile(fullFilePath, { dynamicArgs: () => argsToPass })
232
271
  let returnValueFunction = tsFile.config || tsFile.default || tsFile
233
- if (moduleName) {
272
+ // For default export functions with :property syntax, keep the function and use deep properties
273
+ // For named exports (non-function module), look up the named export
274
+ let includeFirstProperty = false
275
+ if (moduleName && typeof returnValueFunction !== 'function') {
234
276
  returnValueFunction = tsFile[moduleName]
277
+ } else if (moduleName && typeof returnValueFunction === 'function') {
278
+ // Default export function with property access - include first property in path
279
+ includeFirstProperty = true
235
280
  }
236
281
 
237
282
  return processExecutableFile({
@@ -243,7 +288,8 @@ ${JSON.stringify(options.context, null, 2)}`,
243
288
  matchedFileString,
244
289
  relativePath,
245
290
  fileType: 'TypeScript',
246
- getDeeperValue: ctx.getDeeperValue
291
+ getDeeperValue: ctx.getDeeperValue,
292
+ includeFirstProperty
247
293
  })
248
294
  } catch (err) {
249
295
  return Promise.reject(new Error(`Error processing TypeScript file: ${err.message}`))
@@ -257,8 +303,14 @@ ${JSON.stringify(options.context, null, 2)}`,
257
303
  try {
258
304
  const esmFile = await executeESMFile(fullFilePath, { dynamicArgs: () => argsToPass })
259
305
  let returnValueFunction = esmFile.config || esmFile.default || esmFile
260
- if (moduleName) {
306
+ // For default export functions with :property syntax, keep the function and use deep properties
307
+ // For named exports (non-function module), look up the named export
308
+ let includeFirstProperty = false
309
+ if (moduleName && typeof returnValueFunction !== 'function') {
261
310
  returnValueFunction = esmFile[moduleName]
311
+ } else if (moduleName && typeof returnValueFunction === 'function') {
312
+ // Default export function with property access - include first property in path
313
+ includeFirstProperty = true
262
314
  }
263
315
 
264
316
  return processExecutableFile({
@@ -270,7 +322,8 @@ ${JSON.stringify(options.context, null, 2)}`,
270
322
  matchedFileString,
271
323
  relativePath,
272
324
  fileType: 'ESM',
273
- getDeeperValue: ctx.getDeeperValue
325
+ getDeeperValue: ctx.getDeeperValue,
326
+ includeFirstProperty
274
327
  })
275
328
  } catch (err) {
276
329
  return Promise.reject(new Error(`Error processing ESM file: ${err.message}`))
@@ -295,15 +348,15 @@ ${JSON.stringify(options.context, null, 2)}`,
295
348
  }
296
349
  // console.log('deep', variableString)
297
350
  // console.log('matchedFileString', matchedFileString)
298
- let deepProperties = variableString.replace(matchedFileString, '')
351
+ const deepPropertiesStr = variableString.replace(matchedFileString, '')
299
352
  // Support both : and . as the separator for sub properties
300
- const firstChar = deepProperties.substring(0, 1)
353
+ const firstChar = deepPropertiesStr.substring(0, 1)
301
354
  if (firstChar !== ':' && firstChar !== '.') {
302
355
  const errorMessage = `Invalid variable syntax when referencing file "${relativePath}" sub properties
303
- Please use ":" or "." to reference sub properties. ${deepProperties}`
356
+ Please use ":" or "." to reference sub properties. ${deepPropertiesStr}`
304
357
  return Promise.reject(new Error(errorMessage))
305
358
  }
306
- deepProperties = deepProperties.slice(1).split('.')
359
+ const deepProperties = deepPropertiesStr.slice(1).split('.')
307
360
  return ctx.getDeeperValue(deepProperties, valueToPopulate)
308
361
  }
309
362
 
@@ -360,13 +413,21 @@ function parseModuleReference(variableString, matchedFileString) {
360
413
  * Extracts deep properties from variable string after file match
361
414
  * @param {string} variableString - The full variable string
362
415
  * @param {string} matchedFileString - The matched file path portion
416
+ * @param {boolean} [includeFirstProperty=false] - Include first property (for default exports)
363
417
  * @returns {string[]} Array of property keys to traverse
364
418
  */
365
- function extractDeepProperties(variableString, matchedFileString) {
366
- let deepProperties = variableString.replace(matchedFileString, '')
367
- deepProperties = deepProperties.slice(1).split('.')
368
- deepProperties.splice(0, 1)
369
- return deepProperties.map((prop) => trim(prop))
419
+ function extractDeepProperties(variableString, matchedFileString, includeFirstProperty = false) {
420
+ const deepPropertiesStr = variableString.replace(matchedFileString, '')
421
+ if (!deepPropertiesStr || deepPropertiesStr === '') {
422
+ return []
423
+ }
424
+ const deepProperties = deepPropertiesStr.slice(1).split('.')
425
+ // For named exports, skip first property (it's the module name)
426
+ // For default exports, keep all properties
427
+ if (!includeFirstProperty) {
428
+ deepProperties.splice(0, 1)
429
+ }
430
+ return deepProperties.map((prop) => trim(prop)).filter(Boolean)
370
431
  }
371
432
 
372
433
  /**
@@ -381,6 +442,7 @@ function extractDeepProperties(variableString, matchedFileString) {
381
442
  * @param {string} params.relativePath - Relative file path for errors
382
443
  * @param {string} params.fileType - Type of file (javascript/TypeScript/ESM)
383
444
  * @param {Function} params.getDeeperValue - Function to resolve nested values
445
+ * @param {boolean} [params.includeFirstProperty=false] - Include first property in path (for default exports)
384
446
  * @returns {Promise<any>}
385
447
  */
386
448
  async function processExecutableFile({
@@ -392,7 +454,8 @@ async function processExecutableFile({
392
454
  matchedFileString,
393
455
  relativePath,
394
456
  fileType,
395
- getDeeperValue
457
+ getDeeperValue,
458
+ includeFirstProperty = false
396
459
  }) {
397
460
  if (typeof returnValueFunction !== 'function') {
398
461
  const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
@@ -400,10 +463,10 @@ Check if your ${fileType} is exporting a function that returns a value.`
400
463
  return Promise.reject(new Error(errorMessage))
401
464
  }
402
465
 
403
- const valueToPopulate = returnValueFunction.call(fileModule, valueForFunction, ...argsToPass)
466
+ const valueToPopulate = returnValueFunction.call(fileModule, ...argsToPass, valueForFunction)
404
467
 
405
468
  return Promise.resolve(valueToPopulate).then((valueToPopulateResolved) => {
406
- const deepProperties = extractDeepProperties(variableString, matchedFileString)
469
+ const deepProperties = extractDeepProperties(variableString, matchedFileString, includeFirstProperty)
407
470
  return getDeeperValue(deepProperties, valueToPopulateResolved).then((deepValueToPopulateResolved) => {
408
471
  if (typeof deepValueToPopulateResolved === 'undefined') {
409
472
  const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
@@ -10,13 +10,19 @@ const { findProjectRoot } = require('../utils/paths/findProjectRoot')
10
10
  const GIT_PREFIX = 'git'
11
11
  const gitVariableSyntax = RegExp(/^git:/g)
12
12
 
13
+ /**
14
+ * Execute a shell command
15
+ * @param {string} cmd - Command to execute
16
+ * @param {import('child_process').ExecOptions} [options] - Exec options
17
+ * @returns {Promise<string>}
18
+ */
13
19
  async function _exec(cmd, options = { timeout: 1000 }) {
14
20
  return new Promise((resolve, reject) => {
15
21
  childProcess.exec(cmd, options, (err, stdout) => {
16
22
  if (err) {
17
23
  return reject(err)
18
24
  }
19
- return resolve(stdout.trim())
25
+ return resolve(String(stdout).trim())
20
26
  })
21
27
  })
22
28
  }
@@ -213,8 +219,10 @@ const cache = new Map()
213
219
 
214
220
  /**
215
221
  * Gets the last Git commit timestamp for a file
216
- * @param {string} file - Path to the file to check
217
- * @returns {Promise<Date|undefined>} The commit timestamp or undefined if not in Git
222
+ * @param {string} _file - Path to the file to check
223
+ * @param {string} cwd - Working directory
224
+ * @param {boolean} [throwOnMissing] - Whether to throw on missing file
225
+ * @returns {Promise<string|undefined>} The commit timestamp ISO string or undefined if not in Git
218
226
  */
219
227
  async function getGitTimestamp(_file, cwd, throwOnMissing = true) {
220
228
  // Validate file path to prevent command injection
@@ -1,6 +1,9 @@
1
1
  const PAREN_OPEN_PLACEHOLDER = '__PH_PAREN_OPEN__'
2
2
  const OPEN_PAREN_PLACEHOLDER_PATTERN = /__PH_PAREN_OPEN__/g
3
3
 
4
+ const JSON_ENCODED_PREFIX = '__JSON_B64__'
5
+ const JSON_ENCODED_PATTERN = /__JSON_B64__([A-Za-z0-9+/=]+)__/g
6
+
4
7
  function encodeJsSyntax(value = '') {
5
8
  return value.replace(/\(/g, PAREN_OPEN_PLACEHOLDER)
6
9
  }
@@ -14,9 +17,50 @@ function hasParenthesesPlaceholder(value = '') {
14
17
  return OPEN_PAREN_PLACEHOLDER_PATTERN.test(value)
15
18
  }
16
19
 
20
+ /**
21
+ * Encode a JSON object to base64 for safe embedding in variable strings
22
+ * @param {object} obj - Object to encode
23
+ * @returns {string} Encoded string like __JSON_B64__eyJmb28iOiJiYXIifQ==__
24
+ */
25
+ function encodeJsonForVariable(obj) {
26
+ const jsonStr = JSON.stringify(obj)
27
+ const b64 = Buffer.from(jsonStr).toString('base64')
28
+ return `${JSON_ENCODED_PREFIX}${b64}__`
29
+ }
30
+
31
+ /**
32
+ * Decode base64-encoded JSON from variable strings
33
+ * @param {string} value - String potentially containing encoded JSON
34
+ * @returns {string} String with encoded JSON decoded back to JSON strings
35
+ */
36
+ function decodeJsonInVariable(value) {
37
+ if (!value || typeof value !== 'string') return value
38
+ return value.replace(JSON_ENCODED_PATTERN, (match, b64) => {
39
+ try {
40
+ const jsonStr = Buffer.from(b64, 'base64').toString('utf8')
41
+ return jsonStr
42
+ } catch (e) {
43
+ return match
44
+ }
45
+ })
46
+ }
47
+
48
+ /**
49
+ * Check if string contains encoded JSON
50
+ * @param {string} value - String to check
51
+ * @returns {boolean}
52
+ */
53
+ function hasEncodedJson(value) {
54
+ if (!value || typeof value !== 'string') return false
55
+ return value.includes(JSON_ENCODED_PREFIX)
56
+ }
57
+
17
58
  module.exports = {
18
59
  OPEN_PAREN_PLACEHOLDER_PATTERN,
19
60
  hasParenthesesPlaceholder,
20
61
  encodeJsSyntax,
21
62
  decodeJsSyntax,
63
+ encodeJsonForVariable,
64
+ decodeJsonInVariable,
65
+ hasEncodedJson,
22
66
  }
@@ -3,9 +3,10 @@
3
3
  */
4
4
  function mergeByKeys(data, path, keysToMerge) {
5
5
  if (!data) return {}
6
-
7
- const items = path.split('.').reduce((obj, key) => obj?.[key], data)
8
- if (!Array.isArray(items)) return {}
6
+
7
+ // Handle empty path - operate on root data
8
+ const items = path ? path.split('.').reduce((obj, key) => obj?.[key], data) : data
9
+ if (!Array.isArray(items)) return data
9
10
 
10
11
  const result = {}
11
12
  const mergeAll = !keysToMerge || !Array.isArray(keysToMerge) || keysToMerge.length === 0
@@ -31,12 +31,12 @@ function parseFileContents({ contents, filePath, varRegex, dynamicArgs }) {
31
31
 
32
32
  if (fileType.match(/\.(yml|yaml)/i)) {
33
33
  try {
34
- const ymlText = YAML.preProcess(contents, regex)
34
+ const ymlText = YAML.preProcess(contents)
35
35
  configObject = YAML.parse(ymlText)
36
36
  } catch (err) {
37
37
  // Attempt to fix cloudformation refs
38
38
  if (err.message.match(/YAMLException/)) {
39
- const ymlText = YAML.preProcess(contents, regex)
39
+ const ymlText = YAML.preProcess(contents)
40
40
  const result = YAML.load(ymlText, {
41
41
  filename: filePath,
42
42
  schema: cloudFormationSchema.schema,
@@ -82,6 +82,8 @@ function parseFileContents({ contents, filePath, varRegex, dynamicArgs }) {
82
82
  configObject = (typeof configObject.config === 'function') ? configObject.config(jsArgs) : configObject.config
83
83
  } else if (configObject.default) {
84
84
  configObject = (typeof configObject.default === 'function') ? configObject.default(jsArgs) : configObject.default
85
+ } else if (typeof configObject === 'function') {
86
+ configObject = configObject(jsArgs)
85
87
  }
86
88
  // console.log('parseFileContents configObject', configObject)
87
89
  } catch (err) {