configorama 0.10.4 → 0.11.2

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/cli.js CHANGED
@@ -215,6 +215,7 @@ if (argv._.length) {
215
215
 
216
216
  // Set -- flags as options
217
217
  options.options = rest
218
+ options.handleSignalEvents = true
218
219
 
219
220
  // Process the configuration
220
221
  configorama(inputFile, options)
package/index.d.ts CHANGED
@@ -75,6 +75,8 @@ interface ConfigoramaSettings {
75
75
  mergeKeys?: string[]
76
76
  /** Map of file paths to override */
77
77
  filePathOverrides?: Record<string, string>
78
+ /** Install Configorama CLI signal handlers. Defaults to false for library calls. */
79
+ handleSignalEvents?: boolean
78
80
  }
79
81
 
80
82
  interface ConfigoramaResult<T = any> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "configorama",
3
- "version": "0.10.4",
3
+ "version": "0.11.2",
4
4
  "description": "Variable support for configuration files",
5
5
  "main": "src/index.js",
6
6
  "types": "index.d.ts",
package/src/index.js CHANGED
@@ -22,6 +22,7 @@ const { buildVariableSyntax } = require('./utils/variables/variableUtils')
22
22
  * @property {boolean} [dotEnvDebug] - enable env-stage-loader debug logs when useDotenv/useDotEnv is enabled
23
23
  * @property {string[]} [mergeKeys] - keys to merge in arrays of objects
24
24
  * @property {Object.<string, string>} [filePathOverrides] - map of file paths to override
25
+ * @property {boolean} [handleSignalEvents] - install Configorama CLI signal handlers. Defaults to false for library calls.
25
26
  */
26
27
 
27
28
  /**
package/src/main.js CHANGED
@@ -138,8 +138,8 @@ let DEBUG_TYPE = false
138
138
 
139
139
  class Configorama {
140
140
  constructor(fileOrObject, opts) {
141
- /* attach sig events on async calls */
142
- if (opts && !opts.sync) {
141
+ /* CLI-only by default. Library consumers should not get process-level signal handlers. */
142
+ if (opts && opts.handleSignalEvents && !opts.sync) {
143
143
  handleSignalEvents()
144
144
  }
145
145
 
@@ -372,7 +372,12 @@ class Configorama {
372
372
  description: `Resolves values from files. Supports sub-properties via :key or .key lookup.`,
373
373
  match: fileRefSyntax,
374
374
  resolver: (varString, o, x, pathValue) => {
375
- return this.getValueFromFile(varString, { context: pathValue })
375
+ // Inside ignore-path contexts (e.g. Fn::Sub) inline the file as raw text so
376
+ // embedded CloudFormation refs survive and the body stays a string. Skip
377
+ // raw mode when a :key/.key accessor is present — that needs a parsed value.
378
+ const hasAccessor = /\)\s*[:.]/.test(varString)
379
+ const asRawText = !!(pathValue && this.isIgnorePath(pathValue.path)) && !hasAccessor
380
+ return this.getValueFromFile(varString, { asRawText, context: pathValue })
376
381
  },
377
382
  },
378
383
 
@@ -484,6 +489,16 @@ class Configorama {
484
489
  )
485
490
  this.variablesKnownTypes = variablesKnownTypes
486
491
 
492
+ // Explicit configorama types that should still resolve inside ignore-path
493
+ // contexts like Fn::Sub (file, text, env, opt, cron, git, user sources, ...).
494
+ // Excludes self/dot.prop refs — those are left verbatim for CloudFormation /
495
+ // downstream Serverless resolution.
496
+ this.subResolvableTypes = combineRegexes(
497
+ /** @type {RegExp[]} */ (this.variableTypes
498
+ .filter((v) => v.type !== 'string' && v.type !== 'self' && v.type !== 'dot.prop' && v.match instanceof RegExp)
499
+ .map((v) => v.match))
500
+ )
501
+
487
502
  // Build prefix lookup map for O(1) type detection (perf optimization)
488
503
  this._resolverByPrefix = new Map()
489
504
  for (const r of this.variableTypes) {
@@ -1111,9 +1126,23 @@ class Configorama {
1111
1126
  // #######################
1112
1127
  // ## PROPERTY HANDLING ##
1113
1128
  // #######################
1114
- shouldSkipResolution(pathValue) {
1129
+ isIgnorePath(pathValue) {
1115
1130
  return shouldIgnorePath(pathValue, this.ignorePathPatterns)
1116
1131
  }
1132
+ // True when the value has a configorama-typed token that resolves even inside an
1133
+ // ignore-path (file/text/env/opt/cron/git/custom) — i.e. not just self/CFN refs.
1134
+ hasSubResolvableToken(value) {
1135
+ if (typeof value !== 'string') return false
1136
+ const matches = this.getMatches(value)
1137
+ if (!isArray(matches)) return false
1138
+ return matches.some((m) => this.subResolvableTypes.test(m.variable))
1139
+ }
1140
+ shouldSkipResolution(pathValue, value) {
1141
+ if (!this.isIgnorePath(pathValue)) return false
1142
+ // Under an ignore path (Fn::Sub etc.) keep resolving configorama's own typed
1143
+ // refs; only skip when nothing but self/CFN refs remain.
1144
+ return !this.hasSubResolvableToken(value)
1145
+ }
1117
1146
 
1118
1147
  /**
1119
1148
  * The declaration of a terminal property. This declaration includes the path and value of the
@@ -1275,7 +1304,7 @@ class Configorama {
1275
1304
  /* Leave opaque paths verbatim. These often contain non-configorama
1276
1305
  `${...}` syntax from CloudFormation, JavaScript, shell, VTL, etc. */
1277
1306
  variables = variables.filter((property) => {
1278
- return !this.shouldSkipResolution(property.path)
1307
+ return !this.shouldSkipResolution(property.path, property.value)
1279
1308
  })
1280
1309
  /*
1281
1310
  console.log(`variables at call count ${this.callCount}`, variables)
@@ -1563,7 +1592,7 @@ class Configorama {
1563
1592
  console.log(valueObject)
1564
1593
  }
1565
1594
  const property = valueObject.value
1566
- if (this.shouldSkipResolution(valueObject.path)) {
1595
+ if (this.shouldSkipResolution(valueObject.path, property)) {
1567
1596
  return Promise.resolve(property)
1568
1597
  }
1569
1598
  const matches = this.getMatches(property)
@@ -2166,6 +2195,7 @@ Missing Value ${missingValue} - ${matchedString}
2166
2195
  // Cache joined path to avoid repeated array.join('.') calls
2167
2196
  const pathJoined = pathValue && pathValue.length ? pathValue.join('.') : null
2168
2197
 
2198
+
2169
2199
  // Track every call to getValueFromSource for metadata
2170
2200
  if (this._trackCalls && pathJoined) {
2171
2201
  const pathKey = pathJoined
@@ -2318,6 +2348,14 @@ Missing Value ${missingValue} - ${matchedString}
2318
2348
  // console.log('resolverFunction', resolverFunction)
2319
2349
  /** */
2320
2350
 
2351
+ // Inside ignore-path contexts (Fn::Sub, inline code, VTL templates, ...) leave
2352
+ // self refs, bare config refs, and CloudFormation refs verbatim for CloudFormation
2353
+ // / downstream Serverless to resolve. Everything configorama can resolve on its own
2354
+ // (file/text/env/opt/cron/eval/git/custom/string/number) still resolves.
2355
+ if (this.isIgnorePath(pathValue) && (!found || resolverType === 'self' || resolverType === 'dot.prop')) {
2356
+ return Promise.resolve(encodeUnknown(this.varPrefix + variableString + this.varSuffix))
2357
+ }
2358
+
2321
2359
  if (found && resolverFunction) {
2322
2360
  /*
2323
2361
  console.log(`----------Resolver [${resolverType}]----------------------`)
@@ -7,22 +7,7 @@ const path = require('path')
7
7
  * @returns {Promise<*>} The result of executing the ESM file
8
8
  */
9
9
  async function executeESMFile(filePath, opts = {}) {
10
- try {
11
- // Use require for now since ESM dynamic import in async context is complex
12
- // We'll use jiti to handle ESM syntax
13
- const { createJiti } = require('jiti')
14
- const jiti = createJiti(__filename, {
15
- interopDefault: true
16
- })
17
-
18
- // Load the ESM file - resolve to absolute path first
19
- const resolvedPath = path.resolve(filePath)
20
- let esmModule = jiti(resolvedPath)
21
-
22
- return esmModule
23
- } catch (err) {
24
- throw new Error(`Failed to load ESM file ${filePath}: ${err.message}`)
25
- }
10
+ return executeESMFileSync(filePath, opts)
26
11
  }
27
12
 
28
13
  /**
@@ -8,54 +8,7 @@ const fs = require('fs')
8
8
  * @returns {Promise<*>} The exported module from the TypeScript file
9
9
  */
10
10
  async function executeTypeScriptFile(filePath, opts = {}) {
11
- // Check if tsx is available first (preferred)
12
- let useTsx = false
13
- try {
14
- require.resolve('tsx/cjs/api')
15
- useTsx = true
16
- } catch (err) {
17
- // Fallback to ts-node if tsx is not available
18
- try {
19
- require.resolve('ts-node/register')
20
- } catch (tsNodeErr) {
21
- throw new Error(
22
- 'TypeScript support requires either "tsx" or "ts-node" to be installed. ' +
23
- 'Please install one of them:\n' +
24
- ' npm install tsx --save-dev (recommended)\n' +
25
- ' npm install ts-node typescript --save-dev'
26
- )
27
- }
28
- }
29
-
30
- // Clear require cache to ensure fresh execution
31
- const resolvedPath = require.resolve(filePath)
32
- delete require.cache[resolvedPath]
33
-
34
- let tsFile
35
- if (useTsx) {
36
- // Use tsx for modern, fast TypeScript execution
37
- // @ts-ignore - tsx doesn't have type declarations
38
- const { register } = require('tsx/cjs/api')
39
- const restore = register()
40
- try {
41
- tsFile = require(filePath)
42
- } catch (err) {
43
- throw new Error(`Failed to load TypeScript file: ${err.message}`)
44
- } finally {
45
- restore()
46
- }
47
- } else {
48
- // Fallback to ts-node
49
- try {
50
- // @ts-ignore - ts-node is optional peer dependency
51
- require('ts-node/register')
52
- tsFile = require(filePath)
53
- } catch (err) {
54
- throw new Error(`Failed to load TypeScript file with ts-node: ${err.message}`)
55
- }
56
- }
57
-
58
- return tsFile
11
+ return executeTypeScriptFileSync(filePath, opts)
59
12
  }
60
13
 
61
14
  /**
@@ -91,34 +91,13 @@ function parseCronExpression(input) {
91
91
  return `${minute} ${hour} * * *`
92
92
  }
93
93
 
94
- // Parse "every X minutes/hours/days" patterns
95
- const everyMatch = normalizedInput.match(/^every (\d+) (minute|minutes|hour|hours|day|days|week|weeks|month|months)s?$/i)
96
- if (everyMatch) {
97
- const interval = parseInt(everyMatch[1])
98
- const unit = everyMatch[2].toLowerCase().replace(/s$/, '') // Remove trailing 's' if present
99
-
100
- switch (unit) {
101
- case 'minute':
102
- return `*/${interval} * * * *`
103
- case 'hour':
104
- return `0 */${interval} * * *`
105
- case 'day':
106
- return `0 0 */${interval} * *`
107
- case 'week':
108
- return `0 0 * * 0/${interval}`
109
- case 'month':
110
- return `0 0 1 */${interval} *`
111
- default:
112
- throw new Error(`Unsupported interval unit: ${unit}`)
113
- }
114
- }
115
-
116
- // Parse "X minute(s)/hour(s)/day(s)" patterns (e.g., "1 minute", "5 minutes", "1 hour")
117
- const intervalMatch = normalizedInput.match(/^(\d+) (minute|minutes|hour|hours|day|days|week|weeks|month|months)s?$/i)
94
+ // Parse "every X minutes/hours/days" and bare "X minute(s)/hour(s)/day(s)" patterns
95
+ // (e.g., "every 5 minutes", "1 minute", "5 minutes", "1 hour")
96
+ const intervalMatch = normalizedInput.match(/^(?:every )?(\d+) (minute|minutes|hour|hours|day|days|week|weeks|month|months)s?$/i)
118
97
  if (intervalMatch) {
119
98
  const interval = parseInt(intervalMatch[1])
120
99
  const unit = intervalMatch[2].toLowerCase().replace(/s$/, '') // Remove trailing 's' if present
121
-
100
+
122
101
  switch (unit) {
123
102
  case 'minute':
124
103
  return `*/${interval} * * * *`
@@ -115,7 +115,9 @@ function parseFileContents(content, filePath) {
115
115
  */
116
116
  async function getValueFromFile(ctx, variableString, options) {
117
117
  const opts = options || {}
118
- const syntax = opts.asRawText ? ctx.textRefSyntax : ctx.fileRefSyntax
118
+ // Pick syntax from the ref keyword, not the raw-text flag, so a file() ref can
119
+ // also be inlined as raw text (e.g. inside an Fn::Sub) without losing its match.
120
+ const syntax = /^\s*text\(/.test(variableString) ? ctx.textRefSyntax : ctx.fileRefSyntax
119
121
  // console.log('From file', `"${variableString}"`)
120
122
  let matchedFileString = variableString.match(syntax)[0]
121
123
  // console.log('matchedFileString', matchedFileString)
@@ -1,5 +1,6 @@
1
1
  const DEFAULT_IGNORE_PATHS = [
2
2
  '**.Fn::Sub',
3
+ '**.Fn::Sub.0',
3
4
  '**.Properties.Code.ZipFile',
4
5
  '**.Properties.FunctionCode',
5
6
  '**.Properties.UserData',