configorama 0.11.0 → 1.0.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 +429 -123
- package/cli.js +282 -49
- package/index.d.ts +43 -1
- package/package.json +5 -1
- package/src/capabilities.js +59 -0
- package/src/capabilities.test.js +44 -0
- package/src/display.js +70 -7
- package/src/display.test.js +82 -0
- package/src/errors.js +73 -0
- package/src/index.js +91 -1
- package/src/main.js +159 -19
- package/src/parsers/esm.js +1 -16
- package/src/parsers/typescript.js +1 -48
- package/src/resolvers/valueFromCron.js +4 -25
- package/src/resolvers/valueFromEval.js +11 -1
- package/src/resolvers/valueFromFile.js +8 -1
- package/src/resolvers/valueFromGit.js +43 -17
- package/src/resolvers/valueFromOptions.js +5 -4
- package/src/utils/filters/filterArgs.js +57 -0
- package/src/utils/filters/oneOf.js +77 -0
- package/src/utils/introspection/audit.js +78 -0
- package/src/utils/introspection/graph.js +43 -0
- package/src/utils/introspection/model.js +150 -0
- package/src/utils/introspection/model.test.js +93 -0
- package/src/utils/parsing/commentAnnotations.js +107 -0
- package/src/utils/parsing/commentAnnotations.test.js +123 -0
- package/src/utils/parsing/enrichMetadata.js +64 -1
- package/src/utils/parsing/enrichMetadata.test.js +84 -0
- package/src/utils/parsing/extractComment.js +145 -0
- package/src/utils/parsing/extractComment.test.js +182 -0
- package/src/utils/parsing/preProcess.js +2 -1
- package/src/utils/paths/findLineForKey.js +2 -2
- package/src/utils/paths/ignorePaths.js +22 -9
- package/src/utils/redaction/redact.js +78 -0
- package/src/utils/redaction/redact.test.js +38 -0
- package/src/utils/redaction/setupRedaction.js +47 -0
- package/src/utils/redaction/setupRedaction.test.js +68 -0
- package/src/utils/requirements/configRequirements.js +351 -0
- package/src/utils/requirements/configRequirements.test.js +380 -0
- package/src/utils/requirements/serializeRequirements.js +120 -0
- package/src/utils/requirements/serializeRequirements.test.js +211 -0
- package/src/utils/security/evalSafety.js +86 -0
- package/src/utils/security/evalSafety.test.js +61 -0
- package/src/utils/security/safetyPolicy.js +110 -0
- package/src/utils/security/safetyPolicy.test.js +29 -0
- package/src/utils/strings/didYouMean.js +70 -0
- package/src/utils/strings/didYouMean.test.js +52 -0
- package/src/utils/strings/formatFunctionArgs.js +6 -1
- package/src/utils/strings/splitByComma.js +5 -0
- package/src/utils/ui/configWizard.js +208 -34
- package/src/utils/ui/createEditorLink.js +17 -1
- package/src/utils/ui/promptDescriptors.js +196 -0
- package/src/utils/ui/promptDescriptors.test.js +162 -0
- package/src/utils/variables/cleanVariable.js +22 -0
- package/src/utils/variables/getVariableType.js +1 -0
- package/types/src/index.d.ts +0 -24
- package/types/src/index.d.ts.map +1 -1
- package/types/src/main.d.ts +16 -8
- package/types/src/main.d.ts.map +1 -1
- package/types/src/resolvers/valueFromFile.d.ts +0 -2
- package/types/src/resolvers/valueFromFile.d.ts.map +1 -1
- package/types/src/resolvers/valueFromGit.d.ts.map +1 -1
- package/types/src/resolvers/valueFromSelf.d.ts +1 -0
- package/types/src/resolvers/valueFromSelf.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/paths/findLineForKey.d.ts +0 -9
- package/types/src/utils/paths/findLineForKey.d.ts.map +1 -1
- package/types/src/utils/strings/replaceAll.d.ts.map +1 -1
- package/types/src/utils/variables/variableUtils.d.ts +1 -1
- package/types/src/display.d.ts +0 -62
- package/types/src/display.d.ts.map +0 -1
- package/types/src/metadata.d.ts +0 -28
- package/types/src/metadata.d.ts.map +0 -1
- package/types/src/utils/BoundedMap.d.ts +0 -10
- package/types/src/utils/BoundedMap.d.ts.map +0 -1
- package/types/src/utils/paths/ignorePaths.d.ts +0 -5
- package/types/src/utils/paths/ignorePaths.d.ts.map +0 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const KNOWN_TAGS = new Set([
|
|
2
|
+
'description',
|
|
3
|
+
'from',
|
|
4
|
+
'example',
|
|
5
|
+
'default',
|
|
6
|
+
'sensitive',
|
|
7
|
+
'group',
|
|
8
|
+
'deprecated',
|
|
9
|
+
])
|
|
10
|
+
|
|
11
|
+
function toLines(input) {
|
|
12
|
+
if (Array.isArray(input)) return input
|
|
13
|
+
if (input === undefined || input === null) return []
|
|
14
|
+
return String(input).split(/\r?\n/)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function firstNonEmpty(values) {
|
|
18
|
+
return (values || []).find(value => value !== undefined && value !== null && String(value).trim() !== '')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function uniqueNonEmpty(values) {
|
|
22
|
+
const seen = new Set()
|
|
23
|
+
const result = []
|
|
24
|
+
for (const value of values || []) {
|
|
25
|
+
if (value === undefined || value === null) continue
|
|
26
|
+
const normalized = String(value).trim()
|
|
27
|
+
if (!normalized || seen.has(normalized)) continue
|
|
28
|
+
seen.add(normalized)
|
|
29
|
+
result.push(normalized)
|
|
30
|
+
}
|
|
31
|
+
return result
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseSensitive(value) {
|
|
35
|
+
if (value === undefined || value === null) return undefined
|
|
36
|
+
const normalized = String(value).trim().toLowerCase()
|
|
37
|
+
if (normalized === 'true') return true
|
|
38
|
+
if (normalized === 'false') return false
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeAnnotations(tags, plainText) {
|
|
43
|
+
const descriptionFromTag = uniqueNonEmpty(tags.description).join(' ')
|
|
44
|
+
const obtainHint = firstNonEmpty(tags.from)
|
|
45
|
+
const examples = uniqueNonEmpty(tags.example)
|
|
46
|
+
const defaultHint = firstNonEmpty(tags.default)
|
|
47
|
+
const sensitive = parseSensitive(firstNonEmpty(tags.sensitive))
|
|
48
|
+
const group = firstNonEmpty(tags.group)
|
|
49
|
+
const deprecationMessage = firstNonEmpty(tags.deprecated)
|
|
50
|
+
|
|
51
|
+
const annotations = {}
|
|
52
|
+
if (descriptionFromTag) annotations.description = descriptionFromTag
|
|
53
|
+
if (obtainHint) annotations.obtainHint = String(obtainHint).trim()
|
|
54
|
+
if (examples.length) annotations.examples = examples
|
|
55
|
+
if (defaultHint) annotations.defaultHint = String(defaultHint).trim()
|
|
56
|
+
if (sensitive !== undefined) annotations.sensitive = sensitive
|
|
57
|
+
if (group) annotations.group = String(group).trim()
|
|
58
|
+
if (deprecationMessage) annotations.deprecationMessage = String(deprecationMessage).trim()
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
annotations,
|
|
62
|
+
description: annotations.description || plainText || null,
|
|
63
|
+
descriptionSource: annotations.description ? 'commentTag' : null,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseCommentAnnotations(input) {
|
|
68
|
+
const tags = {}
|
|
69
|
+
const plainLines = []
|
|
70
|
+
|
|
71
|
+
for (const line of toLines(input)) {
|
|
72
|
+
const text = String(line || '').trim()
|
|
73
|
+
if (!text) continue
|
|
74
|
+
|
|
75
|
+
const match = text.match(/^@([a-z][a-z0-9_-]*)\b([\s\S]*)$/)
|
|
76
|
+
if (!match) {
|
|
77
|
+
plainLines.push(text)
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const tagName = match[1]
|
|
82
|
+
if (!KNOWN_TAGS.has(tagName)) {
|
|
83
|
+
plainLines.push(text)
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!tags[tagName]) tags[tagName] = []
|
|
88
|
+
tags[tagName].push(match[2].replace(/^\s/, ''))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const plainText = plainLines.join(' ').trim() || null
|
|
92
|
+
const normalized = normalizeAnnotations(tags, plainText)
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
plainText,
|
|
96
|
+
tags,
|
|
97
|
+
annotations: normalized.annotations,
|
|
98
|
+
description: normalized.description,
|
|
99
|
+
descriptionSource: normalized.descriptionSource,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
KNOWN_TAGS,
|
|
105
|
+
parseCommentAnnotations,
|
|
106
|
+
parseSensitive,
|
|
107
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const { test } = require('uvu')
|
|
2
|
+
const assert = require('uvu/assert')
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const { parseCommentAnnotations, parseSensitive } = require('./commentAnnotations')
|
|
6
|
+
|
|
7
|
+
test('parseCommentAnnotations parses supported tags into normalized annotation fields', () => {
|
|
8
|
+
const result = parseCommentAnnotations([
|
|
9
|
+
'@description Stripe live secret key',
|
|
10
|
+
'@from Stripe dashboard > Developers > API keys',
|
|
11
|
+
'@example sk_live_...',
|
|
12
|
+
'@default Set this in CI',
|
|
13
|
+
'@sensitive true',
|
|
14
|
+
'@group Payments',
|
|
15
|
+
'@deprecated Use STRIPE_RESTRICTED_KEY instead',
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
assert.equal(result.tags, {
|
|
19
|
+
description: ['Stripe live secret key'],
|
|
20
|
+
from: ['Stripe dashboard > Developers > API keys'],
|
|
21
|
+
example: ['sk_live_...'],
|
|
22
|
+
default: ['Set this in CI'],
|
|
23
|
+
sensitive: ['true'],
|
|
24
|
+
group: ['Payments'],
|
|
25
|
+
deprecated: ['Use STRIPE_RESTRICTED_KEY instead'],
|
|
26
|
+
})
|
|
27
|
+
assert.equal(result.annotations, {
|
|
28
|
+
description: 'Stripe live secret key',
|
|
29
|
+
obtainHint: 'Stripe dashboard > Developers > API keys',
|
|
30
|
+
examples: ['sk_live_...'],
|
|
31
|
+
defaultHint: 'Set this in CI',
|
|
32
|
+
sensitive: true,
|
|
33
|
+
group: 'Payments',
|
|
34
|
+
deprecationMessage: 'Use STRIPE_RESTRICTED_KEY instead',
|
|
35
|
+
})
|
|
36
|
+
assert.is(result.description, 'Stripe live secret key')
|
|
37
|
+
assert.is(result.descriptionSource, 'commentTag')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('parseCommentAnnotations keeps plain text separate from tag lines', () => {
|
|
41
|
+
const result = parseCommentAnnotations([
|
|
42
|
+
'Stripe live secret key',
|
|
43
|
+
'@from Stripe dashboard > Developers > API keys',
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
assert.is(result.plainText, 'Stripe live secret key')
|
|
47
|
+
assert.is(result.description, 'Stripe live secret key')
|
|
48
|
+
assert.is(result.descriptionSource, null)
|
|
49
|
+
assert.equal(result.annotations, {
|
|
50
|
+
obtainHint: 'Stripe dashboard > Developers > API keys',
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('parseCommentAnnotations treats unknown tag-shaped lines as plain description text', () => {
|
|
55
|
+
const result = parseCommentAnnotations([
|
|
56
|
+
'@david rotate this key after launch',
|
|
57
|
+
'ping @ops before rotating this key',
|
|
58
|
+
])
|
|
59
|
+
|
|
60
|
+
assert.is(result.plainText, '@david rotate this key after launch ping @ops before rotating this key')
|
|
61
|
+
assert.is(result.description, '@david rotate this key after launch ping @ops before rotating this key')
|
|
62
|
+
assert.equal(result.tags, {})
|
|
63
|
+
assert.equal(result.annotations, {})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('parseCommentAnnotations preserves empty tag values in raw tags and omits normalized empty fields', () => {
|
|
67
|
+
const result = parseCommentAnnotations([
|
|
68
|
+
'@from',
|
|
69
|
+
'@default ',
|
|
70
|
+
'@group Platform',
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
assert.equal(result.tags, {
|
|
74
|
+
from: [''],
|
|
75
|
+
default: [''],
|
|
76
|
+
group: ['Platform'],
|
|
77
|
+
})
|
|
78
|
+
assert.equal(result.annotations, {
|
|
79
|
+
group: 'Platform',
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('parseCommentAnnotations joins repeated descriptions and dedupes examples', () => {
|
|
84
|
+
const result = parseCommentAnnotations([
|
|
85
|
+
'@description Stripe live',
|
|
86
|
+
'@description secret key',
|
|
87
|
+
'@example sk_live_...',
|
|
88
|
+
'@example sk_live_...',
|
|
89
|
+
'@example stripe-key',
|
|
90
|
+
])
|
|
91
|
+
|
|
92
|
+
assert.is(result.annotations.description, 'Stripe live secret key')
|
|
93
|
+
assert.equal(result.annotations.examples, ['sk_live_...', 'stripe-key'])
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('parseSensitive accepts only true and false case-insensitively', () => {
|
|
97
|
+
assert.is(parseSensitive('true'), true)
|
|
98
|
+
assert.is(parseSensitive('TRUE'), true)
|
|
99
|
+
assert.is(parseSensitive('false'), false)
|
|
100
|
+
assert.is(parseSensitive('False'), false)
|
|
101
|
+
assert.is(parseSensitive('yes'), undefined)
|
|
102
|
+
assert.is(parseSensitive('1'), undefined)
|
|
103
|
+
assert.is(parseSensitive(''), undefined)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('parseCommentAnnotations ignores invalid sensitive values so heuristics can apply later', () => {
|
|
107
|
+
const result = parseCommentAnnotations([
|
|
108
|
+
'@sensitive yes',
|
|
109
|
+
'@description API token',
|
|
110
|
+
])
|
|
111
|
+
|
|
112
|
+
assert.equal(result.tags.sensitive, ['yes'])
|
|
113
|
+
assert.equal(result.annotations, {
|
|
114
|
+
description: 'API token',
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('comment annotation parser does not require jsdoc-parser', () => {
|
|
119
|
+
const source = fs.readFileSync(path.join(__dirname, 'commentAnnotations.js'), 'utf8')
|
|
120
|
+
assert.not.match(source, /jsdoc-parser|doxxx|jsdoctypeparser/)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test.run()
|
|
@@ -3,6 +3,8 @@ const fs = require('fs')
|
|
|
3
3
|
const path = require('path')
|
|
4
4
|
const { normalizePath, extractFilePath, normalizeFileVariable, resolveInnerVariables } = require('../paths/filePathUtils')
|
|
5
5
|
const { preResolveString, preResolveSingle } = require('../resolution/preResolveVariable')
|
|
6
|
+
const { parseOneOfFilter } = require('../filters/oneOf')
|
|
7
|
+
const { extractComment } = require('./extractComment')
|
|
6
8
|
|
|
7
9
|
// Type filters that indicate expected value types
|
|
8
10
|
const TYPE_FILTERS = ['Boolean', 'String', 'Number', 'Array', 'Object', 'Json']
|
|
@@ -61,6 +63,8 @@ function createOccurrence(instance, varMatch, options = {}) {
|
|
|
61
63
|
|
|
62
64
|
// Extract type from filters
|
|
63
65
|
const type = extractTypeFromFilters(filters)
|
|
66
|
+
const oneOfFilter = filters && filters.find(filter => typeof filter === 'string' && filter.match(/^oneOf\(/))
|
|
67
|
+
const oneOf = parseOneOfFilter(oneOfFilter)
|
|
64
68
|
|
|
65
69
|
const occurrence = {
|
|
66
70
|
originalString: instance.originalStringValue,
|
|
@@ -79,6 +83,11 @@ function createOccurrence(instance, varMatch, options = {}) {
|
|
|
79
83
|
|
|
80
84
|
if (description) {
|
|
81
85
|
occurrence.description = description
|
|
86
|
+
occurrence.descriptionSource = 'help'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (oneOf && oneOf.allowedValues) {
|
|
90
|
+
occurrence.allowedValues = oneOf.allowedValues
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
if (instance.defaultValueSrc) {
|
|
@@ -133,6 +142,56 @@ async function enrichMetadata(
|
|
|
133
142
|
// Create resolve context early for resolving descriptions
|
|
134
143
|
const configDir = configPath ? path.dirname(configPath) : undefined
|
|
135
144
|
const resolveContext = { config: originalConfig, variableSyntax, configDir, options }
|
|
145
|
+
const commentFileType = configPath ? path.extname(configPath) : ''
|
|
146
|
+
let commentLines = []
|
|
147
|
+
if (configPath) {
|
|
148
|
+
try {
|
|
149
|
+
commentLines = fs.readFileSync(configPath, 'utf8').split('\n')
|
|
150
|
+
} catch (error) {
|
|
151
|
+
commentLines = []
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function applyCommentFallback(occurrence) {
|
|
156
|
+
if (!occurrence) return occurrence
|
|
157
|
+
const comment = extractComment(occurrence.path, commentLines, commentFileType)
|
|
158
|
+
if (comment) {
|
|
159
|
+
if (comment.description && (!occurrence.description || comment.descriptionSource === 'commentTag')) {
|
|
160
|
+
occurrence.description = comment.description
|
|
161
|
+
occurrence.descriptionSource = comment.descriptionSource
|
|
162
|
+
}
|
|
163
|
+
if (comment.obtainHint) occurrence.obtainHint = comment.obtainHint
|
|
164
|
+
if (comment.examples) occurrence.examples = comment.examples
|
|
165
|
+
if (comment.defaultHint) occurrence.defaultHint = comment.defaultHint
|
|
166
|
+
if (comment.sensitive !== undefined) {
|
|
167
|
+
occurrence.sensitive = comment.sensitive
|
|
168
|
+
occurrence.sensitiveSource = 'commentTag'
|
|
169
|
+
}
|
|
170
|
+
if (comment.group) occurrence.group = comment.group
|
|
171
|
+
if (comment.deprecationMessage) occurrence.deprecationMessage = comment.deprecationMessage
|
|
172
|
+
}
|
|
173
|
+
return occurrence
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function applyDynamicOneOfAllowedValues(occurrence) {
|
|
177
|
+
if (!occurrence || occurrence.allowedValues || !occurrence.filters) return occurrence
|
|
178
|
+
const oneOfFilter = occurrence.filters.find(filter => typeof filter === 'string' && filter.match(/^oneOf\(/))
|
|
179
|
+
if (!oneOfFilter) return occurrence
|
|
180
|
+
|
|
181
|
+
const match = oneOfFilter.match(/^oneOf\(\s*\$\{([^}]+)\}\s*\)$/)
|
|
182
|
+
if (!match) return occurrence
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const resolved = await preResolveSingle(match[1], resolveContext)
|
|
186
|
+
if (Array.isArray(resolved)) {
|
|
187
|
+
occurrence.allowedValues = resolved.map(String)
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
// Leave unresolved dynamic oneOf args without allowedValues; runtime will report errors.
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return occurrence
|
|
194
|
+
}
|
|
136
195
|
|
|
137
196
|
const varKeys = Object.keys(metadata.variables)
|
|
138
197
|
|
|
@@ -439,6 +498,8 @@ async function enrichMetadata(
|
|
|
439
498
|
for (const instance of varInstances) {
|
|
440
499
|
// Add this occurrence with its full context
|
|
441
500
|
const occurrence = createOccurrence(instance, key)
|
|
501
|
+
applyCommentFallback(occurrence)
|
|
502
|
+
await applyDynamicOneOfAllowedValues(occurrence)
|
|
442
503
|
entry.occurrences.push(occurrence)
|
|
443
504
|
|
|
444
505
|
// Find inner variables in resolveDetails (excluding the outermost variable itself)
|
|
@@ -511,6 +572,8 @@ async function enrichMetadata(
|
|
|
511
572
|
hasFallback: !!detail.hasFallback,
|
|
512
573
|
defaultValue: siblingDefaultValue,
|
|
513
574
|
})
|
|
575
|
+
applyCommentFallback(siblingOccurrence)
|
|
576
|
+
await applyDynamicOneOfAllowedValues(siblingOccurrence)
|
|
514
577
|
if (siblingDefaultValueSrc) {
|
|
515
578
|
siblingOccurrence.defaultValueSrc = siblingDefaultValueSrc
|
|
516
579
|
}
|
|
@@ -763,4 +826,4 @@ async function enrichMetadata(
|
|
|
763
826
|
return metadata
|
|
764
827
|
}
|
|
765
828
|
|
|
766
|
-
module.exports = enrichMetadata
|
|
829
|
+
module.exports = enrichMetadata
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/* eslint-disable no-template-curly-in-string */
|
|
2
|
+
const { test } = require('uvu')
|
|
3
|
+
const assert = require('uvu/assert')
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const os = require('os')
|
|
6
|
+
const path = require('path')
|
|
7
|
+
const configorama = require('../../index')
|
|
8
|
+
|
|
9
|
+
function writeTempConfig(lines) {
|
|
10
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'configorama-annotations-'))
|
|
11
|
+
const configPath = path.join(dir, 'config.yml')
|
|
12
|
+
fs.writeFileSync(configPath, lines.join('\n'))
|
|
13
|
+
return { dir, configPath }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test('enriched metadata maps comment annotations onto occurrences', async () => {
|
|
17
|
+
const { dir, configPath } = writeTempConfig([
|
|
18
|
+
'# Stripe live secret key',
|
|
19
|
+
'# @from Stripe dashboard > Developers > API keys',
|
|
20
|
+
'# @example sk_live_...',
|
|
21
|
+
'# @default Set in CI',
|
|
22
|
+
'# @sensitive false',
|
|
23
|
+
'# @group Payments',
|
|
24
|
+
'# @deprecated Use STRIPE_RESTRICTED_KEY instead',
|
|
25
|
+
'stripeSecret: ${env:STRIPE_SECRET_KEY}',
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const analysis = await configorama.analyze(configPath, { options: {} })
|
|
30
|
+
const occurrence = analysis.uniqueVariables['env:STRIPE_SECRET_KEY'].occurrences[0]
|
|
31
|
+
|
|
32
|
+
assert.is(occurrence.description, 'Stripe live secret key')
|
|
33
|
+
assert.is(occurrence.descriptionSource, 'leadingComment')
|
|
34
|
+
assert.is(occurrence.obtainHint, 'Stripe dashboard > Developers > API keys')
|
|
35
|
+
assert.equal(occurrence.examples, ['sk_live_...'])
|
|
36
|
+
assert.is(occurrence.defaultHint, 'Set in CI')
|
|
37
|
+
assert.is(occurrence.defaultValue, undefined)
|
|
38
|
+
assert.is(occurrence.sensitive, false)
|
|
39
|
+
assert.is(occurrence.sensitiveSource, 'commentTag')
|
|
40
|
+
assert.is(occurrence.group, 'Payments')
|
|
41
|
+
assert.is(occurrence.deprecationMessage, 'Use STRIPE_RESTRICTED_KEY instead')
|
|
42
|
+
} finally {
|
|
43
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('enriched metadata lets @description override help on the same occurrence', async () => {
|
|
48
|
+
const { dir, configPath } = writeTempConfig([
|
|
49
|
+
'# Plain comment loses',
|
|
50
|
+
'# @description Comment tag wins',
|
|
51
|
+
'apiKey: ${env:CONFIGORAMA_ANNOTATED_API_KEY | help("Help loses")}',
|
|
52
|
+
])
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const analysis = await configorama.analyze(configPath, { options: {} })
|
|
56
|
+
const occurrence = analysis.uniqueVariables['env:CONFIGORAMA_ANNOTATED_API_KEY'].occurrences[0]
|
|
57
|
+
|
|
58
|
+
assert.is(occurrence.description, 'Comment tag wins')
|
|
59
|
+
assert.is(occurrence.descriptionSource, 'commentTag')
|
|
60
|
+
} finally {
|
|
61
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('enriched metadata keeps normal comment below help while preserving independent tags', async () => {
|
|
66
|
+
const { dir, configPath } = writeTempConfig([
|
|
67
|
+
'# Plain comment loses to help',
|
|
68
|
+
'# @from Dashboard > Tokens',
|
|
69
|
+
'apiToken: ${env:CONFIGORAMA_ANNOTATED_TOKEN | help("Help wins")}',
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const analysis = await configorama.analyze(configPath, { options: {} })
|
|
74
|
+
const occurrence = analysis.uniqueVariables['env:CONFIGORAMA_ANNOTATED_TOKEN'].occurrences[0]
|
|
75
|
+
|
|
76
|
+
assert.is(occurrence.description, 'Help wins')
|
|
77
|
+
assert.is(occurrence.descriptionSource, 'help')
|
|
78
|
+
assert.is(occurrence.obtainHint, 'Dashboard > Tokens')
|
|
79
|
+
} finally {
|
|
80
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test.run()
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const { findLineByPath, findLineForKey } = require('../paths/findLineForKey')
|
|
2
|
+
const { parseCommentAnnotations } = require('./commentAnnotations')
|
|
3
|
+
|
|
4
|
+
function getCommentMarkers(fileType) {
|
|
5
|
+
if (fileType === '.json') return []
|
|
6
|
+
if (fileType === '.json5' || fileType === '.jsonc') return ['//']
|
|
7
|
+
if (fileType === '.hcl') return ['//', '#']
|
|
8
|
+
if (['.yml', '.yaml', '.toml', '.ini'].includes(fileType)) return ['#']
|
|
9
|
+
return ['#', '//']
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isDecorationComment(text) {
|
|
13
|
+
return /^[\s\-=_*]{3,}$/.test(text)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function stripCommentMarker(line, marker) {
|
|
17
|
+
return line.trim().slice(marker.length).replace(/^\s?/, '')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function findCommentStart(line, markers) {
|
|
21
|
+
let quote = null
|
|
22
|
+
let variableDepth = 0
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < line.length; i++) {
|
|
25
|
+
const char = line[i]
|
|
26
|
+
const prev = i > 0 ? line[i - 1] : ''
|
|
27
|
+
|
|
28
|
+
if (!quote && char === '$' && line[i + 1] === '{') {
|
|
29
|
+
variableDepth++
|
|
30
|
+
i++
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
if (!quote && variableDepth > 0 && char === '}') {
|
|
34
|
+
variableDepth--
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
if ((char === "'" || char === '"') && prev !== '\\') {
|
|
38
|
+
if (!quote) quote = char
|
|
39
|
+
else if (quote === char) quote = null
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
if (quote || variableDepth > 0) continue
|
|
43
|
+
|
|
44
|
+
for (const marker of markers) {
|
|
45
|
+
if (line.slice(i, i + marker.length) === marker) {
|
|
46
|
+
return { index: i, marker }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getLineNumber(configPath, lines, fileType) {
|
|
55
|
+
if (['.yml', '.yaml', '.json5', '.jsonc'].includes(fileType)) {
|
|
56
|
+
const byPath = findLineByPath(configPath, lines, fileType)
|
|
57
|
+
if (byPath) return byPath
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (fileType === '.json') return 0
|
|
61
|
+
const key = String(configPath).split('.').pop()
|
|
62
|
+
return findLineForKey(key, lines, fileType)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getLeadingComment(lines, lineIndex, markers) {
|
|
66
|
+
const comments = getLeadingCommentLines(lines, lineIndex, markers)
|
|
67
|
+
return comments.length ? comments.join(' ') : null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getLeadingCommentLines(lines, lineIndex, markers) {
|
|
71
|
+
const comments = []
|
|
72
|
+
for (let i = lineIndex - 1; i >= 0; i--) {
|
|
73
|
+
const line = lines[i]
|
|
74
|
+
if (!line || line.trim() === '') break
|
|
75
|
+
|
|
76
|
+
const trimmed = line.trim()
|
|
77
|
+
const marker = markers.find(item => trimmed.startsWith(item))
|
|
78
|
+
if (!marker) break
|
|
79
|
+
|
|
80
|
+
const text = stripCommentMarker(trimmed, marker).trim()
|
|
81
|
+
if (text && !isDecorationComment(text)) comments.unshift(text)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return comments
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function hasAnnotations(annotations) {
|
|
88
|
+
return annotations && Object.keys(annotations).length > 0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildCommentResult(commentLines, fallbackDescriptionSource) {
|
|
92
|
+
const parsed = parseCommentAnnotations(commentLines)
|
|
93
|
+
if (!parsed.description && !hasAnnotations(parsed.annotations)) return null
|
|
94
|
+
|
|
95
|
+
const result = {}
|
|
96
|
+
if (parsed.description) {
|
|
97
|
+
result.description = parsed.description
|
|
98
|
+
result.descriptionSource = parsed.descriptionSource || fallbackDescriptionSource
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (hasAnnotations(parsed.annotations)) {
|
|
102
|
+
result.annotations = parsed.annotations
|
|
103
|
+
for (const [key, value] of Object.entries(parsed.annotations)) {
|
|
104
|
+
if (key === 'description') continue
|
|
105
|
+
result[key] = value
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return result
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function extractComment(configPath, lines, fileType) {
|
|
113
|
+
try {
|
|
114
|
+
const markers = getCommentMarkers(fileType)
|
|
115
|
+
if (!markers.length || !configPath || !Array.isArray(lines) || !lines.length) return null
|
|
116
|
+
|
|
117
|
+
const lineNumber = getLineNumber(configPath, lines, fileType)
|
|
118
|
+
if (!lineNumber) return null
|
|
119
|
+
|
|
120
|
+
const lineIndex = lineNumber - 1
|
|
121
|
+
const line = lines[lineIndex] || ''
|
|
122
|
+
const inlineStart = findCommentStart(line, markers)
|
|
123
|
+
if (inlineStart) {
|
|
124
|
+
const text = stripCommentMarker(line.slice(inlineStart.index), inlineStart.marker).trim()
|
|
125
|
+
if (text && !isDecorationComment(text)) {
|
|
126
|
+
return buildCommentResult([text], 'comment')
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const leading = getLeadingCommentLines(lines, lineIndex, markers)
|
|
131
|
+
const result = buildCommentResult(leading, 'leadingComment')
|
|
132
|
+
if (result) return result
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
extractComment,
|
|
142
|
+
findCommentStart,
|
|
143
|
+
getCommentMarkers,
|
|
144
|
+
getLeadingComment,
|
|
145
|
+
}
|