configorama 0.9.12 → 0.9.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +2067 -392
  2. package/cli.js +47 -9
  3. package/index.d.ts +1 -0
  4. package/package.json +1 -17
  5. package/src/main.js +39 -27
  6. package/src/parsers/index.js +3 -1
  7. package/src/parsers/markdown.js +69 -0
  8. package/src/parsers/markdown.test.js +132 -0
  9. package/src/resolvers/valueFromEnv.js +3 -6
  10. package/src/resolvers/valueFromFile.js +4 -4
  11. package/src/resolvers/valueFromGit.js +128 -86
  12. package/src/resolvers/valueFromNumber.js +10 -1
  13. package/src/resolvers/valueFromOptions.js +3 -7
  14. package/src/resolvers/valueFromParam.js +2 -1
  15. package/src/types.d.ts +1 -1
  16. package/src/utils/handleSignalEvents.js +1 -5
  17. package/src/utils/lodash.js +91 -37
  18. package/src/utils/parsing/cloudformationSchema.js +5 -10
  19. package/src/utils/parsing/getValueAtPath.js +111 -0
  20. package/src/utils/parsing/getValueAtPath.test.js +152 -0
  21. package/src/utils/parsing/parse.js +22 -1
  22. package/src/utils/parsing/preProcess.js +16 -10
  23. package/src/utils/regex/index.js +6 -9
  24. package/src/utils/ui/configWizard.js +4 -4
  25. package/src/utils/validation/warnIfNotFound.js +5 -1
  26. package/src/utils/variables/cleanVariable.js +1 -24
  27. package/types/src/main.d.ts +2 -0
  28. package/types/src/main.d.ts.map +1 -1
  29. package/types/src/parsers/markdown.d.ts +17 -0
  30. package/types/src/parsers/markdown.d.ts.map +1 -0
  31. package/types/src/resolvers/valueFromEnv.d.ts +1 -1
  32. package/types/src/resolvers/valueFromEnv.d.ts.map +1 -1
  33. package/types/src/resolvers/valueFromGit.d.ts.map +1 -1
  34. package/types/src/resolvers/valueFromNumber.d.ts +10 -2
  35. package/types/src/resolvers/valueFromNumber.d.ts.map +1 -1
  36. package/types/src/resolvers/valueFromOptions.d.ts.map +1 -1
  37. package/types/src/resolvers/valueFromParam.d.ts.map +1 -1
  38. package/types/src/utils/handleSignalEvents.d.ts.map +1 -1
  39. package/types/src/utils/lodash.d.ts +50 -3
  40. package/types/src/utils/lodash.d.ts.map +1 -1
  41. package/types/src/utils/parsing/getValueAtPath.d.ts +18 -0
  42. package/types/src/utils/parsing/getValueAtPath.d.ts.map +1 -0
  43. package/types/src/utils/parsing/parse.d.ts.map +1 -1
  44. package/types/src/utils/parsing/preProcess.d.ts.map +1 -1
  45. package/types/src/utils/regex/index.d.ts +5 -6
  46. package/types/src/utils/regex/index.d.ts.map +1 -1
  47. package/types/src/utils/validation/warnIfNotFound.d.ts +4 -0
  48. package/types/src/utils/validation/warnIfNotFound.d.ts.map +1 -1
  49. package/types/src/utils/variables/cleanVariable.d.ts +1 -1
  50. package/types/src/utils/variables/cleanVariable.d.ts.map +1 -1
  51. package/src/resolvers/valueFromSelf.js +0 -0
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: 'verify',
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
- -v, --verify Verify the config
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
- // Check for input file
67
- const inputFile = argv._[0]
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.log('error', error)
190
- console.log(errorMsg)
228
+ console.error(errorMsg)
191
229
  if (argv.debug) {
192
- console.error(error.stack)
230
+ console.error('error', error)
193
231
  }
194
232
  process.exit(1)
195
233
  })
package/index.d.ts CHANGED
@@ -124,6 +124,7 @@ declare namespace configorama {
124
124
  toml: any
125
125
  ini: any
126
126
  hcl: any
127
+ markdown: any
127
128
  }
128
129
 
129
130
  /** The Configorama class for advanced usage */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "configorama",
3
- "version": "0.9.12",
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
- // TODO re-enable this
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
- const values = require('env-stage-loader')({
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 (ENABLE_FUNCTIONS && rawValue.match(/> function /)) {
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 (/\b(eval|if)\s*\(/.test(property) && !valueToPopulate.match(this.variableSyntax)) {
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 = /\b(eval|if)\s*\(/.test(property)
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' && /\b(eval|if)\s*\(/.test(property)) {
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 && /\b(eval|if)\s*\(/.test(property)) {
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(/^> function /g) && prop) {
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(/^> function /g)) {
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(/^> function /g, '')
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
- var hasFunc = funcRegex.exec(variableString)
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
@@ -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
- let valueToPopulate
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
- var funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)/g
124
+ const funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)/g
125
125
  // tighter (?<![.\w-])\b(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*
126
- var hasParams = funcParamsRegex.exec(matchedFileString)
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
- && (argsToPass && argsToPass[0] && typeof argsToPass[0] === 'string' && argsToPass[0].toLowerCase() === 'raw')
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