configorama 0.9.11 → 0.9.13
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 +1 -0
- package/cli.js +46 -8
- package/index.d.ts +38 -29
- package/package.json +1 -17
- package/src/main.js +23 -9
- 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/valueFromGit.js +27 -24
- package/src/resolvers/valueFromNumber.js +10 -1
- package/src/types.d.ts +1 -1
- package/src/utils/handleSignalEvents.js +1 -5
- package/src/utils/lodash.js +72 -18
- 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 +21 -0
- package/src/utils/regex/index.js +5 -0
- package/src/utils/ui/configWizard.js +4 -4
- package/src/utils/validation/warnIfNotFound.js +5 -1
- package/src/utils/variables/cleanVariable.js +1 -3
- 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/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/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/regex/index.d.ts +2 -0
- 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/README.md
CHANGED
|
@@ -283,6 +283,7 @@ Supported file types (extensions are case-insensitive):
|
|
|
283
283
|
| TOML | `.toml`, `.tml` |
|
|
284
284
|
| INI | `.ini` |
|
|
285
285
|
| JSON | `.json`, `.json5`, `.jsonc` |
|
|
286
|
+
| Markdown | `.md`, `.mdx`, `.markdown`, `.mdown`, `.mkdn`, `.mkd` |
|
|
286
287
|
|
|
287
288
|
### Sync/Async file references
|
|
288
289
|
|
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.log('error', error)
|
|
190
228
|
console.log(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
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
// Type definitions for configorama
|
|
2
2
|
// Project: https://github.com/DavidWells/configorama
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
export * from './src/types'
|
|
6
|
-
|
|
7
|
-
export interface ConfigoramaSettings {
|
|
4
|
+
interface ConfigoramaSettings {
|
|
8
5
|
/** Options to populate for ${opt:xyz}. These could be CLI flags */
|
|
9
6
|
options?: Record<string, any>
|
|
10
7
|
/** Regex of variable syntax */
|
|
@@ -64,7 +61,7 @@ export interface ConfigoramaSettings {
|
|
|
64
61
|
filePathOverrides?: Record<string, string>
|
|
65
62
|
}
|
|
66
63
|
|
|
67
|
-
|
|
64
|
+
interface ConfigoramaResult<T = any> {
|
|
68
65
|
/** The variable syntax pattern used */
|
|
69
66
|
variableSyntax: RegExp
|
|
70
67
|
/** Map of variable types found */
|
|
@@ -83,7 +80,7 @@ export interface ConfigoramaResult<T = any> {
|
|
|
83
80
|
* Context passed to JS/TS/ESM config file functions
|
|
84
81
|
* Used when config files export a function: `export default function(ctx) { ... }`
|
|
85
82
|
*/
|
|
86
|
-
|
|
83
|
+
interface ConfigContext<T = any> {
|
|
87
84
|
/** The original unresolved configuration object */
|
|
88
85
|
originalConfig: T
|
|
89
86
|
/** The current (partially resolved) configuration object */
|
|
@@ -104,28 +101,40 @@ declare function configorama<T = any>(
|
|
|
104
101
|
settings: ConfigoramaSettings & { returnMetadata: true }
|
|
105
102
|
): Promise<ConfigoramaResult<T>>
|
|
106
103
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
104
|
+
declare namespace configorama {
|
|
105
|
+
// Re-export types for consumers
|
|
106
|
+
export { ConfigoramaSettings, ConfigoramaResult, ConfigContext }
|
|
107
|
+
|
|
108
|
+
/** Configorama sync API */
|
|
109
|
+
export function sync<T = any>(
|
|
110
|
+
configPathOrObject: string | object,
|
|
111
|
+
settings?: ConfigoramaSettings
|
|
112
|
+
): T
|
|
113
|
+
|
|
114
|
+
/** Analyze config variables without resolving them */
|
|
115
|
+
export function analyze(
|
|
116
|
+
configPathOrObject: string | object,
|
|
117
|
+
settings?: ConfigoramaSettings
|
|
118
|
+
): Promise<any>
|
|
119
|
+
|
|
120
|
+
/** Format utilities for parsing various config formats */
|
|
121
|
+
export const format: {
|
|
122
|
+
yaml: any
|
|
123
|
+
json: any
|
|
124
|
+
toml: any
|
|
125
|
+
ini: any
|
|
126
|
+
hcl: any
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The Configorama class for advanced usage */
|
|
130
|
+
export const Configorama: any
|
|
131
|
+
|
|
132
|
+
/** Build variable syntax regex */
|
|
133
|
+
export function buildVariableSyntax(
|
|
134
|
+
prefix?: string,
|
|
135
|
+
suffix?: string,
|
|
136
|
+
excludePatterns?: string[]
|
|
137
|
+
): string
|
|
128
138
|
}
|
|
129
139
|
|
|
130
|
-
|
|
131
|
-
export const Configorama: any
|
|
140
|
+
export = configorama
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "configorama",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.13",
|
|
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,8 +91,6 @@ 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)
|
|
@@ -684,6 +681,17 @@ class Configorama {
|
|
|
684
681
|
/** */
|
|
685
682
|
//process.exit(1)
|
|
686
683
|
|
|
684
|
+
// Strip body content before variable resolution, re-attach after
|
|
685
|
+
if (configObject && configObject._body !== undefined) {
|
|
686
|
+
this._markdownContent = configObject._body
|
|
687
|
+
this._markdownContentKey = '_body'
|
|
688
|
+
delete configObject._body
|
|
689
|
+
} else if (configObject && configObject._content !== undefined) {
|
|
690
|
+
this._markdownContent = configObject._content
|
|
691
|
+
this._markdownContentKey = '_content'
|
|
692
|
+
delete configObject._content
|
|
693
|
+
}
|
|
694
|
+
|
|
687
695
|
this.config = configObject
|
|
688
696
|
this.originalConfig = cloneDeep(configObject)
|
|
689
697
|
}
|
|
@@ -715,12 +723,8 @@ class Configorama {
|
|
|
715
723
|
)
|
|
716
724
|
|
|
717
725
|
if (showFoundVariables) {
|
|
718
|
-
//*
|
|
719
726
|
deepLog('metadata', metadata)
|
|
720
|
-
fs.writeFileSync(`metadata-${path.basename(this.configFilePath)}.json`, JSON.stringify(metadata, null, 2))
|
|
721
727
|
deepLog('enrich', enrich)
|
|
722
|
-
// process.exit(1)
|
|
723
|
-
/** */
|
|
724
728
|
}
|
|
725
729
|
|
|
726
730
|
const variableData = metadata.variables
|
|
@@ -1240,6 +1244,9 @@ class Configorama {
|
|
|
1240
1244
|
|
|
1241
1245
|
/* If no variables found just return early */
|
|
1242
1246
|
if (this.originalString && !this.originalString.match(this.variableSyntax)) {
|
|
1247
|
+
if (this._markdownContent !== undefined) {
|
|
1248
|
+
this.originalConfig[this._markdownContentKey] = this._markdownContent
|
|
1249
|
+
}
|
|
1243
1250
|
return Promise.resolve(this.originalConfig)
|
|
1244
1251
|
}
|
|
1245
1252
|
|
|
@@ -1399,6 +1406,10 @@ class Configorama {
|
|
|
1399
1406
|
deepLog(this.config)
|
|
1400
1407
|
console.log()
|
|
1401
1408
|
}
|
|
1409
|
+
// Re-attach markdown body content after variable resolution
|
|
1410
|
+
if (this._markdownContent !== undefined) {
|
|
1411
|
+
this.config[this._markdownContentKey] = this._markdownContent
|
|
1412
|
+
}
|
|
1402
1413
|
return this.config
|
|
1403
1414
|
})
|
|
1404
1415
|
})
|
|
@@ -2693,7 +2704,10 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2693
2704
|
&& !prop.match(getValueFromEval.match)
|
|
2694
2705
|
&& !prop.match(getValueFromIf.match)
|
|
2695
2706
|
// AND is not multiline value
|
|
2696
|
-
&& (func && prop.split('\n').length < 3)
|
|
2707
|
+
&& (func && prop.split('\n').length < 3)
|
|
2708
|
+
// Only tag as function if the function name is actually registered
|
|
2709
|
+
// Prevents resolved values like git messages "fix(scope)" from being treated as functions
|
|
2710
|
+
&& (func[1] && (this.functions[func[1]] || this.functions[func[1].toLowerCase()]))) {
|
|
2697
2711
|
// console.log('IS FUNCTION')
|
|
2698
2712
|
/* if matches function signature like ${merge('foo', 'bar')}
|
|
2699
2713
|
rewrite the variable to run the function after inputs resolved
|
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()
|
|
@@ -27,6 +27,24 @@ async function _exec(cmd, options = { timeout: 1000 }) {
|
|
|
27
27
|
})
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Execute a command with arguments array (safe from shell injection)
|
|
32
|
+
* @param {string} command - Command to execute
|
|
33
|
+
* @param {string[]} args - Arguments array
|
|
34
|
+
* @param {import('child_process').ExecFileOptions} [options] - ExecFile options
|
|
35
|
+
* @returns {Promise<string>}
|
|
36
|
+
*/
|
|
37
|
+
async function _execFile(command, args, options = { timeout: 1000 }) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
childProcess.execFile(command, args, options, (err, stdout) => {
|
|
40
|
+
if (err) {
|
|
41
|
+
return reject(err)
|
|
42
|
+
}
|
|
43
|
+
return resolve(String(stdout).trim())
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
30
48
|
// TODO denote computed fields in metadata
|
|
31
49
|
/*
|
|
32
50
|
{
|
|
@@ -230,28 +248,16 @@ async function getGitTimestamp(_file, cwd, throwOnMissing = true) {
|
|
|
230
248
|
throw new Error('File path must be a string')
|
|
231
249
|
}
|
|
232
250
|
|
|
233
|
-
//
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
// /\.\./, // Directory traversal
|
|
238
|
-
// /^[\/\\]/, // Absolute paths
|
|
239
|
-
/[\x00-\x1f\x7f-\x9f]/ // Control characters
|
|
240
|
-
]
|
|
241
|
-
|
|
242
|
-
if (dangerousPatterns.some(pattern => pattern.test(_file))) {
|
|
243
|
-
throw new Error('Invalid characters or pattern in file path')
|
|
244
|
-
}
|
|
251
|
+
// Strip surrounding quotes and leading slash
|
|
252
|
+
const file = _file
|
|
253
|
+
.replace(/^['"]|['"]$/g, '')
|
|
254
|
+
.replace(/^\//, '')
|
|
245
255
|
|
|
246
|
-
//
|
|
247
|
-
if (
|
|
256
|
+
// Reject control characters
|
|
257
|
+
if (/[\x00-\x1f\x7f-\x9f]/.test(file)) {
|
|
248
258
|
throw new Error('File path contains invalid characters')
|
|
249
259
|
}
|
|
250
260
|
|
|
251
|
-
// Normalize path and remove leading slash
|
|
252
|
-
const file = _file
|
|
253
|
-
.replace(/^\//, '')
|
|
254
|
-
|
|
255
261
|
const cachedTimestamp = cache.get(file)
|
|
256
262
|
if (cachedTimestamp) return cachedTimestamp
|
|
257
263
|
|
|
@@ -263,10 +269,7 @@ async function getGitTimestamp(_file, cwd, throwOnMissing = true) {
|
|
|
263
269
|
}
|
|
264
270
|
|
|
265
271
|
try {
|
|
266
|
-
const
|
|
267
|
-
// console.log('cmd', cmd)
|
|
268
|
-
// console.log('cwd', cwd)
|
|
269
|
-
const output = await _exec(cmd, { cwd })
|
|
272
|
+
const output = await _execFile('git', ['log', '-1', '--pretty=%ai', '--', file], { cwd })
|
|
270
273
|
const date = new Date(output)
|
|
271
274
|
const dateString = date.toISOString()
|
|
272
275
|
cache.set(file, dateString)
|
|
@@ -281,8 +284,8 @@ async function getGitTimestamp(_file, cwd, throwOnMissing = true) {
|
|
|
281
284
|
}
|
|
282
285
|
|
|
283
286
|
try {
|
|
284
|
-
const backupFile = path.join(projectRoot,
|
|
285
|
-
const output = await
|
|
287
|
+
const backupFile = path.join(projectRoot, file)
|
|
288
|
+
const output = await _execFile('git', ['log', '-1', '--pretty=%ai', '--', backupFile], { cwd: projectRoot })
|
|
286
289
|
const date = new Date(output)
|
|
287
290
|
const dateString = date.toISOString()
|
|
288
291
|
cache.set(file, dateString)
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
// Resolves numeric literal variables to their Number values
|
|
2
|
+
const { isNumber } = require('../utils/lodash')
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} variableString
|
|
6
|
+
* @returns {boolean}
|
|
7
|
+
*/
|
|
3
8
|
function isNumberVariable(variableString) {
|
|
4
9
|
if (!variableString || variableString.trim().length === 0) {
|
|
5
10
|
return false
|
|
@@ -8,6 +13,10 @@ function isNumberVariable(variableString) {
|
|
|
8
13
|
return !isNaN(num) && isNumber(num)
|
|
9
14
|
}
|
|
10
15
|
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} variableString
|
|
18
|
+
* @returns {Promise<number>}
|
|
19
|
+
*/
|
|
11
20
|
function getValueFromNumber(variableString) {
|
|
12
21
|
return Promise.resolve(Number(variableString))
|
|
13
22
|
}
|
package/src/types.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// This file provides TypeScript support for validating configuration variables
|
|
3
3
|
|
|
4
4
|
// Valid variable prefixes supported by configorama
|
|
5
|
-
export type KnownVariablePrefix = 'env:' | 'opt:' | 'self:' | 'file:' | 'git:' | 'cron:'
|
|
5
|
+
export type KnownVariablePrefix = 'env:' | 'opt:' | 'self:' | 'file:' | 'git:' | 'cron:' | 'param:'
|
|
6
6
|
|
|
7
7
|
// Quoted string literal type for fallback values
|
|
8
8
|
type QuotedString = `"${string}"` | `'${string}'`
|