configorama 0.7.2 → 0.9.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.
package/src/main.js CHANGED
@@ -2,19 +2,16 @@
2
2
  const os = require('os')
3
3
  const path = require('path')
4
4
  const fs = require('fs')
5
-
6
5
  /* // disable logs to find broken tests
7
6
  console.log = () => {}
8
7
  // process.exit(1)
9
8
  /** */
10
-
11
9
  /* External dependencies */
12
10
  const promiseFinallyShim = require('promise.prototype.finally').shim()
13
11
  const findUp = require('find-up')
14
12
  const traverse = require('traverse')
15
13
  const dotProp = require('dot-prop')
16
14
  const { makeBox, makeStackedBoxes } = require('@davidwells/box-logger')
17
-
18
15
  /* Utils - root */
19
16
  const {
20
17
  isArray, isString, isNumber, isObject, isDate, isRegExp, isFunction,
@@ -23,71 +20,56 @@ const {
23
20
  } = require('./utils/lodash')
24
21
  const PromiseTracker = require('./utils/PromiseTracker')
25
22
  const handleSignalEvents = require('./utils/handleSignalEvents')
26
-
27
23
  /* Utils - encoders */
28
24
  const { encodeUnknown, decodeUnknown } = require('./utils/encoders/unknown-values')
29
25
  const { decodeEncodedValue } = require('./utils/encoders')
30
- const { encodeJsSyntax, decodeJsSyntax, hasParenthesesPlaceholder, encodeJsonForVariable } = require('./utils/encoders/js-fixes')
31
-
26
+ const { decodeJsSyntax, hasParenthesesPlaceholder, encodeJsonForVariable } = require('./utils/encoders/js-fixes')
32
27
  /* Utils - parsing */
33
28
  const enrichMetadata = require('./utils/parsing/enrichMetadata')
34
29
  const preProcess = require('./utils/parsing/preProcess')
35
30
  const { parseFileContents } = require('./utils/parsing/parse')
36
31
  const { mergeByKeys } = require('./utils/parsing/mergeByKeys')
37
32
  const { arrayToJsonPath } = require('./utils/parsing/arrayToJsonPath')
38
-
39
33
  /* Utils - paths */
40
34
  const { normalizePath, extractFilePath, resolveInnerVariables } = require('./utils/paths/filePathUtils')
41
- const { resolveAlias } = require('./utils/paths/resolveAlias')
42
- const { resolveFilePathFromMatch } = require('./utils/paths/getFullFilePath')
43
35
  const { findLineForKey } = require('./utils/paths/findLineForKey')
44
-
45
36
  /* Utils - regex */
46
- const { combineRegexes, funcRegex, funcStartOfLineRegex, subFunctionRegex } = require('./utils/regex')
47
-
37
+ const { combineRegexes, funcRegex } = require('./utils/regex')
48
38
  /* Utils - strings */
49
39
  const formatFunctionArgs = require('./utils/strings/formatFunctionArgs')
50
-
51
40
  const { splitByComma } = require('./utils/strings/splitByComma')
52
41
  const { splitCsv } = require('./utils/strings/splitCsv')
53
42
  const { replaceAll } = require('./utils/strings/replaceAll')
54
43
  const { getTextAfterOccurrence, findNestedVariable } = require('./utils/strings/textUtils')
55
- const { trimSurroundingQuotes, ensureQuote, isSurroundedByQuotes, startsWithQuotedPipe } = require('./utils/strings/quoteUtils')
56
-
44
+ const { ensureQuote, isSurroundedByQuotes, startsWithQuotedPipe } = require('./utils/strings/quoteUtils')
57
45
  /* Utils - ui */
58
46
  const chalk = require('./utils/ui/chalk')
59
47
  const deepLog = require('./utils/ui/deep-log')
60
48
  const { logHeader } = require('./utils/ui/logs')
61
49
  const { createEditorLink } = require('./utils/ui/createEditorLink')
62
50
  const { runConfigWizard, isSensitiveVariable } = require('./utils/ui/configWizard')
63
-
64
51
  /* Utils - validation */
65
52
  const { warnIfNotFound, isValidValue } = require('./utils/validation/warnIfNotFound')
66
-
67
53
  /* Utils - variables */
68
54
  const cleanVariable = require('./utils/variables/cleanVariable')
69
55
  const appendDeepVariable = require('./utils/variables/appendDeepVariable')
70
- const { extractVariableWrapper, getFallbackString, verifyVariable } = require('./utils/variables/variableUtils')
56
+ const { extractVariableWrapper, getFallbackString, verifyVariable, buildVariableSyntax } = require('./utils/variables/variableUtils')
71
57
  const { findNestedVariables } = require('./utils/variables/findNestedVariables')
72
-
73
58
  /* Resolvers */
74
59
  const getValueFromString = require('./resolvers/valueFromString')
75
60
  const getValueFromNumber = require('./resolvers/valueFromNumber')
76
61
  const getValueFromEnv = require('./resolvers/valueFromEnv')
77
62
  const getValueFromOptions = require('./resolvers/valueFromOptions')
63
+ const getValueFromParam = require('./resolvers/valueFromParam')
78
64
  const getValueFromCron = require('./resolvers/valueFromCron')
79
65
  const getValueFromEval = require('./resolvers/valueFromEval')
80
66
  const createGitResolver = require('./resolvers/valueFromGit')
81
67
  const { getValueFromFile: getValueFromFileResolver } = require('./resolvers/valueFromFile')
82
-
83
68
  /* Parsers */
84
- const YAML = require('./parsers/yaml')
85
- const TOML = require('./parsers/toml')
86
- const INI = require('./parsers/ini')
87
69
  const JSON5 = require('./parsers/json5')
88
-
89
70
  /* Functions */
90
71
  const md5Function = require('./functions/md5')
72
+
91
73
  /**
92
74
  * Maintainer's notes:
93
75
  *
@@ -102,6 +84,7 @@ const md5Function = require('./functions/md5')
102
84
  * pause population, noting the continued depth to traverse. This motivated "deep" variables.
103
85
  * Original issue #4687
104
86
  */
87
+
105
88
  const deepRefSyntax = RegExp(/(\${)?deep:\d+(\.[^}]+)*()}?/)
106
89
  const deepIndexReplacePattern = new RegExp(/^deep:|(\.[^}]+)*$/g)
107
90
  const deepIndexPattern = /deep\:(\d*)/
@@ -110,8 +93,6 @@ const fileRefSyntax = RegExp(/^file\((~?[@\{\}\:\$a-zA-Z0-9._\-\/,'" =+]+?)\)/g)
110
93
  const textRefSyntax = RegExp(/^text\((~?[@\{\}\:\$a-zA-Z0-9._\-\/,'" =+]+?)\)/g)
111
94
  // TODO update file regex ^file\((~?[a-zA-Z0-9._\-\/, ]+?)\)
112
95
  // To match file(asyncValue.js, lol) input params
113
- const envRefSyntax = RegExp(/^env:/g)
114
- const optRefSyntax = RegExp(/^opt:/g)
115
96
  const selfRefSyntax = RegExp(/^self:/g)
116
97
  const base64WrapperRegex = /\[_\[([A-Za-z0-9+/=\s]*)\]_\]/g
117
98
  const logLines = '─────────────────────────────────────────────────'
@@ -133,13 +114,14 @@ class Configorama {
133
114
  const options = opts || {}
134
115
  // Set opts to pass into JS file calls
135
116
  this.settings = Object.assign({}, {
136
- // Allow for unknown variable syntax to pass through without throwing errors
137
- allowUnknownVariables: false,
138
- // Allow undefined to be an end result.
117
+ // Allow unknown ${xyz:...} syntax where xyz is not a registered resolver
118
+ // Can be: false | true | ['ssm', 'cf', ...]
119
+ allowUnknownVariableTypes: false,
120
+ // Allow undefined to be an end result
139
121
  allowUndefinedValues: false,
140
- // Allow unknown file refs to pass through without throwing errors
141
- allowUnknownFileRefs: false,
142
122
  // Allow known variable types that can't be resolved to pass through
123
+ // Can be: false | true | ['param', 'file', 'env', ...]
124
+ // Note: Does not apply to self: or dotprop refs - those always error
143
125
  allowUnresolvedVariables: false,
144
126
  // Return metadata
145
127
  returnMetadata: false,
@@ -147,10 +129,26 @@ class Configorama {
147
129
  returnPreResolvedVariableDetails: false,
148
130
  }, options)
149
131
 
150
- // Backward compat: allowUnknownVars -> allowUnknownVariables
151
- if (options.allowUnknownVars !== undefined && options.allowUnknownVariables === undefined) {
152
- this.settings.allowUnknownVariables = options.allowUnknownVars
132
+ // Backward compat: allowUnknownVars -> allowUnknownVariableTypes
133
+ if (options.allowUnknownVars !== undefined && options.allowUnknownVariableTypes === undefined) {
134
+ this.settings.allowUnknownVariableTypes = options.allowUnknownVars
135
+ }
136
+ // Backward compat: allowUnknownVariables -> allowUnknownVariableTypes
137
+ if (options.allowUnknownVariables !== undefined && options.allowUnknownVariableTypes === undefined) {
138
+ this.settings.allowUnknownVariableTypes = options.allowUnknownVariables
139
+ }
140
+
141
+ // Merge legacy allowUnknownParams and allowUnknownFileRefs into allowUnresolvedVariables
142
+ let unresolvedSetting = this.settings.allowUnresolvedVariables
143
+ if (unresolvedSetting !== true) {
144
+ const specificTypes = Array.isArray(unresolvedSetting) ? [...unresolvedSetting] : []
145
+ if (options.allowUnknownParams) specificTypes.push('param')
146
+ if (options.allowUnknownFileRefs) specificTypes.push('file')
147
+ if (specificTypes.length > 0) {
148
+ unresolvedSetting = [...new Set(specificTypes)]
149
+ }
153
150
  }
151
+ this.settings.allowUnresolvedVariables = unresolvedSetting
154
152
 
155
153
  this.filterCache = {}
156
154
 
@@ -160,8 +158,8 @@ class Configorama {
160
158
  // Track variable resolutions for metadata (keyed by path)
161
159
  this.resolutionTracking = {}
162
160
 
163
- const defaultSyntax = '\\${((?!AWS|stageVariables)[ ~:a-zA-Z0-9=+!@#%*<>?._\'",|\\-\\/\\(\\)\\\\]+?)}'
164
-
161
+ const defaultSyntax = buildVariableSyntax('${', '}', ['AWS', 'stageVariables'])
162
+
165
163
  const varSyntax = options.syntax || defaultSyntax
166
164
  let varRegex
167
165
  if (typeof varSyntax === 'string') {
@@ -229,6 +227,14 @@ class Configorama {
229
227
  */
230
228
  getValueFromOptions,
231
229
 
230
+ /**
231
+ * Parameters
232
+ * Usage:
233
+ * ${param:domain}
234
+ * ${param:key, "fallbackValue"}
235
+ */
236
+ getValueFromParam,
237
+
232
238
  /**
233
239
  * Cron expressions
234
240
  * Usage:
@@ -542,6 +548,56 @@ class Configorama {
542
548
  this.callCount = 0
543
549
  }
544
550
 
551
+ /**
552
+ * Check if unresolved variables of a given type should pass through
553
+ * @param {string} type - The resolver type (e.g., 'param', 'file', 'env')
554
+ * @returns {boolean}
555
+ */
556
+ isUnresolvedAllowed(type) {
557
+ const setting = this.settings.allowUnresolvedVariables
558
+ if (setting === true) return true
559
+ if (setting === false || setting === undefined) return false
560
+ if (Array.isArray(setting) && setting.includes(type)) return true
561
+ return false
562
+ }
563
+
564
+ /**
565
+ * Extract type prefix from a variable string
566
+ * @param {string} varString - Variable string like 'ssm:path/to/thing' or 'custom:value'
567
+ * @returns {string|null} The type prefix or null if not found
568
+ */
569
+ extractTypePrefix(varString) {
570
+ if (!varString || typeof varString !== 'string') return null
571
+ const colonIndex = varString.indexOf(':')
572
+ if (colonIndex === -1) return null
573
+ return varString.substring(0, colonIndex)
574
+ }
575
+
576
+ /**
577
+ * Check if unknown variable types should pass through
578
+ * @param {string} varString - Variable string like 'ssm:path' or full '${ssm:path}'
579
+ * @returns {boolean}
580
+ */
581
+ isUnknownTypeAllowed(varString) {
582
+ const setting = this.settings.allowUnknownVariableTypes
583
+ if (setting === true) return true
584
+ if (setting === false || setting === undefined) return false
585
+ if (Array.isArray(setting)) {
586
+ // Extract type prefix from variable string
587
+ // Handle both 'ssm:path' and '${ssm:path}' formats
588
+ let cleanVar = varString
589
+ if (cleanVar.startsWith(this.varPrefix)) {
590
+ cleanVar = cleanVar.slice(this.varPrefix.length)
591
+ }
592
+ if (cleanVar.endsWith(this.varSuffix)) {
593
+ cleanVar = cleanVar.slice(0, -this.varSuffix.length)
594
+ }
595
+ const typePrefix = this.extractTypePrefix(cleanVar)
596
+ if (typePrefix && setting.includes(typePrefix)) return true
597
+ }
598
+ return false
599
+ }
600
+
545
601
  // ################
546
602
  // ## PUBLIC API ##
547
603
  // ################
@@ -1941,8 +1997,8 @@ class Configorama {
1941
1997
  for (let i = 0; i < matches.length; i += 1) {
1942
1998
  warnIfNotFound(matches[i].variable, results[i], {
1943
1999
  patterns: {
1944
- env: envRefSyntax,
1945
- opt: optRefSyntax,
2000
+ env: getValueFromEnv.match,
2001
+ opt: getValueFromOptions.match,
1946
2002
  self: selfRefSyntax,
1947
2003
  file: fileRefSyntax,
1948
2004
  deep: deepRefSyntax,
@@ -2380,7 +2436,7 @@ class Configorama {
2380
2436
 
2381
2437
  if (nestedVar) {
2382
2438
  const fallbackStr = getFallbackString(splitVars, nestedVar)
2383
- if (!this.settings.allowUnknownVariables) {
2439
+ if (!this.isUnknownTypeAllowed(nestedVar)) {
2384
2440
  verifyVariable(nestedVar, valueObject, this.variableTypes, this.config)
2385
2441
  }
2386
2442
 
@@ -2712,6 +2768,25 @@ Missing Value ${missingValue} - ${matchedString}
2712
2768
 
2713
2769
  // console.log('getValueFromSrc propertyString', propertyString)
2714
2770
  // console.log(`tracker contains ${variableString}`, this.tracker.contains(variableString))
2771
+
2772
+ // Cycle detection: track dependencies and check for cycles
2773
+ const fromPath = valueObject.path ? valueObject.path.join('.') : null
2774
+ // Extract target path from variableString (e.g., 'self:b' → 'b', 'b.c' → 'b.c')
2775
+ let toPath = variableString
2776
+ if (variableString.startsWith('self:')) {
2777
+ toPath = variableString.slice(5)
2778
+ }
2779
+ // For cycle detection, only track self-references
2780
+ if (fromPath && (variableString.startsWith('self:') || !variableString.includes(':'))) {
2781
+ if (this.tracker.wouldCreateCycle(fromPath, toPath)) {
2782
+ const cyclePath = this.tracker.getCyclePath(fromPath, toPath)
2783
+ return Promise.reject(new Error(
2784
+ `Circular variable dependency detected: ${cyclePath.join(' → ')}`
2785
+ ))
2786
+ }
2787
+ this.tracker.addDependency(fromPath, toPath)
2788
+ }
2789
+
2715
2790
  if (this.tracker.contains(variableString)) {
2716
2791
  // console.log('try to get', variableString)
2717
2792
  return this.tracker.get(variableString, propertyString)
@@ -2864,14 +2939,21 @@ Missing Value ${missingValue} - ${matchedString}
2864
2939
  // console.log('nestedVars', nestedVars)
2865
2940
  const noNestedVars = nestedVars.length < 2
2866
2941
 
2867
- if (this.settings.allowUnknownFileRefs && variableString.match(fileRefSyntax)) {
2868
- // Encode the unknown file variable to pass through resolution
2942
+ // Check if this unresolved variable type should pass through
2943
+ const isFileRef = variableString.match(fileRefSyntax)
2944
+ const isParamRef = variableString.match(getValueFromParam.match)
2945
+
2946
+ // Params pass through entirely (including fallbacks) for third-party resolution
2947
+ if (isParamRef && this.isUnresolvedAllowed('param')) {
2869
2948
  return Promise.resolve(encodeUnknown(propertyString))
2870
2949
  }
2871
2950
 
2872
- if (this.settings.allowUnresolvedVariables) {
2951
+ const isUnresolvedAllowed =
2952
+ this.settings.allowUnresolvedVariables === true ||
2953
+ (isFileRef && this.isUnresolvedAllowed('file'))
2954
+
2955
+ if (isUnresolvedAllowed) {
2873
2956
  // Check if outer expression has fallbacks we can use
2874
- // valueCount[0] is the primary var, valueCount[1+] are fallbacks
2875
2957
  if (valueCount.length > 1) {
2876
2958
  const primaryVar = valueCount[0]
2877
2959
  // If the unresolvable variableString is used INSIDE the primary var,
@@ -2880,7 +2962,6 @@ Missing Value ${missingValue} - ${matchedString}
2880
2962
  return Promise.resolve(undefined)
2881
2963
  }
2882
2964
  }
2883
- // Encode unresolved variable to pass through resolution
2884
2965
  return Promise.resolve(encodeUnknown(propertyString))
2885
2966
  }
2886
2967
 
@@ -3046,7 +3127,7 @@ Missing Value ${missingValue} - ${matchedString}
3046
3127
  // console.log('nestedVar', nestedVar)
3047
3128
 
3048
3129
  if (nestedVar) {
3049
- if (!this.settings.allowUnknownVariables) {
3130
+ if (!this.isUnknownTypeAllowed(nestedVar)) {
3050
3131
  verifyVariable(nestedVar, valueObject, this.variableTypes, this.config)
3051
3132
  }
3052
3133
  const fallbackStr = getFallbackString(split, nestedVar)
@@ -3148,8 +3229,8 @@ Missing Value ${missingValue} - ${matchedString}
3148
3229
 
3149
3230
 
3150
3231
 
3151
- /* Pass through unknown variables */
3152
- if (this.settings.allowUnknownVariables || allowSpecialCase) {
3232
+ /* Pass through unknown variable types */
3233
+ if (allowSpecialCase || this.isUnknownTypeAllowed(propertyString)) {
3153
3234
  // console.log('allowUnknownVars propertyString', propertyString)
3154
3235
  const varMatches = propertyString.match(this.variableSyntax)
3155
3236
  let allowUnknownVars = propertyString
@@ -3192,6 +3273,7 @@ Missing Value ${missingValue} - ${matchedString}
3192
3273
  // console.log('self fixed deepProperties', deepProperties)
3193
3274
  }
3194
3275
  }
3276
+
3195
3277
  return this.getDeeperValue(deepProperties, valueToPopulate).then((res) => {
3196
3278
  /*
3197
3279
  console.log('self getDeeperValue variableString', variableString)
@@ -197,7 +197,13 @@ async function getValueFromFile(ctx, variableString, options) {
197
197
  // console.log('NO FILE FOUND', fullFilePath)
198
198
  // console.log('variableString', variableString)
199
199
 
200
- if (!hasFallback && !ctx.opts.allowUnknownFileRefs) {
200
+ // Check if file refs are allowed to pass through unresolved
201
+ const allowUnresolved = ctx.opts.allowUnresolvedVariables
202
+ const isFileAllowed = allowUnresolved === true ||
203
+ (Array.isArray(allowUnresolved) && allowUnresolved.includes('file')) ||
204
+ ctx.opts.allowUnknownFileRefs // backward compat
205
+
206
+ if (!hasFallback && !isFileAllowed) {
201
207
  const errorMsg = makeBox({
202
208
  title: `File Not Found in ${originalVar}`,
203
209
  minWidth: '100%',
@@ -0,0 +1,91 @@
1
+
2
+ const paramRefSyntax = RegExp(/^param:/g)
3
+
4
+ /**
5
+ * Resolves parameter values following the Serverless Framework parameter resolution hierarchy:
6
+ * 1. CLI params (--param="key=value")
7
+ * 2. Stage-specific params (stages.<stage>.params)
8
+ * 3. Default params (stages.default.params)
9
+ *
10
+ * @param {string} variableString - The variable string (e.g., "param:domain")
11
+ * @param {Object} options - CLI options that may contain params
12
+ * @param {Object} config - The full config object for stage-specific params
13
+ * @returns {Promise<any>} The resolved parameter value
14
+ */
15
+ function getValueFromParam(variableString, options = {}, config = {}) {
16
+ const requestedParam = variableString.split(':')[1]
17
+
18
+ if (requestedParam === '') {
19
+ throw new Error(`Invalid variable syntax for parameter reference "${variableString}".
20
+
21
+ \${param} variable must have a key path.
22
+
23
+ Example: \${param:domain}
24
+ `)
25
+ }
26
+
27
+ let valueToPopulate
28
+
29
+ // 1. First, check CLI params (--param="key=value")
30
+ // The param option can be either a string or an array of strings
31
+ if (options.param) {
32
+ const params = Array.isArray(options.param) ? options.param : [options.param]
33
+
34
+ // Parse param flags in the format "key=value"
35
+ for (const param of params) {
36
+ const [key, ...valueParts] = param.split('=')
37
+ if (key === requestedParam) {
38
+ valueToPopulate = valueParts.join('=') // rejoin in case value contains =
39
+ return Promise.resolve(valueToPopulate)
40
+ }
41
+ }
42
+ }
43
+
44
+ // 2. Check for stage-specific params (stages.<stage>.params)
45
+ const stage = options.stage || 'dev'
46
+ if (config.stages && config.stages[stage] && config.stages[stage].params) {
47
+ valueToPopulate = config.stages[stage].params[requestedParam]
48
+ if (valueToPopulate !== undefined) {
49
+ return Promise.resolve(valueToPopulate)
50
+ }
51
+ }
52
+
53
+ // 3. Check for default params (stages.default.params)
54
+ if (config.stages && config.stages.default && config.stages.default.params) {
55
+ valueToPopulate = config.stages.default.params[requestedParam]
56
+ if (valueToPopulate !== undefined) {
57
+ return Promise.resolve(valueToPopulate)
58
+ }
59
+ }
60
+
61
+ // 4. Check top-level params property (for backwards compatibility)
62
+ if (config.params) {
63
+ // Check stage-specific params first
64
+ if (config.params[stage]) {
65
+ valueToPopulate = config.params[stage][requestedParam]
66
+ if (valueToPopulate !== undefined) {
67
+ return Promise.resolve(valueToPopulate)
68
+ }
69
+ }
70
+
71
+ // Then check default params
72
+ if (config.params.default) {
73
+ valueToPopulate = config.params.default[requestedParam]
74
+ if (valueToPopulate !== undefined) {
75
+ return Promise.resolve(valueToPopulate)
76
+ }
77
+ }
78
+ }
79
+
80
+ // If not found, return undefined (will trigger fallback if specified)
81
+ return Promise.resolve(valueToPopulate)
82
+ }
83
+
84
+ module.exports = {
85
+ type: 'param',
86
+ source: 'user',
87
+ syntax: '${param:paramName}',
88
+ description: 'Resolves parameter values from CLI flags, stage-specific params, or default params. Examples: ${param:domain}, ${param:key, "fallbackValue"}',
89
+ match: paramRefSyntax,
90
+ resolver: getValueFromParam
91
+ }
@@ -0,0 +1,207 @@
1
+ const { test } = require('uvu')
2
+ const assert = require('uvu/assert')
3
+ const { resolver } = require('./valueFromParam')
4
+
5
+ test('Resolves parameter from CLI flag', async () => {
6
+ const options = { param: 'domain=myapp.com' }
7
+ const result = await resolver('param:domain', options)
8
+ assert.is(result, 'myapp.com')
9
+ })
10
+
11
+ test('Resolves parameter from multiple CLI flags', async () => {
12
+ const options = { param: ['domain=myapp.com', 'key=value'] }
13
+ const result = await resolver('param:key', options)
14
+ assert.is(result, 'value')
15
+ })
16
+
17
+ test('Resolves parameter with equals sign in value', async () => {
18
+ const options = { param: 'connectionString=Server=localhost;Port=5432' }
19
+ const result = await resolver('param:connectionString', options)
20
+ assert.is(result, 'Server=localhost;Port=5432')
21
+ })
22
+
23
+ test('Resolves parameter from stage-specific params', async () => {
24
+ const options = { stage: 'prod' }
25
+ const config = {
26
+ stages: {
27
+ prod: {
28
+ params: {
29
+ domain: 'production.myapp.com'
30
+ }
31
+ }
32
+ }
33
+ }
34
+ const result = await resolver('param:domain', options, config)
35
+ assert.is(result, 'production.myapp.com')
36
+ })
37
+
38
+ test('Resolves parameter from default stage params', async () => {
39
+ const options = { stage: 'dev' }
40
+ const config = {
41
+ stages: {
42
+ default: {
43
+ params: {
44
+ domain: 'default.myapp.com'
45
+ }
46
+ }
47
+ }
48
+ }
49
+ const result = await resolver('param:domain', options, config)
50
+ assert.is(result, 'default.myapp.com')
51
+ })
52
+
53
+ test('CLI params override stage params', async () => {
54
+ const options = {
55
+ stage: 'prod',
56
+ param: 'domain=cli-override.com'
57
+ }
58
+ const config = {
59
+ stages: {
60
+ prod: {
61
+ params: {
62
+ domain: 'production.myapp.com'
63
+ }
64
+ }
65
+ }
66
+ }
67
+ const result = await resolver('param:domain', options, config)
68
+ assert.is(result, 'cli-override.com')
69
+ })
70
+
71
+ test('Stage-specific params override default params', async () => {
72
+ const options = { stage: 'prod' }
73
+ const config = {
74
+ stages: {
75
+ default: {
76
+ params: {
77
+ domain: 'default.myapp.com'
78
+ }
79
+ },
80
+ prod: {
81
+ params: {
82
+ domain: 'production.myapp.com'
83
+ }
84
+ }
85
+ }
86
+ }
87
+ const result = await resolver('param:domain', options, config)
88
+ assert.is(result, 'production.myapp.com')
89
+ })
90
+
91
+ test('Returns undefined for non-existent parameter', async () => {
92
+ const options = { stage: 'dev' }
93
+ const config = { stages: { dev: { params: {} } } }
94
+ const result = await resolver('param:nonExistent', options, config)
95
+ assert.is(result, undefined)
96
+ })
97
+
98
+ test('Throws error for empty parameter name', async () => {
99
+ try {
100
+ await resolver('param:')
101
+ assert.unreachable('Should have thrown an error')
102
+ } catch (error) {
103
+ assert.ok(error.message.includes('Invalid variable syntax'))
104
+ assert.ok(error.message.includes('must have a key path'))
105
+ }
106
+ })
107
+
108
+ test('Defaults to dev stage when no stage specified', async () => {
109
+ const options = {}
110
+ const config = {
111
+ stages: {
112
+ dev: {
113
+ params: {
114
+ domain: 'dev.myapp.com'
115
+ }
116
+ }
117
+ }
118
+ }
119
+ const result = await resolver('param:domain', options, config)
120
+ assert.is(result, 'dev.myapp.com')
121
+ })
122
+
123
+ test('Supports top-level params property (backwards compatibility)', async () => {
124
+ const options = { stage: 'prod' }
125
+ const config = {
126
+ params: {
127
+ prod: {
128
+ domain: 'production.myapp.com'
129
+ }
130
+ }
131
+ }
132
+ const result = await resolver('param:domain', options, config)
133
+ assert.is(result, 'production.myapp.com')
134
+ })
135
+
136
+ test('Supports top-level params default property', async () => {
137
+ const options = { stage: 'dev' }
138
+ const config = {
139
+ params: {
140
+ default: {
141
+ domain: 'default.myapp.com'
142
+ }
143
+ }
144
+ }
145
+ const result = await resolver('param:domain', options, config)
146
+ assert.is(result, 'default.myapp.com')
147
+ })
148
+
149
+ test('Prefers stages property over params property', async () => {
150
+ const options = { stage: 'prod' }
151
+ const config = {
152
+ stages: {
153
+ prod: {
154
+ params: {
155
+ domain: 'stages.prod.myapp.com'
156
+ }
157
+ }
158
+ },
159
+ params: {
160
+ prod: {
161
+ domain: 'params.prod.myapp.com'
162
+ }
163
+ }
164
+ }
165
+ const result = await resolver('param:domain', options, config)
166
+ assert.is(result, 'stages.prod.myapp.com')
167
+ })
168
+
169
+ test('Returns Promise that resolves to value', async () => {
170
+ const options = { param: 'test=promise-value' }
171
+ const promise = resolver('param:test', options)
172
+ assert.ok(promise instanceof Promise)
173
+ const result = await promise
174
+ assert.is(result, 'promise-value')
175
+ })
176
+
177
+ test('Handles parameter with special characters in value', async () => {
178
+ const options = { param: 'special=value-with-special-chars-!@#$%' }
179
+ const result = await resolver('param:special', options)
180
+ assert.is(result, 'value-with-special-chars-!@#$%')
181
+ })
182
+
183
+ test('Handles empty string parameter value', async () => {
184
+ const options = { param: 'empty=' }
185
+ const result = await resolver('param:empty', options)
186
+ assert.is(result, '')
187
+ })
188
+
189
+ test('Handles numeric parameter value', async () => {
190
+ const options = { param: 'port=3000' }
191
+ const result = await resolver('param:port', options)
192
+ assert.is(result, '3000')
193
+ })
194
+
195
+ test('Handles parameter with underscore', async () => {
196
+ const options = { param: 'my_param=underscore-value' }
197
+ const result = await resolver('param:my_param', options)
198
+ assert.is(result, 'underscore-value')
199
+ })
200
+
201
+ test('Handles parameter with numbers', async () => {
202
+ const options = { param: 'param123=numeric-value' }
203
+ const result = await resolver('param:param123', options)
204
+ assert.is(result, 'numeric-value')
205
+ })
206
+
207
+ test.run()