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.
- package/README.md +2067 -392
- package/cli.js +47 -9
- package/index.d.ts +1 -0
- package/package.json +1 -17
- package/src/main.js +39 -27
- package/src/parsers/index.js +3 -1
- package/src/parsers/markdown.js +69 -0
- package/src/parsers/markdown.test.js +132 -0
- package/src/resolvers/valueFromEnv.js +3 -6
- package/src/resolvers/valueFromFile.js +4 -4
- package/src/resolvers/valueFromGit.js +128 -86
- package/src/resolvers/valueFromNumber.js +10 -1
- package/src/resolvers/valueFromOptions.js +3 -7
- package/src/resolvers/valueFromParam.js +2 -1
- package/src/types.d.ts +1 -1
- package/src/utils/handleSignalEvents.js +1 -5
- package/src/utils/lodash.js +91 -37
- package/src/utils/parsing/cloudformationSchema.js +5 -10
- package/src/utils/parsing/getValueAtPath.js +111 -0
- package/src/utils/parsing/getValueAtPath.test.js +152 -0
- package/src/utils/parsing/parse.js +22 -1
- package/src/utils/parsing/preProcess.js +16 -10
- package/src/utils/regex/index.js +6 -9
- package/src/utils/ui/configWizard.js +4 -4
- package/src/utils/validation/warnIfNotFound.js +5 -1
- package/src/utils/variables/cleanVariable.js +1 -24
- package/types/src/main.d.ts +2 -0
- package/types/src/main.d.ts.map +1 -1
- package/types/src/parsers/markdown.d.ts +17 -0
- package/types/src/parsers/markdown.d.ts.map +1 -0
- package/types/src/resolvers/valueFromEnv.d.ts +1 -1
- package/types/src/resolvers/valueFromEnv.d.ts.map +1 -1
- package/types/src/resolvers/valueFromGit.d.ts.map +1 -1
- package/types/src/resolvers/valueFromNumber.d.ts +10 -2
- package/types/src/resolvers/valueFromNumber.d.ts.map +1 -1
- package/types/src/resolvers/valueFromOptions.d.ts.map +1 -1
- package/types/src/resolvers/valueFromParam.d.ts.map +1 -1
- package/types/src/utils/handleSignalEvents.d.ts.map +1 -1
- package/types/src/utils/lodash.d.ts +50 -3
- package/types/src/utils/lodash.d.ts.map +1 -1
- package/types/src/utils/parsing/getValueAtPath.d.ts +18 -0
- package/types/src/utils/parsing/getValueAtPath.d.ts.map +1 -0
- package/types/src/utils/parsing/parse.d.ts.map +1 -1
- package/types/src/utils/parsing/preProcess.d.ts.map +1 -1
- package/types/src/utils/regex/index.d.ts +5 -6
- package/types/src/utils/regex/index.d.ts.map +1 -1
- package/types/src/utils/validation/warnIfNotFound.d.ts +4 -0
- package/types/src/utils/validation/warnIfNotFound.d.ts.map +1 -1
- package/types/src/utils/variables/cleanVariable.d.ts +1 -1
- package/types/src/utils/variables/cleanVariable.d.ts.map +1 -1
- package/src/resolvers/valueFromSelf.js +0 -0
package/cli.js
CHANGED
|
@@ -7,14 +7,16 @@ const deepLog = require('./src/utils/ui/deep-log')
|
|
|
7
7
|
const { logHeader } = require('./src/utils/ui/logs')
|
|
8
8
|
const configorama = require('./src')
|
|
9
9
|
const { makeBox } = require('@davidwells/box-logger')
|
|
10
|
+
const getValueAtPath = require('./src/utils/parsing/getValueAtPath')
|
|
10
11
|
|
|
11
12
|
// Parse command line arguments
|
|
12
13
|
const argv = minimist(process.argv.slice(2), {
|
|
13
14
|
string: ['output', 'o', 'format', 'f', 'param'],
|
|
14
|
-
boolean: ['help', 'h', 'version', 'v', 'debug', 'allow-unknown', 'allow-undefined', 'list', 'info', 'verify'],
|
|
15
|
+
boolean: ['help', 'h', 'version', 'v', 'V', 'debug', 'allow-unknown', 'allow-undefined', 'list', 'info', 'verify'],
|
|
15
16
|
alias: {
|
|
16
17
|
h: 'help',
|
|
17
|
-
v: '
|
|
18
|
+
v: 'version',
|
|
19
|
+
V: 'verify',
|
|
18
20
|
o: 'output',
|
|
19
21
|
f: 'format',
|
|
20
22
|
l: 'list',
|
|
@@ -31,7 +33,7 @@ if (argv.help) {
|
|
|
31
33
|
Configorama - Variable resolution for configuration files
|
|
32
34
|
|
|
33
35
|
Usage:
|
|
34
|
-
configorama [options] <file>
|
|
36
|
+
configorama [options] <file> [path]
|
|
35
37
|
|
|
36
38
|
Options:
|
|
37
39
|
-h, --help Show this help message
|
|
@@ -40,13 +42,27 @@ Options:
|
|
|
40
42
|
-f, --format <format> Output format: json, yaml, or js (default: json)
|
|
41
43
|
-d, --debug Enable debug mode
|
|
42
44
|
-i, --info Show info about the config
|
|
43
|
-
-
|
|
45
|
+
-V, --verify Verify the config
|
|
44
46
|
--param <key=value> Pass parameter values (can be used multiple times)
|
|
45
47
|
--allow-unknown Allow unknown variables to pass through
|
|
46
48
|
--allow-undefined Allow undefined values in the final output
|
|
47
49
|
|
|
50
|
+
Path Extraction:
|
|
51
|
+
Use jq-style paths to extract specific values from the resolved config.
|
|
52
|
+
Paths can appear before or after options.
|
|
53
|
+
|
|
54
|
+
Supported syntax:
|
|
55
|
+
.foo Object key access
|
|
56
|
+
.foo.bar Nested key access
|
|
57
|
+
.[0] Array index (0-based)
|
|
58
|
+
.[-1] Negative index (from end)
|
|
59
|
+
.foo[0].bar Mixed access
|
|
60
|
+
.["key-name"] Bracket notation for special keys
|
|
61
|
+
|
|
48
62
|
Examples:
|
|
49
63
|
configorama config.yml
|
|
64
|
+
configorama config.yml .database.host
|
|
65
|
+
configorama '.servers[0].port' config.yml
|
|
50
66
|
configorama --info config.yml
|
|
51
67
|
configorama --format yaml config.json
|
|
52
68
|
configorama --output resolved.json config.yml
|
|
@@ -63,8 +79,22 @@ if (argv.version) {
|
|
|
63
79
|
process.exit(0)
|
|
64
80
|
}
|
|
65
81
|
|
|
66
|
-
//
|
|
67
|
-
|
|
82
|
+
// Parse positional args: find file path and jq-style extraction path
|
|
83
|
+
// File is first arg that exists as a file, jq path starts with '.' or '['
|
|
84
|
+
let inputFile = null
|
|
85
|
+
let extractPath = null
|
|
86
|
+
|
|
87
|
+
for (const arg of argv._) {
|
|
88
|
+
if (arg === 'setup') continue
|
|
89
|
+
|
|
90
|
+
// jq-style paths start with '.' or '['
|
|
91
|
+
if (arg.startsWith('.') || arg.startsWith('[')) {
|
|
92
|
+
extractPath = arg
|
|
93
|
+
} else if (!inputFile) {
|
|
94
|
+
inputFile = arg
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
68
98
|
if (!inputFile) {
|
|
69
99
|
console.error('Error: No input file specified')
|
|
70
100
|
console.error('Run with --help for usage information')
|
|
@@ -135,6 +165,15 @@ options.options = rest
|
|
|
135
165
|
// Process the configuration
|
|
136
166
|
configorama(inputFile, options)
|
|
137
167
|
.then((config) => {
|
|
168
|
+
// Apply path extraction if specified
|
|
169
|
+
if (extractPath) {
|
|
170
|
+
config = getValueAtPath(config, extractPath)
|
|
171
|
+
if (config === undefined) {
|
|
172
|
+
console.error(`Error: Path not found: ${extractPath}`)
|
|
173
|
+
process.exit(1)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
138
177
|
let output
|
|
139
178
|
|
|
140
179
|
// Format the output
|
|
@@ -186,10 +225,9 @@ configorama(inputFile, options)
|
|
|
186
225
|
content: error.message,
|
|
187
226
|
type: 'error',
|
|
188
227
|
})
|
|
189
|
-
console.
|
|
190
|
-
console.log(errorMsg)
|
|
228
|
+
console.error(errorMsg)
|
|
191
229
|
if (argv.debug) {
|
|
192
|
-
console.error(error
|
|
230
|
+
console.error('error', error)
|
|
193
231
|
}
|
|
194
232
|
process.exit(1)
|
|
195
233
|
})
|
package/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "configorama",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.14",
|
|
4
4
|
"description": "Variable support for configuration files",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -59,26 +59,10 @@
|
|
|
59
59
|
"jiti": "^2.4.2",
|
|
60
60
|
"js-yaml": "^3.14.1",
|
|
61
61
|
"json5": "^2.2.3",
|
|
62
|
-
"lodash.assign": "^4.2.0",
|
|
63
62
|
"lodash.camelcase": "^4.3.0",
|
|
64
|
-
"lodash.capitalize": "^4.2.1",
|
|
65
63
|
"lodash.clonedeep": "^4.5.0",
|
|
66
|
-
"lodash.flatten": "^4.4.0",
|
|
67
|
-
"lodash.includes": "^4.3.0",
|
|
68
|
-
"lodash.isarray": "^4.0.0",
|
|
69
|
-
"lodash.isdate": "^4.0.1",
|
|
70
|
-
"lodash.isempty": "^4.4.0",
|
|
71
|
-
"lodash.isfunction": "^3.0.9",
|
|
72
|
-
"lodash.isnumber": "^3.0.3",
|
|
73
|
-
"lodash.isobject": "^3.0.2",
|
|
74
|
-
"lodash.isregexp": "^4.0.1",
|
|
75
|
-
"lodash.isstring": "^4.0.1",
|
|
76
64
|
"lodash.kebabcase": "^4.1.1",
|
|
77
|
-
"lodash.map": "^4.6.0",
|
|
78
|
-
"lodash.mapvalues": "^4.6.0",
|
|
79
|
-
"lodash.split": "^4.4.2",
|
|
80
65
|
"minimist": "^1.2.8",
|
|
81
|
-
"promise.prototype.finally": "^3.1.8",
|
|
82
66
|
"safe-chalk": "^1.0.4",
|
|
83
67
|
"subscript": "^9.1.0",
|
|
84
68
|
"sync-rpc": "^1.3.6",
|
package/src/main.js
CHANGED
|
@@ -7,7 +7,6 @@ console.log = () => {}
|
|
|
7
7
|
// process.exit(1)
|
|
8
8
|
/** */
|
|
9
9
|
/* External dependencies */
|
|
10
|
-
const promiseFinallyShim = require('promise.prototype.finally').shim()
|
|
11
10
|
const findUp = require('find-up')
|
|
12
11
|
const traverse = require('traverse')
|
|
13
12
|
const dotProp = require('dot-prop')
|
|
@@ -34,7 +33,7 @@ const { arrayToJsonPath } = require('./utils/parsing/arrayToJsonPath')
|
|
|
34
33
|
const { normalizePath, extractFilePath, resolveInnerVariables } = require('./utils/paths/filePathUtils')
|
|
35
34
|
const { findLineForKey } = require('./utils/paths/findLineForKey')
|
|
36
35
|
/* Utils - regex */
|
|
37
|
-
const { combineRegexes, funcRegex } = require('./utils/regex')
|
|
36
|
+
const { combineRegexes, funcRegex, fileRefSyntax, textRefSyntax } = require('./utils/regex')
|
|
38
37
|
/* Utils - strings */
|
|
39
38
|
const formatFunctionArgs = require('./utils/strings/formatFunctionArgs')
|
|
40
39
|
const { splitByComma } = require('./utils/strings/splitByComma')
|
|
@@ -92,20 +91,18 @@ const deepRefSyntax = RegExp(/(\${)?deep:\d+(\.[^}]+)*()}?/)
|
|
|
92
91
|
const deepIndexReplacePattern = new RegExp(/^deep:|(\.[^}]+)*$/g)
|
|
93
92
|
const deepIndexPattern = /deep\:(\d*)/
|
|
94
93
|
const deepPrefixReplacePattern = /(?:^deep:)\d+\.?/g
|
|
95
|
-
const fileRefSyntax = RegExp(/^file\((~?[@\{\}\:\$a-zA-Z0-9._\-\/,'" =+]+?)\)/g)
|
|
96
|
-
const textRefSyntax = RegExp(/^text\((~?[@\{\}\:\$a-zA-Z0-9._\-\/,'" =+]+?)\)/g)
|
|
97
94
|
// TODO update file regex ^file\((~?[a-zA-Z0-9._\-\/, ]+?)\)
|
|
98
95
|
// To match file(asyncValue.js, lol) input params
|
|
99
96
|
const selfRefSyntax = RegExp(/^self:/g)
|
|
100
|
-
const base64WrapperRegex = /\[_\[([A-Za-z0-9+/=\s]*)\]_\]/g
|
|
101
97
|
const logLines = '─────────────────────────────────────────────────'
|
|
98
|
+
const evalIfPattern = /\b(eval|if)\s*\(/
|
|
99
|
+
const functionPrefixPattern = /^> function /
|
|
102
100
|
|
|
103
101
|
let DEBUG = process.argv.includes('--debug') ? true : false
|
|
104
102
|
let VERBOSE = process.argv.includes('--verbose') ? true : false
|
|
105
103
|
let SETUP_MODE = process.argv.includes('--setup') ? true : false
|
|
106
104
|
// DEBUG = true
|
|
107
105
|
let DEBUG_TYPE = false
|
|
108
|
-
const ENABLE_FUNCTIONS = true
|
|
109
106
|
|
|
110
107
|
class Configorama {
|
|
111
108
|
constructor(fileOrObject, opts) {
|
|
@@ -565,7 +562,6 @@ class Configorama {
|
|
|
565
562
|
}
|
|
566
563
|
return assign({}, value, otherValue)
|
|
567
564
|
},
|
|
568
|
-
math: () => {},
|
|
569
565
|
upperKeys: (o) => {
|
|
570
566
|
return Object.keys(o).reduce((c, k) => ((c[k.toUpperCase()] = o[k]), c), {}) // eslint-disable-line
|
|
571
567
|
},
|
|
@@ -684,6 +680,17 @@ class Configorama {
|
|
|
684
680
|
/** */
|
|
685
681
|
//process.exit(1)
|
|
686
682
|
|
|
683
|
+
// Strip body content before variable resolution, re-attach after
|
|
684
|
+
if (configObject && configObject._body !== undefined) {
|
|
685
|
+
this._markdownContent = configObject._body
|
|
686
|
+
this._markdownContentKey = '_body'
|
|
687
|
+
delete configObject._body
|
|
688
|
+
} else if (configObject && configObject._content !== undefined) {
|
|
689
|
+
this._markdownContent = configObject._content
|
|
690
|
+
this._markdownContentKey = '_content'
|
|
691
|
+
delete configObject._content
|
|
692
|
+
}
|
|
693
|
+
|
|
687
694
|
this.config = configObject
|
|
688
695
|
this.originalConfig = cloneDeep(configObject)
|
|
689
696
|
}
|
|
@@ -715,12 +722,8 @@ class Configorama {
|
|
|
715
722
|
)
|
|
716
723
|
|
|
717
724
|
if (showFoundVariables) {
|
|
718
|
-
//*
|
|
719
725
|
deepLog('metadata', metadata)
|
|
720
|
-
fs.writeFileSync(`metadata-${path.basename(this.configFilePath)}.json`, JSON.stringify(metadata, null, 2))
|
|
721
726
|
deepLog('enrich', enrich)
|
|
722
|
-
// process.exit(1)
|
|
723
|
-
/** */
|
|
724
727
|
}
|
|
725
728
|
|
|
726
729
|
const variableData = metadata.variables
|
|
@@ -1231,8 +1234,7 @@ class Configorama {
|
|
|
1231
1234
|
|
|
1232
1235
|
/* Exit early if list or info flag is set */
|
|
1233
1236
|
if (showFoundVariables) {
|
|
1234
|
-
|
|
1235
|
-
// process.exit(0)
|
|
1237
|
+
return Promise.resolve(this.config)
|
|
1236
1238
|
}
|
|
1237
1239
|
}
|
|
1238
1240
|
|
|
@@ -1240,6 +1242,9 @@ class Configorama {
|
|
|
1240
1242
|
|
|
1241
1243
|
/* If no variables found just return early */
|
|
1242
1244
|
if (this.originalString && !this.originalString.match(this.variableSyntax)) {
|
|
1245
|
+
if (this._markdownContent !== undefined) {
|
|
1246
|
+
this.originalConfig[this._markdownContentKey] = this._markdownContent
|
|
1247
|
+
}
|
|
1243
1248
|
return Promise.resolve(this.originalConfig)
|
|
1244
1249
|
}
|
|
1245
1250
|
|
|
@@ -1255,7 +1260,7 @@ class Configorama {
|
|
|
1255
1260
|
}
|
|
1256
1261
|
const stage = cliOpts.stage || providerStage || process.env.NODE_ENV || 'dev'
|
|
1257
1262
|
/* Load env variables into process.env */
|
|
1258
|
-
|
|
1263
|
+
require('env-stage-loader')({
|
|
1259
1264
|
// silent: true,
|
|
1260
1265
|
// debug: true,
|
|
1261
1266
|
env: stage,
|
|
@@ -1304,7 +1309,7 @@ class Configorama {
|
|
|
1304
1309
|
if (typeof rawValue === 'string') {
|
|
1305
1310
|
// console.log('rawValue', rawValue)
|
|
1306
1311
|
/* Process inline functions like merge() */
|
|
1307
|
-
if (
|
|
1312
|
+
if (rawValue.match(functionPrefixPattern)) {
|
|
1308
1313
|
// console.log('RAW FUNCTION', rawFunction)
|
|
1309
1314
|
const funcString = rawValue.replace(/> function /g, '')
|
|
1310
1315
|
// console.log('funcString', funcString)
|
|
@@ -1399,6 +1404,10 @@ class Configorama {
|
|
|
1399
1404
|
deepLog(this.config)
|
|
1400
1405
|
console.log()
|
|
1401
1406
|
}
|
|
1407
|
+
// Re-attach markdown body content after variable resolution
|
|
1408
|
+
if (this._markdownContent !== undefined) {
|
|
1409
|
+
this.config[this._markdownContentKey] = this._markdownContent
|
|
1410
|
+
}
|
|
1402
1411
|
return this.config
|
|
1403
1412
|
})
|
|
1404
1413
|
})
|
|
@@ -2141,9 +2150,6 @@ class Configorama {
|
|
|
2141
2150
|
|
|
2142
2151
|
historyEntry.match = matches[i].match
|
|
2143
2152
|
historyEntry.variable = matches[i].variable
|
|
2144
|
-
if (historyEntry.resultType === 'string' && historyEntry.result.match(/^>passthrough\[/)) {
|
|
2145
|
-
historyEntry.variableType = 'encodedUnknown'
|
|
2146
|
-
}
|
|
2147
2153
|
if (resolverType) {
|
|
2148
2154
|
historyEntry.variableType = resolverType
|
|
2149
2155
|
}
|
|
@@ -2156,6 +2162,9 @@ class Configorama {
|
|
|
2156
2162
|
}
|
|
2157
2163
|
|
|
2158
2164
|
historyEntry.resultType = typeof finalResult
|
|
2165
|
+
if (historyEntry.resultType === 'string' && typeof cleanResult === 'string' && cleanResult.match(/^>passthrough\[/)) {
|
|
2166
|
+
historyEntry.variableType = 'encodedUnknown'
|
|
2167
|
+
}
|
|
2159
2168
|
historyEntry.valueBeforeResolution = valueBeforeResolution
|
|
2160
2169
|
historyEntry.from = 'renderMatches'
|
|
2161
2170
|
if (isDeepResult) {
|
|
@@ -2474,7 +2483,7 @@ class Configorama {
|
|
|
2474
2483
|
|
|
2475
2484
|
// For eval/if expressions, string values need quotes unless already quoted
|
|
2476
2485
|
// BUT don't quote strings that contain variable refs (they need further resolution)
|
|
2477
|
-
if (
|
|
2486
|
+
if (evalIfPattern.test(property) && !valueToPopulate.match(this.variableSyntax)) {
|
|
2478
2487
|
const matchIdx = property.indexOf(currentMatchedString)
|
|
2479
2488
|
const charBefore = matchIdx > 0 ? property[matchIdx - 1] : ''
|
|
2480
2489
|
// Always escape quotes in values for eval/if context
|
|
@@ -2502,7 +2511,7 @@ class Configorama {
|
|
|
2502
2511
|
if (DEBUG_TYPE) console.log('DEBUG_TYPE isObject')
|
|
2503
2512
|
|
|
2504
2513
|
// For eval/if expressions, encode objects to avoid {} breaking variable syntax
|
|
2505
|
-
const isEvalOrIf =
|
|
2514
|
+
const isEvalOrIf = evalIfPattern.test(property)
|
|
2506
2515
|
if (isEvalOrIf) {
|
|
2507
2516
|
const encoded = encodeValueForEval(valueToPopulate)
|
|
2508
2517
|
property = replaceAll(matchedString, encoded, property)
|
|
@@ -2539,12 +2548,12 @@ class Configorama {
|
|
|
2539
2548
|
// console.log('other new prop', property)
|
|
2540
2549
|
|
|
2541
2550
|
// partial replacement, boolean inside eval/if expressions
|
|
2542
|
-
} else if (typeof valueToPopulate === 'boolean' &&
|
|
2551
|
+
} else if (typeof valueToPopulate === 'boolean' && evalIfPattern.test(property)) {
|
|
2543
2552
|
if (DEBUG_TYPE) console.log('DEBUG_TYPE isBoolean in eval/if')
|
|
2544
2553
|
property = replaceAll(matchedString, String(valueToPopulate), property)
|
|
2545
2554
|
|
|
2546
2555
|
// partial replacement, null inside eval/if expressions
|
|
2547
|
-
} else if (valueToPopulate === null &&
|
|
2556
|
+
} else if (valueToPopulate === null && evalIfPattern.test(property)) {
|
|
2548
2557
|
if (DEBUG_TYPE) console.log('DEBUG_TYPE isNull in eval/if')
|
|
2549
2558
|
property = replaceAll(matchedString, '__NULL__', property)
|
|
2550
2559
|
|
|
@@ -2644,13 +2653,13 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2644
2653
|
}
|
|
2645
2654
|
|
|
2646
2655
|
// console.log('prop', prop)
|
|
2647
|
-
if (property.match(
|
|
2656
|
+
if (property.match(functionPrefixPattern) && prop) {
|
|
2648
2657
|
// console.log('func prop', property)
|
|
2649
2658
|
// console.log('Prop', prop)
|
|
2650
2659
|
}
|
|
2651
2660
|
const func = funcRegex.exec(property)
|
|
2652
2661
|
// console.log('func', func)
|
|
2653
|
-
if (func && property.match(
|
|
2662
|
+
if (func && property.match(functionPrefixPattern)) {
|
|
2654
2663
|
/* IMPORTANT fix `finalProp` for nested function reference
|
|
2655
2664
|
nestedOne: 'hi'
|
|
2656
2665
|
nestedTwo: ${merge('nice', 'wow')}
|
|
@@ -2693,12 +2702,15 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2693
2702
|
&& !prop.match(getValueFromEval.match)
|
|
2694
2703
|
&& !prop.match(getValueFromIf.match)
|
|
2695
2704
|
// AND is not multiline value
|
|
2696
|
-
&& (func && prop.split('\n').length < 3)
|
|
2705
|
+
&& (func && prop.split('\n').length < 3)
|
|
2706
|
+
// Only tag as function if the function name is actually registered
|
|
2707
|
+
// Prevents resolved values like git messages "fix(scope)" from being treated as functions
|
|
2708
|
+
&& (func[1] && (this.functions[func[1]] || this.functions[func[1].toLowerCase()]))) {
|
|
2697
2709
|
// console.log('IS FUNCTION')
|
|
2698
2710
|
/* if matches function signature like ${merge('foo', 'bar')}
|
|
2699
2711
|
rewrite the variable to run the function after inputs resolved
|
|
2700
2712
|
*/
|
|
2701
|
-
const rep = property.replace(
|
|
2713
|
+
const rep = property.replace(functionPrefixPattern, '')
|
|
2702
2714
|
property = `> function ${rep}`
|
|
2703
2715
|
}
|
|
2704
2716
|
// if (prop.match(/\s\|/)) {
|
|
@@ -3610,7 +3622,7 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
3610
3622
|
return variableString
|
|
3611
3623
|
}
|
|
3612
3624
|
// console.log('runFunction', variableString)
|
|
3613
|
-
|
|
3625
|
+
const hasFunc = funcRegex.exec(variableString)
|
|
3614
3626
|
// TODO finish Function handling. Need to move this down below resolver to resolve inner refs first
|
|
3615
3627
|
// console.log('hasFunc', hasFunc)
|
|
3616
3628
|
// Skip special expressions (cron, eval, if) - these aren't user functions
|
package/src/parsers/index.js
CHANGED
|
@@ -9,6 +9,7 @@ const toml = require('./toml')
|
|
|
9
9
|
const yaml = require('./yaml')
|
|
10
10
|
const ini = require('./ini')
|
|
11
11
|
const hcl = require('./hcl')
|
|
12
|
+
const markdown = require('./markdown')
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Collection of format parsers for different config file types
|
|
@@ -19,5 +20,6 @@ module.exports = {
|
|
|
19
20
|
toml: toml,
|
|
20
21
|
yaml: yaml,
|
|
21
22
|
ini: ini,
|
|
22
|
-
hcl: hcl
|
|
23
|
+
hcl: hcl,
|
|
24
|
+
markdown: markdown
|
|
23
25
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Extracts and detects frontmatter format from markdown/MDX files
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect frontmatter syntax format from raw content
|
|
5
|
+
* @param {string} rawFrontmatter - Raw frontmatter string (without delimiters)
|
|
6
|
+
* @returns {'yaml'|'toml'|'json'} Detected format
|
|
7
|
+
*/
|
|
8
|
+
function detectSyntax(rawFrontmatter) {
|
|
9
|
+
const trimmed = rawFrontmatter.trim()
|
|
10
|
+
if (trimmed.startsWith('{')) {
|
|
11
|
+
return 'json'
|
|
12
|
+
}
|
|
13
|
+
if (/^\[[\w.-]+\]/m.test(trimmed)) {
|
|
14
|
+
return 'toml'
|
|
15
|
+
}
|
|
16
|
+
return 'yaml'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extract frontmatter and body content from a markdown file
|
|
21
|
+
* @param {string} fileContents - Full file contents
|
|
22
|
+
* @returns {{ frontmatterContent: string|null, content: string, format: 'yaml'|'toml'|'json'|null }}
|
|
23
|
+
*/
|
|
24
|
+
function extractFrontmatter(fileContents) {
|
|
25
|
+
const noMatch = { frontmatterContent: null, content: fileContents, format: null }
|
|
26
|
+
|
|
27
|
+
if (!fileContents) {
|
|
28
|
+
return noMatch
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Normalize CRLF to LF for consistent delimiter matching
|
|
32
|
+
fileContents = fileContents.replace(/\r\n/g, '\n')
|
|
33
|
+
|
|
34
|
+
// +++ delimiters → TOML
|
|
35
|
+
if (fileContents.startsWith('+++\n')) {
|
|
36
|
+
const endIdx = fileContents.indexOf('\n+++', 4)
|
|
37
|
+
if (endIdx === -1) return noMatch
|
|
38
|
+
const frontmatterContent = fileContents.slice(4, endIdx)
|
|
39
|
+
const content = fileContents.slice(endIdx + 4)
|
|
40
|
+
return { frontmatterContent, content, format: 'toml' }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- delimiters → detect format from content
|
|
44
|
+
if (fileContents.startsWith('---\n')) {
|
|
45
|
+
const endIdx = fileContents.indexOf('\n---', 4)
|
|
46
|
+
if (endIdx === -1) return noMatch
|
|
47
|
+
const frontmatterContent = fileContents.slice(4, endIdx)
|
|
48
|
+
const content = fileContents.slice(endIdx + 4)
|
|
49
|
+
const format = detectSyntax(frontmatterContent)
|
|
50
|
+
return { frontmatterContent, content, format }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// <!-- --> comment frontmatter (strict: position 0)
|
|
54
|
+
if (fileContents.startsWith('<!--\n')) {
|
|
55
|
+
const endIdx = fileContents.indexOf('\n-->', 5)
|
|
56
|
+
if (endIdx === -1) return noMatch
|
|
57
|
+
const frontmatterContent = fileContents.slice(5, endIdx)
|
|
58
|
+
const content = fileContents.slice(endIdx + 4)
|
|
59
|
+
const format = detectSyntax(frontmatterContent)
|
|
60
|
+
return { frontmatterContent, content, format }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return noMatch
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
extractFrontmatter,
|
|
68
|
+
detectSyntax
|
|
69
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/* eslint-disable no-template-curly-in-string */
|
|
2
|
+
// Unit tests for markdown frontmatter extraction and format detection
|
|
3
|
+
const { test } = require('uvu')
|
|
4
|
+
const assert = require('uvu/assert')
|
|
5
|
+
const { extractFrontmatter, detectSyntax } = require('./markdown')
|
|
6
|
+
|
|
7
|
+
// --- detectSyntax tests ---
|
|
8
|
+
|
|
9
|
+
test('detectSyntax: JSON detected by leading {', () => {
|
|
10
|
+
assert.is(detectSyntax('{ "title": "hello" }'), 'json')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('detectSyntax: TOML detected by [section] header', () => {
|
|
14
|
+
assert.is(detectSyntax('title = "hello"\n[section]\nkey = "val"'), 'toml')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('detectSyntax: TOML detected by [dotted.section] header', () => {
|
|
18
|
+
assert.is(detectSyntax('[my.config]\nkey = "val"'), 'toml')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('detectSyntax: defaults to yaml', () => {
|
|
22
|
+
assert.is(detectSyntax('title: hello\ndescription: world'), 'yaml')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('detectSyntax: yaml even with empty content', () => {
|
|
26
|
+
assert.is(detectSyntax(''), 'yaml')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// --- extractFrontmatter: YAML with --- delimiters ---
|
|
30
|
+
|
|
31
|
+
test('extract YAML frontmatter with --- delimiters', () => {
|
|
32
|
+
const input = '---\ntitle: hello\nstage: ${opt:stage}\n---\n\n# My Doc\n\nBody content here.'
|
|
33
|
+
const result = extractFrontmatter(input)
|
|
34
|
+
assert.is(result.format, 'yaml')
|
|
35
|
+
assert.is(result.frontmatterContent, 'title: hello\nstage: ${opt:stage}')
|
|
36
|
+
assert.is(result.content, '\n\n# My Doc\n\nBody content here.')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// --- extractFrontmatter: TOML with +++ delimiters ---
|
|
40
|
+
|
|
41
|
+
test('extract TOML frontmatter with +++ delimiters', () => {
|
|
42
|
+
const input = '+++\ntitle = "hello"\nstage = "${opt:stage}"\n+++\n\n# My Doc\n\nBody.'
|
|
43
|
+
const result = extractFrontmatter(input)
|
|
44
|
+
assert.is(result.format, 'toml')
|
|
45
|
+
assert.is(result.frontmatterContent, 'title = "hello"\nstage = "${opt:stage}"')
|
|
46
|
+
assert.is(result.content, '\n\n# My Doc\n\nBody.')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// --- extractFrontmatter: TOML detected inside --- delimiters ---
|
|
50
|
+
|
|
51
|
+
test('extract TOML detected inside --- delimiters via [section]', () => {
|
|
52
|
+
const input = '---\n[metadata]\ntitle = "hello"\n---\n\nBody.'
|
|
53
|
+
const result = extractFrontmatter(input)
|
|
54
|
+
assert.is(result.format, 'toml')
|
|
55
|
+
assert.is(result.frontmatterContent, '[metadata]\ntitle = "hello"')
|
|
56
|
+
assert.is(result.content, '\n\nBody.')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// --- extractFrontmatter: JSON detected inside --- delimiters ---
|
|
60
|
+
|
|
61
|
+
test('extract JSON detected inside --- delimiters via leading {', () => {
|
|
62
|
+
const input = '---\n{\n "title": "hello"\n}\n---\n\nBody.'
|
|
63
|
+
const result = extractFrontmatter(input)
|
|
64
|
+
assert.is(result.format, 'json')
|
|
65
|
+
assert.is(result.frontmatterContent, '{\n "title": "hello"\n}')
|
|
66
|
+
assert.is(result.content, '\n\nBody.')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// --- extractFrontmatter: HTML comment frontmatter ---
|
|
70
|
+
|
|
71
|
+
test('extract hidden comment frontmatter <!-- -->', () => {
|
|
72
|
+
const input = '<!--\ntitle: hello\nstage: dev\n-->\n\n# Doc\n\nBody.'
|
|
73
|
+
const result = extractFrontmatter(input)
|
|
74
|
+
assert.is(result.format, 'yaml')
|
|
75
|
+
assert.is(result.frontmatterContent, 'title: hello\nstage: dev')
|
|
76
|
+
assert.is(result.content, '\n\n# Doc\n\nBody.')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('comment frontmatter not detected when not at position 0', () => {
|
|
80
|
+
const input = '\n<!--\ntitle: hello\n-->\n\nBody.'
|
|
81
|
+
const result = extractFrontmatter(input)
|
|
82
|
+
assert.is(result.frontmatterContent, null)
|
|
83
|
+
assert.is(result.format, null)
|
|
84
|
+
assert.is(result.content, '\n<!--\ntitle: hello\n-->\n\nBody.')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// --- extractFrontmatter: no frontmatter ---
|
|
88
|
+
|
|
89
|
+
test('no frontmatter returns null + full content', () => {
|
|
90
|
+
const input = '# Just a markdown file\n\nNo frontmatter here.'
|
|
91
|
+
const result = extractFrontmatter(input)
|
|
92
|
+
assert.is(result.frontmatterContent, null)
|
|
93
|
+
assert.is(result.format, null)
|
|
94
|
+
assert.is(result.content, '# Just a markdown file\n\nNo frontmatter here.')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// --- Multiple --- in body don't confuse extraction ---
|
|
98
|
+
|
|
99
|
+
test('thematic breaks in body do not confuse extraction', () => {
|
|
100
|
+
const input = '---\ntitle: hello\n---\n\nSome text\n\n---\n\nMore text after thematic break.'
|
|
101
|
+
const result = extractFrontmatter(input)
|
|
102
|
+
assert.is(result.format, 'yaml')
|
|
103
|
+
assert.is(result.frontmatterContent, 'title: hello')
|
|
104
|
+
assert.is(result.content, '\n\nSome text\n\n---\n\nMore text after thematic break.')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// --- Edge cases ---
|
|
108
|
+
|
|
109
|
+
test('empty file returns no frontmatter', () => {
|
|
110
|
+
const result = extractFrontmatter('')
|
|
111
|
+
assert.is(result.frontmatterContent, null)
|
|
112
|
+
assert.is(result.format, null)
|
|
113
|
+
assert.is(result.content, '')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('--- only at start with no closing returns no frontmatter', () => {
|
|
117
|
+
const input = '---\ntitle: hello\nno closing delimiter'
|
|
118
|
+
const result = extractFrontmatter(input)
|
|
119
|
+
assert.is(result.frontmatterContent, null)
|
|
120
|
+
assert.is(result.format, null)
|
|
121
|
+
assert.is(result.content, '---\ntitle: hello\nno closing delimiter')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('+++ only at start with no closing returns no frontmatter', () => {
|
|
125
|
+
const input = '+++\ntitle = "hello"\nno closing delimiter'
|
|
126
|
+
const result = extractFrontmatter(input)
|
|
127
|
+
assert.is(result.frontmatterContent, null)
|
|
128
|
+
assert.is(result.format, null)
|
|
129
|
+
assert.is(result.content, '+++\ntitle = "hello"\nno closing delimiter')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test.run()
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Resolves values from process.env environment variables
|
|
2
|
+
// Matches ${env:VAR_NAME} syntax with optional fallback values
|
|
1
3
|
|
|
2
4
|
const envRefSyntax = RegExp(/^env:/g)
|
|
3
5
|
|
|
@@ -13,12 +15,7 @@ Example: \${env:MY_ENV_VAR}
|
|
|
13
15
|
`)
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
if (requestedEnvVar !== '' || '' in process.env) {
|
|
18
|
-
valueToPopulate = process.env[requestedEnvVar]
|
|
19
|
-
} else {
|
|
20
|
-
valueToPopulate = process.env
|
|
21
|
-
}
|
|
18
|
+
const valueToPopulate = process.env[requestedEnvVar]
|
|
22
19
|
return Promise.resolve(valueToPopulate)
|
|
23
20
|
}
|
|
24
21
|
|
|
@@ -121,9 +121,9 @@ async function getValueFromFile(ctx, variableString, options) {
|
|
|
121
121
|
|
|
122
122
|
// Get function input params if any supplied https://regex101.com/r/qlNFVm/1
|
|
123
123
|
// var funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*/g
|
|
124
|
-
|
|
124
|
+
const funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)/g
|
|
125
125
|
// tighter (?<![.\w-])\b(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*
|
|
126
|
-
|
|
126
|
+
const hasParams = funcParamsRegex.exec(matchedFileString)
|
|
127
127
|
|
|
128
128
|
let argsToPass = []
|
|
129
129
|
if (hasParams) {
|
|
@@ -257,8 +257,8 @@ ${JSON.stringify(options.context, null, 2)}`,
|
|
|
257
257
|
const variableFileContents = fs.readFileSync(fullFilePath, 'utf-8')
|
|
258
258
|
|
|
259
259
|
/* handle case for referencing raw JS files to inline them */
|
|
260
|
-
if (argsToPass.length
|
|
261
|
-
&&
|
|
260
|
+
if ((argsToPass.length
|
|
261
|
+
&& argsToPass[0] && typeof argsToPass[0] === 'string' && argsToPass[0].toLowerCase() === 'raw')
|
|
262
262
|
|| opts.asRawText
|
|
263
263
|
) {
|
|
264
264
|
// Encode foo() to foo__PH_PAREN_OPEN__) to avoid function collisions
|