configorama 0.9.12 → 0.9.14

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 (51) hide show
  1. package/README.md +2067 -392
  2. package/cli.js +47 -9
  3. package/index.d.ts +1 -0
  4. package/package.json +1 -17
  5. package/src/main.js +39 -27
  6. package/src/parsers/index.js +3 -1
  7. package/src/parsers/markdown.js +69 -0
  8. package/src/parsers/markdown.test.js +132 -0
  9. package/src/resolvers/valueFromEnv.js +3 -6
  10. package/src/resolvers/valueFromFile.js +4 -4
  11. package/src/resolvers/valueFromGit.js +128 -86
  12. package/src/resolvers/valueFromNumber.js +10 -1
  13. package/src/resolvers/valueFromOptions.js +3 -7
  14. package/src/resolvers/valueFromParam.js +2 -1
  15. package/src/types.d.ts +1 -1
  16. package/src/utils/handleSignalEvents.js +1 -5
  17. package/src/utils/lodash.js +91 -37
  18. package/src/utils/parsing/cloudformationSchema.js +5 -10
  19. package/src/utils/parsing/getValueAtPath.js +111 -0
  20. package/src/utils/parsing/getValueAtPath.test.js +152 -0
  21. package/src/utils/parsing/parse.js +22 -1
  22. package/src/utils/parsing/preProcess.js +16 -10
  23. package/src/utils/regex/index.js +6 -9
  24. package/src/utils/ui/configWizard.js +4 -4
  25. package/src/utils/validation/warnIfNotFound.js +5 -1
  26. package/src/utils/variables/cleanVariable.js +1 -24
  27. package/types/src/main.d.ts +2 -0
  28. package/types/src/main.d.ts.map +1 -1
  29. package/types/src/parsers/markdown.d.ts +17 -0
  30. package/types/src/parsers/markdown.d.ts.map +1 -0
  31. package/types/src/resolvers/valueFromEnv.d.ts +1 -1
  32. package/types/src/resolvers/valueFromEnv.d.ts.map +1 -1
  33. package/types/src/resolvers/valueFromGit.d.ts.map +1 -1
  34. package/types/src/resolvers/valueFromNumber.d.ts +10 -2
  35. package/types/src/resolvers/valueFromNumber.d.ts.map +1 -1
  36. package/types/src/resolvers/valueFromOptions.d.ts.map +1 -1
  37. package/types/src/resolvers/valueFromParam.d.ts.map +1 -1
  38. package/types/src/utils/handleSignalEvents.d.ts.map +1 -1
  39. package/types/src/utils/lodash.d.ts +50 -3
  40. package/types/src/utils/lodash.d.ts.map +1 -1
  41. package/types/src/utils/parsing/getValueAtPath.d.ts +18 -0
  42. package/types/src/utils/parsing/getValueAtPath.d.ts.map +1 -0
  43. package/types/src/utils/parsing/parse.d.ts.map +1 -1
  44. package/types/src/utils/parsing/preProcess.d.ts.map +1 -1
  45. package/types/src/utils/regex/index.d.ts +5 -6
  46. package/types/src/utils/regex/index.d.ts.map +1 -1
  47. package/types/src/utils/validation/warnIfNotFound.d.ts +4 -0
  48. package/types/src/utils/validation/warnIfNotFound.d.ts.map +1 -1
  49. package/types/src/utils/variables/cleanVariable.d.ts +1 -1
  50. package/types/src/utils/variables/cleanVariable.d.ts.map +1 -1
  51. package/src/resolvers/valueFromSelf.js +0 -0
@@ -10,6 +10,21 @@ const { findProjectRoot } = require('../utils/paths/findProjectRoot')
10
10
  const GIT_PREFIX = 'git'
11
11
  const gitVariableSyntax = RegExp(/^git:/g)
12
12
 
13
+ /**
14
+ * Check if a directory is inside a git repository.
15
+ * @param {string} [dir] - Directory to check (defaults to process.cwd())
16
+ * @returns {boolean}
17
+ */
18
+ function isGitRepo(dir) {
19
+ const start = dir || process.cwd()
20
+ try {
21
+ if (!fs.existsSync(start)) return false
22
+ return findProjectRoot(start) !== null
23
+ } catch (err) {
24
+ return false
25
+ }
26
+ }
27
+
13
28
  /**
14
29
  * Execute a shell command
15
30
  * @param {string} cmd - Command to execute
@@ -27,6 +42,43 @@ async function _exec(cmd, options = { timeout: 1000 }) {
27
42
  })
28
43
  }
29
44
 
45
+ /**
46
+ * Execute a command with arguments array (safe from shell injection)
47
+ * @param {string} command - Command to execute
48
+ * @param {string[]} args - Arguments array
49
+ * @param {import('child_process').ExecFileOptions} [options] - ExecFile options
50
+ * @returns {Promise<string>}
51
+ */
52
+ async function _execFile(command, args, options = { timeout: 1000 }) {
53
+ return new Promise((resolve, reject) => {
54
+ childProcess.execFile(command, args, options, (err, stdout) => {
55
+ if (err) {
56
+ return reject(err)
57
+ }
58
+ return resolve(String(stdout).trim())
59
+ })
60
+ })
61
+ }
62
+
63
+ /**
64
+ * Run a git command and return undefined on failure. This lets the variable
65
+ * resolver fall through to user-provided fallbacks (e.g. `${git:branch, "main"}`)
66
+ * when not in a git repo, without surfacing raw `fatal: not a git repository`
67
+ * errors. When no fallback is provided, the outer resolver in main.js still
68
+ * produces a clear "Unable to resolve config variable" error pointing at the
69
+ * exact config path.
70
+ *
71
+ * @param {() => Promise<string>} cmdFn - Function that runs the git command
72
+ * @returns {Promise<string|undefined>}
73
+ */
74
+ async function _safeGit(cmdFn) {
75
+ try {
76
+ return await cmdFn()
77
+ } catch (err) {
78
+ return undefined
79
+ }
80
+ }
81
+
30
82
  // TODO denote computed fields in metadata
31
83
  /*
32
84
  {
@@ -65,14 +117,21 @@ function createResolver(cwd) {
65
117
  const variable = variableString.split(`${GIT_PREFIX}:`)[1]
66
118
  let value = null
67
119
  // console.log('createResolver variableString', variableString)
120
+
121
+ // If we're not inside a git repository, every git: variable resolves to
122
+ // undefined. This lets fallbacks like `${git:branch, "main"}` work, and
123
+ // when there's no fallback the outer resolver throws a clear "Unable to
124
+ // resolve config variable" error pointing at the config path.
125
+ if (!isGitRepo(cwd)) {
126
+ return undefined
127
+ }
128
+
68
129
  if (variable.match(/^remote/i)) {
69
130
  const hasParams = functionRegex.exec(variableString)
70
131
  const remoteName = (hasParams && hasParams[2]) ? formatFunctionArgs(hasParams[2]) : 'origin'
71
- value = await getGitRemote(remoteName)
72
- return value
132
+ return _safeGit(() => getGitRemote(remoteName))
73
133
  }
74
134
 
75
- const verifyMsg = `Verify the cwd has a .git directory\n`
76
135
  const normalizedVar = (variable || '').toLowerCase()
77
136
  // console.log('normalizedVar', normalizedVar)
78
137
 
@@ -82,7 +141,7 @@ function createResolver(cwd) {
82
141
  const funcName = argsMatch[1]
83
142
  const args = argsMatch[2]
84
143
  if (funcName === 'timestamp' && args) {
85
- value = await getGitTimestamp(args, cwd)
144
+ value = await getGitTimestamp(args, cwd, false)
86
145
  }
87
146
  }
88
147
 
@@ -91,55 +150,61 @@ function createResolver(cwd) {
91
150
  case GIT_KEYS.repo:
92
151
  case 'repository':
93
152
  case 'reposlug':
94
- case 'repo-slug':
95
- const urla = await getGitRemote()
153
+ case 'repo-slug': {
154
+ const urla = await _safeGit(() => getGitRemote())
155
+ if (!urla) return undefined
96
156
  const parseda = GitUrlParse(urla)
97
157
  value = parseda.full_name
98
- break;
158
+ break
159
+ }
99
160
  // Repo name
100
161
  case GIT_KEYS.name:
101
162
  case 'reponame': // repoName
102
- case 'repo-name':
103
- value = await _exec('basename `git rev-parse --show-toplevel`')
104
- break;
163
+ case 'repo-name': {
164
+ const toplevel = await _safeGit(() => _execFile('git', ['rev-parse', '--show-toplevel']))
165
+ if (!toplevel) return undefined
166
+ value = path.basename(toplevel)
167
+ break
168
+ }
105
169
  // Repo org or owner
106
170
  case GIT_KEYS.org:
107
171
  case 'owner':
108
172
  case 'organization':
109
173
  case 'repoowner': // repoOwner
110
- case 'repo-owner':
111
- const url = await getGitRemote()
174
+ case 'repo-owner': {
175
+ const url = await _safeGit(() => getGitRemote())
176
+ if (!url) return undefined
112
177
  const parsed = GitUrlParse(url)
113
178
  value = parsed.organization || parsed.owner
114
- break;
179
+ break
180
+ }
115
181
  // Repo name
116
182
  case GIT_KEYS.dir:
117
183
  case 'directory':
118
184
  case 'dirpath': // dirPath
119
185
  case 'dir-path':
120
- case 'dir_path':
121
- const gitBasePath = await _exec('git rev-parse --show-toplevel')
186
+ case 'dir_path': {
187
+ const gitBasePath = await _safeGit(() => _execFile('git', ['rev-parse', '--show-toplevel']))
188
+ if (!gitBasePath) return undefined
122
189
  if (cwd) {
123
190
  const subPath = cwd.replace(gitBasePath, '')
124
- const branch = await _exec('git rev-parse --abbrev-ref HEAD')
125
- const url = await getGitRemote()
126
- value = (subPath) ? `${url}/tree/${branch}${subPath}` : url
191
+ const branch = await _safeGit(() => _execFile('git', ['rev-parse', '--abbrev-ref', 'HEAD']))
192
+ const url = await _safeGit(() => getGitRemote())
193
+ if (!url) return undefined
194
+ value = (subPath && branch) ? `${url}/tree/${branch}${subPath}` : url
127
195
  }
128
- break;
196
+ break
197
+ }
129
198
  // Repo url
130
199
  case GIT_KEYS.url:
131
200
  case 'repourl': // repoUrl
132
201
  case 'repo-url':
133
- value = await getGitRemote()
134
- break;
202
+ value = await _safeGit(() => getGitRemote())
203
+ break
135
204
  // Current commit sha
136
205
  case 'sha':
137
206
  case 'sha1':
138
- try {
139
- value = await _exec('git rev-parse --short HEAD')
140
- } catch (err) {
141
- throw new Error(`\${git:sha1} error ${verifyMsg}`)
142
- }
207
+ value = await _safeGit(() => _execFile('git', ['rev-parse', '--short', 'HEAD']))
143
208
  break
144
209
  // Current commit full sha
145
210
  case GIT_KEYS.commit:
@@ -147,11 +212,7 @@ function createResolver(cwd) {
147
212
  case 'commit-sha':
148
213
  case 'commithash':
149
214
  case 'commit-hash':
150
- try {
151
- value = await _exec('git rev-parse HEAD')
152
- } catch (err) {
153
- throw new Error(`\${git:commit} error. ${verifyMsg}`)
154
- }
215
+ value = await _safeGit(() => _execFile('git', ['rev-parse', 'HEAD']))
155
216
  break
156
217
  // Branches
157
218
  case GIT_KEYS.branch:
@@ -159,11 +220,7 @@ function createResolver(cwd) {
159
220
  case 'branch-name':
160
221
  case 'currentbranch': // currentBranch
161
222
  case 'current-branch':
162
- try {
163
- value = await _exec('git rev-parse --abbrev-ref HEAD')
164
- } catch (err) {
165
- throw new Error(`\${git:branch} error. ${verifyMsg}`)
166
- }
223
+ value = await _safeGit(() => _execFile('git', ['rev-parse', '--abbrev-ref', 'HEAD']))
167
224
  break
168
225
  // Commit msg
169
226
  case GIT_KEYS.message:
@@ -172,42 +229,36 @@ function createResolver(cwd) {
172
229
  case 'commit-message':
173
230
  case 'commitmsg': // commitMsg
174
231
  case 'commit-msg':
175
- try {
176
- value = await _exec('git log -1 --pretty=%B')
177
- } catch (err) {
178
- throw new Error(`\${git:message} error. ${verifyMsg}`)
179
- }
180
- break;
232
+ value = await _safeGit(() => _execFile('git', ['log', '-1', '--pretty=%B']))
233
+ break
181
234
  // Git tags
182
235
  case GIT_KEYS.tag:
183
236
  case 'describe':
184
- try {
185
- value = await _exec('git describe --always')
186
- } catch (err) {
187
- throw new Error(`\${git:describeLight} error. ${verifyMsg}`)
188
- }
189
- break;
237
+ value = await _safeGit(() => _execFile('git', ['describe', '--always']))
238
+ break
190
239
  // Git tags
191
240
  case 'describeLight':
192
241
  case 'describelight':
193
242
  case 'describe-light':
194
- try {
195
- value = await _exec('git describe --always --tags')
196
- } catch (err) {
197
- throw new Error(`\${git:describeLight} error. ${verifyMsg}`)
198
- }
199
- break;
243
+ value = await _safeGit(() => _execFile('git', ['describe', '--always', '--tags']))
244
+ break
200
245
  // Is branch dirty
201
246
  case 'isDirty':
202
247
  case 'isdirty':
203
- case 'is-dirty':
204
- const writeTree = await _exec('git write-tree')
205
- const changes = await _exec(`git diff-index ${writeTree} --`)
248
+ case 'is-dirty': {
249
+ const writeTree = await _safeGit(() => _execFile('git', ['write-tree']))
250
+ if (!writeTree) return undefined
251
+ const changes = await _safeGit(() => _execFile('git', ['diff-index', writeTree.trim(), '--']))
252
+ if (changes === undefined) return undefined
206
253
  value = `${changes.length > 0}`
207
254
  break
255
+ }
208
256
  default:
209
257
  if (!value) {
210
- throw new Error(`Git variable ${variable} is unknown. Candidates are 'describe', 'describeLight', 'sha1', 'commit', 'branch', 'message', 'repository'`)
258
+ // Unknown variable name (likely a typo). This is a config error,
259
+ // not an environment one, so throw a helpful message listing the
260
+ // valid keys.
261
+ throw new Error(`Git variable "${variable}" is unknown. Valid options: ${Object.values(GIT_KEYS).join(', ')}`)
211
262
  }
212
263
  }
213
264
  return value
@@ -230,28 +281,16 @@ async function getGitTimestamp(_file, cwd, throwOnMissing = true) {
230
281
  throw new Error('File path must be a string')
231
282
  }
232
283
 
233
- // Check for suspicious characters and patterns that could be used for command injection or path traversal
234
- const dangerousPatterns = [
235
- /[;&|`$]/, // Command injection chars
236
- // /\.\.\//, // Directory traversal
237
- // /\.\./, // Directory traversal
238
- // /^[\/\\]/, // Absolute paths
239
- /[\x00-\x1f\x7f-\x9f]/ // Control characters
240
- ]
241
-
242
- if (dangerousPatterns.some(pattern => pattern.test(_file))) {
243
- throw new Error('Invalid characters or pattern in file path')
244
- }
284
+ // Strip surrounding quotes and leading slash
285
+ const file = _file
286
+ .replace(/^['"]|['"]$/g, '')
287
+ .replace(/^\//, '')
245
288
 
246
- // Only allow alphanumeric chars, dashes, underscores, forward slashes, and dots
247
- if (!/^[a-zA-Z0-9-_./\\'"]+$/.test(_file)) {
289
+ // Reject control characters
290
+ if (/[\x00-\x1f\x7f-\x9f]/.test(file)) {
248
291
  throw new Error('File path contains invalid characters')
249
292
  }
250
293
 
251
- // Normalize path and remove leading slash
252
- const file = _file
253
- .replace(/^\//, '')
254
-
255
294
  const cachedTimestamp = cache.get(file)
256
295
  if (cachedTimestamp) return cachedTimestamp
257
296
 
@@ -263,10 +302,7 @@ async function getGitTimestamp(_file, cwd, throwOnMissing = true) {
263
302
  }
264
303
 
265
304
  try {
266
- const cmd = `git log -1 --pretty="%ai" ${file}`
267
- // console.log('cmd', cmd)
268
- // console.log('cwd', cwd)
269
- const output = await _exec(cmd, { cwd })
305
+ const output = await _execFile('git', ['log', '-1', '--pretty=%ai', '--', file], { cwd })
270
306
  const date = new Date(output)
271
307
  const dateString = date.toISOString()
272
308
  cache.set(file, dateString)
@@ -281,8 +317,8 @@ async function getGitTimestamp(_file, cwd, throwOnMissing = true) {
281
317
  }
282
318
 
283
319
  try {
284
- const backupFile = path.join(projectRoot, _file)
285
- const output = await _exec(`git log -1 --pretty="%ai" ${backupFile}`, { cwd: projectRoot })
320
+ const backupFile = path.join(projectRoot, file)
321
+ const output = await _execFile('git', ['log', '-1', '--pretty=%ai', '--', backupFile], { cwd: projectRoot })
286
322
  const date = new Date(output)
287
323
  const dateString = date.toISOString()
288
324
  cache.set(file, dateString)
@@ -296,14 +332,19 @@ async function getGitTimestamp(_file, cwd, throwOnMissing = true) {
296
332
  }
297
333
  }
298
334
 
335
+ const remoteCache = new Map()
336
+
299
337
  async function getGitRemote(name = 'origin') {
300
- const remoteValues = await _exec('git remote -v')
338
+ if (remoteCache.has(name)) {
339
+ return remoteCache.get(name)
340
+ }
341
+ const remoteValues = await _execFile('git', ['remote', '-v'])
301
342
  const remotes = remoteValues.toString().split(os.EOL)
302
343
  .filter(function filterOnlyFetchRows(remote) {
303
344
  return remote.match('(fetch)')
304
345
  })
305
346
  .map(function mapRemoteLineToObject(remote) {
306
- var parts = remote.split('\t')
347
+ const parts = remote.split('\t')
307
348
  if (parts.length < 2) {
308
349
  return
309
350
  }
@@ -323,7 +364,6 @@ async function getGitRemote(name = 'origin') {
323
364
 
324
365
  if (!originUrl) {
325
366
  throw new Error(`No git remote "${name}" found. Please double check your remote names`)
326
- return
327
367
  }
328
368
  // console.log('originUrl', originUrl)
329
369
  const parsed = GitUrlParse(originUrl)
@@ -331,7 +371,9 @@ async function getGitRemote(name = 'origin') {
331
371
  // @TODO finish git api
332
372
  // console.log('parsed', parsed)
333
373
  if (parsed && parsed.source && parsed.full_name) {
334
- return `https://${parsed.source}/${parsed.full_name}`
374
+ const result = `https://${parsed.source}/${parsed.full_name}`
375
+ remoteCache.set(name, result)
376
+ return result
335
377
  }
336
378
  }
337
379
 
@@ -1,5 +1,10 @@
1
- const isNumber = require('lodash.isnumber')
1
+ // Resolves numeric literal variables to their Number values
2
+ const { isNumber } = require('../utils/lodash')
2
3
 
4
+ /**
5
+ * @param {string} variableString
6
+ * @returns {boolean}
7
+ */
3
8
  function isNumberVariable(variableString) {
4
9
  if (!variableString || variableString.trim().length === 0) {
5
10
  return false
@@ -8,6 +13,10 @@ function isNumberVariable(variableString) {
8
13
  return !isNaN(num) && isNumber(num)
9
14
  }
10
15
 
16
+ /**
17
+ * @param {string} variableString
18
+ * @returns {Promise<number>}
19
+ */
11
20
  function getValueFromNumber(variableString) {
12
21
  return Promise.resolve(Number(variableString))
13
22
  }
@@ -1,14 +1,10 @@
1
-
1
+ // Resolves values from CLI option flags
2
+ // Matches ${opt:FLAG_NAME} syntax with optional fallback values
2
3
  const optRefSyntax = RegExp(/^opt:/g)
3
4
 
4
5
  function getValueFromOptions(variableString, options) {
5
6
  const requestedOption = variableString.split(':')[1]
6
- let valueToPopulate
7
- if (requestedOption !== '' || '' in options) {
8
- valueToPopulate = options[requestedOption]
9
- } else {
10
- valueToPopulate = options
11
- }
7
+ const valueToPopulate = options[requestedOption]
12
8
  return Promise.resolve(valueToPopulate)
13
9
  }
14
10
 
@@ -1,4 +1,5 @@
1
-
1
+ // Resolves values from named parameters
2
+ // Matches ${param:KEY} syntax with optional fallback values
2
3
  const paramRefSyntax = RegExp(/^param:/g)
3
4
 
4
5
  /**
package/src/types.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // This file provides TypeScript support for validating configuration variables
3
3
 
4
4
  // Valid variable prefixes supported by configorama
5
- export type KnownVariablePrefix = 'env:' | 'opt:' | 'self:' | 'file:' | 'git:' | 'cron:'
5
+ export type KnownVariablePrefix = 'env:' | 'opt:' | 'self:' | 'file:' | 'git:' | 'cron:' | 'param:'
6
6
 
7
7
  // Quoted string literal type for fallback values
8
8
  type QuotedString = `"${string}"` | `'${string}'`
@@ -1,3 +1,4 @@
1
+ // Registers graceful shutdown handlers for SIGINT/SIGTERM/SIGBREAK
1
2
  const readline = require('readline')
2
3
 
3
4
  function handleSignalEvents() {
@@ -29,11 +30,6 @@ Exit received. Waiting for current operation to finish...
29
30
  rl.on('SIGBREAK', () => process.emit('SIGBREAK'))
30
31
  }
31
32
 
32
- // Remove any existing listeners before adding new ones
33
- process.removeAllListeners('SIGINT')
34
- process.removeAllListeners('SIGTERM')
35
- process.removeAllListeners('SIGBREAK')
36
-
37
33
  process.on('SIGINT', () => {
38
34
  global.signalEventHandling.SIGINTCount += 1
39
35
  global.signalEventHandling.shouldExitGracefully = true
@@ -1,70 +1,124 @@
1
- const isArray = require('lodash.isarray')
2
- const isString = require('lodash.isstring')
3
- const isNumber = require('lodash.isnumber')
4
- const isObject = require('lodash.isobject')
5
- const isDate = require('lodash.isdate')
6
- const isRegExp = require('lodash.isregexp')
7
- const isFunction = require('lodash.isfunction')
8
- const isEmpty = require('lodash.isempty')
1
+ // Native replacements for lodash utilities used across the codebase
2
+ const isArray = Array.isArray
3
+ const isString = (val) => typeof val === 'string'
4
+ const isNumber = (val) => typeof val === 'number' && !isNaN(val)
5
+ const isObject = (val) => val != null && typeof val === 'object'
6
+ const isDate = (val) => val instanceof Date
7
+ const isRegExp = (val) => val instanceof RegExp
8
+ const isFunction = (val) => typeof val === 'function'
9
+
10
+ /**
11
+ * @param {*} val
12
+ * @returns {boolean}
13
+ */
14
+ function isEmpty(val) {
15
+ if (val == null) return true
16
+ if (isArray(val) || isString(val)) return val.length === 0
17
+ if (val instanceof Map || val instanceof Set) return val.size === 0
18
+ if (isObject(val)) return Object.keys(val).length === 0
19
+ return false
20
+ }
21
+
22
+ // Non-trivial utilities kept as dependencies
9
23
  const camelCase = require('lodash.camelcase')
10
24
  const kebabCase = require('lodash.kebabcase')
11
- const capitalize = require('lodash.capitalize')
12
- const split = require('lodash.split')
13
- const map = require('lodash.map')
14
- const mapValues = require('lodash.mapvalues')
15
- const assign = require('lodash.assign')
16
25
  const cloneDeep = require('lodash.clonedeep')
17
26
 
18
- // Custom implementation of lodash.set
27
+ /**
28
+ * @param {string} str
29
+ * @returns {string}
30
+ */
31
+ function capitalize(str) {
32
+ if (!str) return ''
33
+ const s = String(str)
34
+ return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase()
35
+ }
36
+
37
+ /**
38
+ * @param {*[]} arr
39
+ * @param {Function} fn
40
+ * @returns {*[]}
41
+ */
42
+ function map(arr, fn) {
43
+ if (arr == null) return []
44
+ return Array.prototype.map.call(arr, fn)
45
+ }
46
+
47
+ /**
48
+ * @param {Object} obj
49
+ * @param {Function} fn
50
+ * @returns {Object}
51
+ */
52
+ function mapValues(obj, fn) {
53
+ if (obj == null) return {}
54
+ const result = {}
55
+ const keys = Object.keys(obj)
56
+ for (let i = 0; i < keys.length; i++) {
57
+ result[keys[i]] = fn(obj[keys[i]], keys[i], obj)
58
+ }
59
+ return result
60
+ }
61
+
62
+ /**
63
+ * @param {Object} object - Target object
64
+ * @param {string|string[]} path - Dot-delimited path or array of keys
65
+ * @param {*} value - Value to set
66
+ * @returns {Object} The mutated object
67
+ */
19
68
  function set(object, path, value) {
20
69
  if (object === null || typeof object !== 'object') {
21
- return object;
70
+ return object
22
71
  }
23
-
72
+
24
73
  const keys = Array.isArray(path) ? path : String(path)
25
74
  .split('.')
26
75
  .map(key => {
27
- const numKey = Number(key);
28
- return Number.isInteger(numKey) && numKey >= 0 ? numKey : key;
29
- });
30
-
31
- let current = object;
32
- const lastIndex = keys.length - 1;
33
-
76
+ const numKey = Number(key)
77
+ return Number.isInteger(numKey) && numKey >= 0 ? numKey : key
78
+ })
79
+
80
+ let current = object
81
+ const lastIndex = keys.length - 1
82
+
34
83
  for (let i = 0; i < lastIndex; i++) {
35
84
  const key = keys[i]
36
-
85
+
37
86
  // Check if value is undefined, null, or not an object (primitives can't have properties)
38
87
  if (current[key] == null || typeof current[key] !== 'object') {
39
88
  // Create appropriate container based on next key type
40
- current[key] = Number.isInteger(keys[i + 1]) && keys[i + 1] >= 0 ? [] : {}
89
+ const nextKey = keys[i + 1]
90
+ current[key] = Number.isInteger(nextKey) && /** @type {number} */ (nextKey) >= 0 ? [] : {}
41
91
  }
42
-
92
+
43
93
  current = current[key]
44
94
  }
45
-
46
- current[keys[lastIndex]] = value;
47
- return object;
95
+
96
+ current[keys[lastIndex]] = value
97
+ return object
48
98
  }
49
99
 
50
100
  // Cache for trim regex patterns (perf: avoid recompilation)
51
101
  const trimRegexCache = new Map()
52
102
 
53
- // Custom implementation of lodash.trim
103
+ /**
104
+ * @param {string} string - String to trim
105
+ * @param {string} [chars] - Characters to trim (defaults to whitespace)
106
+ * @returns {string}
107
+ */
54
108
  function trim(string, chars) {
55
109
  if (string === null || string === undefined) {
56
- return '';
110
+ return ''
57
111
  }
58
112
 
59
- string = String(string);
113
+ string = String(string)
60
114
 
61
115
  if (!chars && String.prototype.trim) {
62
- return string.trim();
116
+ return string.trim()
63
117
  }
64
118
 
65
119
  if (!chars) {
66
120
  // Default characters to trim (whitespace)
67
- chars = ' \t\n\r\f\v\u00a0\u1680\u2000\u200a\u2028\u2029\u202f\u205f\u3000\ufeff';
121
+ chars = ' \t\n\r\f\v\u00a0\u1680\u2000\u200a\u2028\u2029\u202f\u205f\u3000\ufeff'
68
122
  }
69
123
 
70
124
  // Check cache first
@@ -78,7 +132,7 @@ function trim(string, chars) {
78
132
 
79
133
  // Reset lastIndex for global regex reuse
80
134
  pattern.lastIndex = 0
81
- return string.replace(pattern, '');
135
+ return string.replace(pattern, '')
82
136
  }
83
137
 
84
138
  module.exports = {
@@ -94,10 +148,10 @@ module.exports = {
94
148
  camelCase,
95
149
  kebabCase,
96
150
  capitalize,
97
- split,
151
+ split: (str, sep) => String(str).split(sep),
98
152
  map,
99
153
  mapValues,
100
- assign,
154
+ assign: Object.assign,
101
155
  set,
102
156
  cloneDeep,
103
157
  }
@@ -1,8 +1,5 @@
1
1
  const YAML = require('js-yaml');
2
- const includes = require('lodash.includes');
3
- const isString = require('lodash.isstring');
4
- const flatten = require('lodash.flatten');
5
- const map = require('lodash.map');
2
+ const { isString } = require('../lodash')
6
3
 
7
4
  const functionNames = [
8
5
  // Standard intrinsic functions
@@ -36,7 +33,7 @@ const functionNames = [
36
33
  ];
37
34
 
38
35
  const yamlType = (name, kind) => {
39
- const functionName = includes(['Ref', 'Condition'], name) ? name : `Fn::${name}`;
36
+ const functionName = ['Ref', 'Condition'].includes(name) ? name : `Fn::${name}`;
40
37
  return new YAML.Type(`!${name}`, {
41
38
  kind,
42
39
  construct: data => {
@@ -61,11 +58,9 @@ const yamlType = (name, kind) => {
61
58
  };
62
59
 
63
60
  const createSchema = () => {
64
- const types = flatten(
65
- map(functionNames, functionName =>
66
- map(['mapping', 'scalar', 'sequence'], kind => yamlType(functionName, kind))
67
- )
68
- );
61
+ const types = functionNames.flatMap(functionName =>
62
+ ['mapping', 'scalar', 'sequence'].map(kind => yamlType(functionName, kind))
63
+ )
69
64
  return YAML.Schema.create(YAML.DEFAULT_SAFE_SCHEMA, types);
70
65
  };
71
66