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/README.md +193 -13
- package/cli.js +3 -1
- package/index.d.ts +36 -3
- package/package.json +1 -1
- package/src/index.js +15 -2
- package/src/main.js +129 -47
- package/src/resolvers/valueFromFile.js +7 -1
- package/src/resolvers/valueFromParam.js +91 -0
- package/src/resolvers/valueFromParam.test.js +207 -0
- package/src/utils/PromiseTracker.js +54 -0
- package/src/utils/variables/variableUtils.js +54 -1
- package/src/utils/variables/variableUtils.test.js +44 -0
- package/types/src/index.d.ts +4 -2
- package/types/src/index.d.ts.map +1 -1
- package/types/src/main.d.ts +18 -0
- package/types/src/main.d.ts.map +1 -1
- package/types/src/resolvers/valueFromFile.d.ts.map +1 -1
- package/types/src/resolvers/valueFromParam.d.ts +19 -0
- package/types/src/resolvers/valueFromParam.d.ts.map +1 -0
- package/types/src/utils/PromiseTracker.d.ts +4 -0
- package/types/src/utils/PromiseTracker.d.ts.map +1 -1
- package/types/src/utils/variables/variableUtils.d.ts +9 -0
- package/types/src/utils/variables/variableUtils.d.ts.map +1 -1
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 {
|
|
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
|
|
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 {
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
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 ->
|
|
151
|
-
if (options.allowUnknownVars !== undefined && options.
|
|
152
|
-
this.settings.
|
|
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 = '
|
|
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:
|
|
1945
|
-
opt:
|
|
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.
|
|
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
|
|
2868
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
3152
|
-
if (
|
|
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
|
|
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()
|