configorama 0.9.5 → 0.9.11
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 +156 -5
- package/package.json +20 -2
- package/src/main.js +268 -105
- package/src/parsers/esm.js +0 -14
- package/src/parsers/hcl-parse-script.js +40 -0
- package/src/parsers/hcl.js +131 -3
- package/src/parsers/hcl.slow-test.js +141 -0
- package/src/parsers/index.js +3 -1
- package/src/parsers/typescript.js +0 -10
- package/src/resolvers/valueFromEval.js +69 -11
- package/src/resolvers/valueFromFile.js +54 -1
- package/src/resolvers/valueFromIf.js +75 -0
- package/src/resolvers/valueFromIf.test.js +66 -0
- package/src/resolvers/valueFromNumber.js +3 -0
- package/src/utils/handleSignalEvents.js +3 -4
- package/src/utils/lodash.js +18 -7
- package/src/utils/parsing/cloudformationSchema.js +1 -2
- package/src/utils/parsing/cloudformationSchema.test.js +14 -0
- package/src/utils/parsing/parse.js +11 -1
- package/src/utils/parsing/preProcess.js +220 -5
- package/src/utils/paths/getFullFilePath.js +6 -2
- package/src/utils/paths/getFullFilePath.test.js +18 -0
- package/src/utils/regex/index.js +18 -3
- package/src/utils/regex/index.test.js +24 -0
- package/src/utils/strings/quoteAware.js +141 -0
- package/src/utils/strings/replaceAll.js +13 -1
- package/src/utils/strings/splitByComma.js +25 -15
- package/src/utils/strings/splitByComma.test.js +19 -0
- package/src/utils/strings/splitOnPipe.js +30 -0
- package/src/utils/strings/splitOnPipe.test.js +68 -0
- package/src/utils/validation/isValidValue.test.js +1 -1
- package/src/utils/validation/warnIfNotFound.js +1 -1
- package/src/utils/variables/findNestedVariables.js +8 -2
- package/types/src/main.d.ts +3 -1
- package/types/src/main.d.ts.map +1 -1
- package/types/src/parsers/esm.d.ts.map +1 -1
- package/types/src/parsers/hcl-parse-script.d.ts +3 -0
- package/types/src/parsers/hcl-parse-script.d.ts.map +1 -0
- package/types/src/parsers/hcl.d.ts +43 -0
- package/types/src/parsers/hcl.d.ts.map +1 -1
- package/types/src/parsers/hcl.slow-test.d.ts +2 -0
- package/types/src/parsers/hcl.slow-test.d.ts.map +1 -0
- package/types/src/parsers/typescript.d.ts.map +1 -1
- package/types/src/resolvers/valueFromEval.d.ts +1 -0
- package/types/src/resolvers/valueFromEval.d.ts.map +1 -1
- package/types/src/resolvers/valueFromFile.d.ts +4 -0
- package/types/src/resolvers/valueFromFile.d.ts.map +1 -1
- package/types/src/resolvers/valueFromIf.d.ts +7 -0
- package/types/src/resolvers/valueFromIf.d.ts.map +1 -0
- 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.map +1 -1
- package/types/src/utils/parsing/parse.d.ts.map +1 -1
- package/types/src/utils/parsing/preProcess.d.ts +5 -1
- package/types/src/utils/parsing/preProcess.d.ts.map +1 -1
- package/types/src/utils/paths/getFullFilePath.d.ts.map +1 -1
- package/types/src/utils/regex/index.d.ts.map +1 -1
- package/types/src/utils/strings/quoteAware.d.ts +30 -0
- package/types/src/utils/strings/quoteAware.d.ts.map +1 -0
- package/types/src/utils/strings/replaceAll.d.ts.map +1 -1
- package/types/src/utils/strings/splitByComma.d.ts +1 -1
- package/types/src/utils/strings/splitByComma.d.ts.map +1 -1
- package/types/src/utils/strings/splitOnPipe.d.ts +8 -0
- package/types/src/utils/strings/splitOnPipe.d.ts.map +1 -0
- package/types/src/utils/variables/findNestedVariables.d.ts.map +1 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Standalone script to parse HCL content
|
|
4
|
+
* Used by hcl.js for synchronous parsing via child_process
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
async function main() {
|
|
8
|
+
try {
|
|
9
|
+
const args = process.argv.slice(2)
|
|
10
|
+
const filename = args[0] || 'config.tf'
|
|
11
|
+
const contents = args[1] || ''
|
|
12
|
+
|
|
13
|
+
if (!contents) {
|
|
14
|
+
throw new Error('HCL content is required')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let hcl2json
|
|
18
|
+
try {
|
|
19
|
+
hcl2json = require('@cdktf/hcl2json')
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (err.code === 'MODULE_NOT_FOUND') {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'HCL/Terraform file support requires "@cdktf/hcl2json" to be installed. ' +
|
|
24
|
+
'Please install it: npm install @cdktf/hcl2json'
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
throw err
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const result = await hcl2json.parse(filename, contents)
|
|
31
|
+
|
|
32
|
+
// Output result as JSON
|
|
33
|
+
console.log(JSON.stringify(result))
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error(JSON.stringify({ error: error.message }))
|
|
36
|
+
process.exit(1)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
main()
|
package/src/parsers/hcl.js
CHANGED
|
@@ -1,3 +1,131 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
const YAML = require('./yaml')
|
|
2
|
+
const JSON = require('./json5')
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get the hcl2json module, throwing helpful error if not installed
|
|
6
|
+
* @returns {{ parse: Function }} The hcl2json module
|
|
7
|
+
* @throws {Error} If @cdktf/hcl2json is not installed
|
|
8
|
+
*/
|
|
9
|
+
function getHcl2Json() {
|
|
10
|
+
try {
|
|
11
|
+
return require('@cdktf/hcl2json')
|
|
12
|
+
} catch (err) {
|
|
13
|
+
if (err.code === 'MODULE_NOT_FOUND') {
|
|
14
|
+
throw new Error(
|
|
15
|
+
'HCL/Terraform file support requires "@cdktf/hcl2json" to be installed.\n' +
|
|
16
|
+
'Please install it:\n' +
|
|
17
|
+
' npm install @cdktf/hcl2json'
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
throw err
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse HCL content into JavaScript object
|
|
26
|
+
* Uses @cdktf/hcl2json to convert HCL to JSON
|
|
27
|
+
* @param {string} hclContents - HCL string to parse
|
|
28
|
+
* @param {string} [filename='config.tf'] - Filename for context
|
|
29
|
+
* @returns {Promise<Object>} Parsed HCL object
|
|
30
|
+
* @throws {Error} If HCL parsing fails
|
|
31
|
+
*/
|
|
32
|
+
async function parse(hclContents, filename = 'config.tf') {
|
|
33
|
+
let hclObject = {}
|
|
34
|
+
try {
|
|
35
|
+
const { parse: hclParse } = getHcl2Json()
|
|
36
|
+
const result = await hclParse(filename, hclContents)
|
|
37
|
+
hclObject = result
|
|
38
|
+
} catch (e) {
|
|
39
|
+
throw new Error(`Failed to parse HCL: ${e.message}`)
|
|
40
|
+
}
|
|
41
|
+
return hclObject
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Synchronous HCL parsing using child process
|
|
46
|
+
* @param {string} hclContents - HCL string to parse
|
|
47
|
+
* @param {string} [filename='config.tf'] - Filename for context
|
|
48
|
+
* @returns {Object} Parsed HCL object
|
|
49
|
+
* @throws {Error} If HCL parsing fails
|
|
50
|
+
*/
|
|
51
|
+
function parseSync(hclContents, filename = 'config.tf') {
|
|
52
|
+
const { execFileSync } = require('child_process')
|
|
53
|
+
const scriptPath = require.resolve('./hcl-parse-script.js')
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const result = execFileSync(process.execPath, [scriptPath, filename, hclContents], {
|
|
57
|
+
encoding: 'utf8',
|
|
58
|
+
maxBuffer: 10 * 1024 * 1024 // 10MB buffer
|
|
59
|
+
})
|
|
60
|
+
return JSON.parse(result.trim())
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// Check if error output contains JSON error
|
|
63
|
+
if (error.stderr) {
|
|
64
|
+
try {
|
|
65
|
+
const errorData = JSON.parse(error.stderr)
|
|
66
|
+
throw new Error(`Failed to parse HCL: ${errorData.error}`)
|
|
67
|
+
} catch (parseErr) {
|
|
68
|
+
// If stderr is not JSON, use original error
|
|
69
|
+
throw new Error(`Failed to parse HCL: ${error.message}`)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`Failed to parse HCL: ${error.message}`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Convert JavaScript object to HCL string
|
|
78
|
+
* Note: HCL generation is complex and not fully supported
|
|
79
|
+
* This is a placeholder for potential future implementation
|
|
80
|
+
* @param {Object} object - Object to convert to HCL
|
|
81
|
+
* @returns {string} HCL string representation
|
|
82
|
+
* @throws {Error} Always throws - HCL generation not implemented
|
|
83
|
+
*/
|
|
84
|
+
function dump(object) {
|
|
85
|
+
throw new Error('HCL generation (dump) is not currently supported. HCL files can be read but not written.')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Convert HCL content to YAML format
|
|
90
|
+
* @param {string} hclContents - HCL string to convert
|
|
91
|
+
* @param {string} [filename='config.tf'] - Filename for context
|
|
92
|
+
* @returns {Promise<string>} YAML string representation
|
|
93
|
+
* @throws {Error} If conversion fails
|
|
94
|
+
*/
|
|
95
|
+
async function toYaml(hclContents, filename = 'config.tf') {
|
|
96
|
+
let yml
|
|
97
|
+
try {
|
|
98
|
+
const parsed = await parse(hclContents, filename)
|
|
99
|
+
yml = YAML.dump(parsed)
|
|
100
|
+
} catch (e) {
|
|
101
|
+
throw new Error(`Failed to convert HCL to YAML: ${e.message}`)
|
|
102
|
+
}
|
|
103
|
+
return yml
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Convert HCL content to JSON format
|
|
108
|
+
* @param {string} hclContents - HCL string to convert
|
|
109
|
+
* @param {string} [filename='config.tf'] - Filename for context
|
|
110
|
+
* @returns {Promise<string>} JSON string representation
|
|
111
|
+
* @throws {Error} If conversion fails
|
|
112
|
+
*/
|
|
113
|
+
async function toJson(hclContents, filename = 'config.tf') {
|
|
114
|
+
let json
|
|
115
|
+
try {
|
|
116
|
+
const parsed = await parse(hclContents, filename)
|
|
117
|
+
json = JSON.dump(parsed)
|
|
118
|
+
} catch (e) {
|
|
119
|
+
throw new Error(`Failed to convert HCL to JSON: ${e.message}`)
|
|
120
|
+
}
|
|
121
|
+
return json
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
parse: parseSync, // Export sync version for compatibility with existing parsers
|
|
126
|
+
parseAsync: parse, // Export async version for direct use
|
|
127
|
+
parseSync: parseSync,
|
|
128
|
+
dump: dump,
|
|
129
|
+
toYaml: toYaml,
|
|
130
|
+
toJson: toJson
|
|
131
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/* eslint-disable no-template-curly-in-string */
|
|
2
|
+
const { test } = require('uvu')
|
|
3
|
+
const assert = require('uvu/assert')
|
|
4
|
+
const hcl = require('./hcl')
|
|
5
|
+
const JSON5 = require('json5')
|
|
6
|
+
const path = require('path')
|
|
7
|
+
const fs = require('fs')
|
|
8
|
+
|
|
9
|
+
function normalize(obj) {
|
|
10
|
+
return JSON.parse(JSON.stringify(obj))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test('hcl parse basic variable', async () => {
|
|
14
|
+
const hclContent = `variable "name" {
|
|
15
|
+
description = "Name to be used"
|
|
16
|
+
type = string
|
|
17
|
+
default = "test"
|
|
18
|
+
}`
|
|
19
|
+
const result = hcl.parse(hclContent, 'test.tf')
|
|
20
|
+
console.log('basic result', JSON.stringify(result, null, 2))
|
|
21
|
+
|
|
22
|
+
assert.ok(result, 'result should exist')
|
|
23
|
+
assert.ok(result.variable, 'should have variable section')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('hcl parse multiple variables', async () => {
|
|
27
|
+
const hclContent = `variable "region" {
|
|
28
|
+
description = "AWS region"
|
|
29
|
+
type = string
|
|
30
|
+
default = "us-east-1"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
variable "count" {
|
|
34
|
+
description = "Number of instances"
|
|
35
|
+
type = number
|
|
36
|
+
default = 2
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
variable "enabled" {
|
|
40
|
+
description = "Feature flag"
|
|
41
|
+
type = bool
|
|
42
|
+
default = true
|
|
43
|
+
}`
|
|
44
|
+
const result = hcl.parse(hclContent, 'test.tf')
|
|
45
|
+
console.log('multiple vars result', JSON.stringify(result, null, 2))
|
|
46
|
+
|
|
47
|
+
assert.ok(result, 'result should exist')
|
|
48
|
+
assert.ok(result.variable, 'should have variable section')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('hcl parse with locals', async () => {
|
|
52
|
+
const hclContent = `variable "environment" {
|
|
53
|
+
type = string
|
|
54
|
+
default = "dev"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
locals {
|
|
58
|
+
app_name = "myapp-\${var.environment}"
|
|
59
|
+
}`
|
|
60
|
+
const result = hcl.parse(hclContent, 'test.tf')
|
|
61
|
+
console.log('locals result', JSON.stringify(result, null, 2))
|
|
62
|
+
|
|
63
|
+
assert.ok(result, 'result should exist')
|
|
64
|
+
assert.ok(result.variable || result.locals, 'should have variable or locals section')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('hcl parse resource block', async () => {
|
|
68
|
+
const hclContent = `resource "aws_instance" "app" {
|
|
69
|
+
ami = "ami-12345678"
|
|
70
|
+
instance_type = "t3.micro"
|
|
71
|
+
|
|
72
|
+
tags = {
|
|
73
|
+
Name = "MyApp"
|
|
74
|
+
Environment = "dev"
|
|
75
|
+
}
|
|
76
|
+
}`
|
|
77
|
+
const result = hcl.parse(hclContent, 'test.tf')
|
|
78
|
+
console.log('resource result', JSON.stringify(result, null, 2))
|
|
79
|
+
|
|
80
|
+
assert.ok(result, 'result should exist')
|
|
81
|
+
assert.ok(result.resource, 'should have resource section')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('hcl parse from file - simple.tf', async () => {
|
|
85
|
+
const filePath = path.join(__dirname, '../../tests/hclTests/simple.tf')
|
|
86
|
+
|
|
87
|
+
// Skip if file doesn't exist
|
|
88
|
+
if (!fs.existsSync(filePath)) {
|
|
89
|
+
console.log('Skipping test - fixture file not found')
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const contents = fs.readFileSync(filePath, 'utf8')
|
|
94
|
+
const result = hcl.parse(contents, 'simple.tf')
|
|
95
|
+
console.log('file parse result', JSON.stringify(result, null, 2))
|
|
96
|
+
|
|
97
|
+
assert.ok(result, 'result should exist')
|
|
98
|
+
assert.ok(result.variable, 'should have variable section')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('hcl dump should throw error', () => {
|
|
102
|
+
let error
|
|
103
|
+
try {
|
|
104
|
+
hcl.dump({ foo: 'bar' })
|
|
105
|
+
} catch (e) {
|
|
106
|
+
error = e
|
|
107
|
+
}
|
|
108
|
+
assert.ok(error instanceof Error, 'should throw error')
|
|
109
|
+
assert.ok(error.message.includes('not currently supported'), 'error message should indicate not supported')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('hcl toJson basic', async () => {
|
|
113
|
+
const hclContent = `variable "test" {
|
|
114
|
+
type = string
|
|
115
|
+
default = "value"
|
|
116
|
+
}`
|
|
117
|
+
|
|
118
|
+
const result = await hcl.toJson(hclContent, 'test.tf')
|
|
119
|
+
console.log('toJson result', result)
|
|
120
|
+
|
|
121
|
+
assert.ok(result, 'result should exist')
|
|
122
|
+
assert.ok(typeof result === 'string', 'result should be string')
|
|
123
|
+
|
|
124
|
+
const parsed = JSON5.parse(result)
|
|
125
|
+
assert.ok(parsed, 'should be valid JSON5')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('hcl toYaml basic', async () => {
|
|
129
|
+
const hclContent = `variable "test" {
|
|
130
|
+
type = string
|
|
131
|
+
default = "value"
|
|
132
|
+
}`
|
|
133
|
+
|
|
134
|
+
const result = await hcl.toYaml(hclContent, 'test.tf')
|
|
135
|
+
console.log('toYaml result', result)
|
|
136
|
+
|
|
137
|
+
assert.ok(result, 'result should exist')
|
|
138
|
+
assert.ok(typeof result === 'string', 'result should be string')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test.run()
|
package/src/parsers/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const json = require('./json5')
|
|
|
8
8
|
const toml = require('./toml')
|
|
9
9
|
const yaml = require('./yaml')
|
|
10
10
|
const ini = require('./ini')
|
|
11
|
+
const hcl = require('./hcl')
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Collection of format parsers for different config file types
|
|
@@ -17,5 +18,6 @@ module.exports = {
|
|
|
17
18
|
json: json,
|
|
18
19
|
toml: toml,
|
|
19
20
|
yaml: yaml,
|
|
20
|
-
ini: ini
|
|
21
|
+
ini: ini,
|
|
22
|
+
hcl: hcl
|
|
21
23
|
}
|
|
@@ -55,11 +55,6 @@ async function executeTypeScriptFile(filePath, opts = {}) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
// Handle ES module default exports
|
|
59
|
-
if (tsFile && typeof tsFile === 'object' && 'default' in tsFile) {
|
|
60
|
-
tsFile = tsFile.default
|
|
61
|
-
}
|
|
62
|
-
|
|
63
58
|
return tsFile
|
|
64
59
|
}
|
|
65
60
|
|
|
@@ -117,11 +112,6 @@ function executeTypeScriptFileSync(filePath, opts = {}) {
|
|
|
117
112
|
}
|
|
118
113
|
}
|
|
119
114
|
|
|
120
|
-
// Handle ES module default exports
|
|
121
|
-
if (tsFile && typeof tsFile === 'object' && 'default' in tsFile) {
|
|
122
|
-
tsFile = tsFile.default
|
|
123
|
-
}
|
|
124
|
-
|
|
125
115
|
return tsFile
|
|
126
116
|
}
|
|
127
117
|
|
|
@@ -1,29 +1,86 @@
|
|
|
1
1
|
// const evalRefSyntax = RegExp(/^eval\((~?[\{\}\:\${}a-zA=>+!-Z0-9._\-\/,'"\*\` ]+?)?\)/g)
|
|
2
2
|
const evalRefSyntax = RegExp(/^eval\((.*)?\)/g)
|
|
3
|
+
const { replaceOutsideQuotes } = require('../utils/strings/quoteAware')
|
|
4
|
+
|
|
5
|
+
// Pattern for encoded objects/arrays: __OBJ:base64__ or __ARR:base64__
|
|
6
|
+
const ENCODED_PATTERN = /__(?:OBJ|ARR):([A-Za-z0-9+/=]+)__/g
|
|
7
|
+
|
|
8
|
+
// Encode object/array for embedding in eval expressions
|
|
9
|
+
function encodeValue(value) {
|
|
10
|
+
const prefix = Array.isArray(value) ? 'ARR' : 'OBJ'
|
|
11
|
+
const encoded = Buffer.from(JSON.stringify(value)).toString('base64')
|
|
12
|
+
return `__${prefix}:${encoded}__`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Decode encoded values and build context for subscript
|
|
16
|
+
function decodeValues(expression) {
|
|
17
|
+
const context = {}
|
|
18
|
+
let idx = 0
|
|
19
|
+
|
|
20
|
+
const processed = expression.replace(ENCODED_PATTERN, (match, base64) => {
|
|
21
|
+
const decoded = JSON.parse(Buffer.from(base64, 'base64').toString('utf8'))
|
|
22
|
+
const placeholder = `__VAL${idx}__`
|
|
23
|
+
context[`__VAL${idx}__`] = decoded
|
|
24
|
+
idx++
|
|
25
|
+
return placeholder
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
return { processed, context }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Wrap individual comparisons in parentheses for correct precedence with && / ||
|
|
32
|
+
// Subscript has operator precedence issues without explicit parens
|
|
33
|
+
function wrapComparisons(expr) {
|
|
34
|
+
if (!/&&|\|\|/.test(expr)) return expr
|
|
35
|
+
|
|
36
|
+
// Match comparisons: value op value (where op is ===, !==, ==, !=, >=, <=, >, <)
|
|
37
|
+
// Values can be: quoted strings, numbers, identifiers, or __VAL0__ placeholders
|
|
38
|
+
const compPattern = /((?:"[^"]*"|'[^']*'|__VAL\d+__|__NULL__|[a-zA-Z_][a-zA-Z0-9_]*|[\d.]+))\s*(===|!==|==|!=|>=|<=|>|<)\s*((?:"[^"]*"|'[^']*'|__VAL\d+__|__NULL__|[a-zA-Z_][a-zA-Z0-9_]*|[\d.]+))/g
|
|
39
|
+
|
|
40
|
+
return expr.replace(compPattern, '($1 $2 $3)')
|
|
41
|
+
}
|
|
3
42
|
|
|
4
43
|
async function getValueFromEval(variableString) {
|
|
5
|
-
// console.log('getValueFromEval variableString', variableString)
|
|
6
|
-
// console.log('getValueFromEval variableString', variableString)
|
|
7
44
|
// Extract the expression inside eval()
|
|
8
45
|
const match = variableString.match(/^eval\((.+)\)$/)
|
|
9
|
-
// console.log('match', match)
|
|
10
46
|
if (!match) {
|
|
11
47
|
throw new Error(`Invalid eval syntax: ${variableString}. Expected format: eval(expression)`)
|
|
12
48
|
}
|
|
13
|
-
|
|
49
|
+
|
|
14
50
|
const expression = match[1].trim()
|
|
15
|
-
|
|
16
|
-
|
|
51
|
+
if (process.env.DEBUG_EVAL) console.log('eval expression:', expression)
|
|
52
|
+
|
|
17
53
|
// Use "justin" variant to support strict comparison (===, !==) and other JS-like operators
|
|
18
54
|
try {
|
|
19
55
|
const { default: subscript } = await import('subscript/justin')
|
|
20
|
-
|
|
56
|
+
|
|
21
57
|
// Handle string comparisons by ensuring both sides are quoted
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
//
|
|
58
|
+
let processedExpression = expression.replace(/([a-zA-Z0-9_]+)\s*([=!<>]=?)\s*['"]([^'"]+)['"]/g, '"$1"$2"$3"')
|
|
59
|
+
|
|
60
|
+
// Decode any encoded objects/arrays
|
|
61
|
+
const { processed: withDecodedValues, context: valueContext } = decodeValues(processedExpression)
|
|
62
|
+
processedExpression = withDecodedValues
|
|
63
|
+
|
|
64
|
+
// Workaround: subscript doesn't handle null keyword correctly
|
|
65
|
+
// Replace null with placeholder and inject via context (but not inside quoted strings)
|
|
66
|
+
const hasNull = /\bnull\b/.test(processedExpression)
|
|
67
|
+
if (hasNull) {
|
|
68
|
+
processedExpression = replaceOutsideQuotes(processedExpression, 'null', '__NULL__')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Build context with null and any decoded values
|
|
72
|
+
/** @type {Record<string, unknown>} */
|
|
73
|
+
const context = { ...valueContext }
|
|
74
|
+
if (hasNull) {
|
|
75
|
+
context.__NULL__ = null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Wrap comparisons in parens for correct precedence with && / ||
|
|
79
|
+
processedExpression = wrapComparisons(processedExpression)
|
|
80
|
+
|
|
81
|
+
if (process.env.DEBUG_EVAL) console.log('eval processed:', processedExpression)
|
|
25
82
|
const fn = subscript(processedExpression)
|
|
26
|
-
const result = fn()
|
|
83
|
+
const result = fn(Object.keys(context).length > 0 ? context : undefined)
|
|
27
84
|
return result
|
|
28
85
|
} catch (error) {
|
|
29
86
|
throw new Error(`Error evaluating expression "${expression}": ${error.message}`)
|
|
@@ -33,6 +90,7 @@ async function getValueFromEval(variableString) {
|
|
|
33
90
|
module.exports = {
|
|
34
91
|
type: 'eval',
|
|
35
92
|
source: 'readonly',
|
|
93
|
+
encodeValue,
|
|
36
94
|
description: '${eval(expression)} - Evaluates mathematical expressions',
|
|
37
95
|
match: evalRefSyntax,
|
|
38
96
|
resolver: getValueFromEval
|
|
@@ -14,6 +14,34 @@ const YAML = require('../parsers/yaml')
|
|
|
14
14
|
const TOML = require('../parsers/toml')
|
|
15
15
|
const INI = require('../parsers/ini')
|
|
16
16
|
const JSON5 = require('../parsers/json5')
|
|
17
|
+
const HCL = require('../parsers/hcl')
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Convert HCL $[...] syntax to the main config's variable syntax
|
|
21
|
+
* This allows configorama variables in .tf files to work when imported from other formats
|
|
22
|
+
* @param {*} obj - Object to convert
|
|
23
|
+
* @param {string} varPrefix - Variable prefix (e.g., '${')
|
|
24
|
+
* @param {string} varSuffix - Variable suffix (e.g., '}')
|
|
25
|
+
* @returns {*} Converted object
|
|
26
|
+
*/
|
|
27
|
+
function convertHclVarSyntax(obj, varPrefix, varSuffix) {
|
|
28
|
+
if (!obj) return obj
|
|
29
|
+
if (typeof obj === 'string') {
|
|
30
|
+
// Convert $[...] to the main config's syntax (e.g., ${...})
|
|
31
|
+
return obj.replace(/\$\[([^\]]+)\]/g, `${varPrefix}$1${varSuffix}`)
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(obj)) {
|
|
34
|
+
return obj.map(item => convertHclVarSyntax(item, varPrefix, varSuffix))
|
|
35
|
+
}
|
|
36
|
+
if (typeof obj === 'object') {
|
|
37
|
+
const converted = {}
|
|
38
|
+
for (const key of Object.keys(obj)) {
|
|
39
|
+
converted[key] = convertHclVarSyntax(obj[key], varPrefix, varSuffix)
|
|
40
|
+
}
|
|
41
|
+
return converted
|
|
42
|
+
}
|
|
43
|
+
return obj
|
|
44
|
+
}
|
|
17
45
|
|
|
18
46
|
/**
|
|
19
47
|
* Recursively clean encoded JSON from an object
|
|
@@ -78,6 +106,8 @@ function parseFileContents(content, filePath) {
|
|
|
78
106
|
* @param {Function} ctx.getDeeperValue - Method for nested lookups
|
|
79
107
|
* @param {RegExp} ctx.fileRefSyntax - Regex for file() syntax
|
|
80
108
|
* @param {RegExp} ctx.textRefSyntax - Regex for text() syntax
|
|
109
|
+
* @param {string} ctx.varPrefix - Variable prefix (e.g., '${')
|
|
110
|
+
* @param {string} ctx.varSuffix - Variable suffix (e.g., '}')
|
|
81
111
|
* @param {string} variableString - The variable string to resolve
|
|
82
112
|
* @param {object} options - Resolution options
|
|
83
113
|
* @returns {Promise<any>}
|
|
@@ -228,7 +258,7 @@ ${JSON.stringify(options.context, null, 2)}`,
|
|
|
228
258
|
|
|
229
259
|
/* handle case for referencing raw JS files to inline them */
|
|
230
260
|
if (argsToPass.length
|
|
231
|
-
&& (argsToPass && argsToPass[0] && argsToPass[0].toLowerCase() === 'raw')
|
|
261
|
+
&& (argsToPass && argsToPass[0] && typeof argsToPass[0] === 'string' && argsToPass[0].toLowerCase() === 'raw')
|
|
232
262
|
|| opts.asRawText
|
|
233
263
|
) {
|
|
234
264
|
// Encode foo() to foo__PH_PAREN_OPEN__) to avoid function collisions
|
|
@@ -365,6 +395,19 @@ ${JSON.stringify(options.context, null, 2)}`,
|
|
|
365
395
|
if (fileExtension === 'ini') {
|
|
366
396
|
valueToPopulate = INI.toJson(valueToPopulate)
|
|
367
397
|
}
|
|
398
|
+
if (fileExtension === 'tf' || fileExtension === 'hcl') {
|
|
399
|
+
// Parse HCL and convert $[...] to main config's syntax for variable resolution
|
|
400
|
+
const parsed = convertHclVarSyntax(HCL.parse(valueToPopulate, relativePath), ctx.varPrefix, ctx.varSuffix)
|
|
401
|
+
valueToPopulate = JSON.stringify(parsed)
|
|
402
|
+
}
|
|
403
|
+
if (fileExtension === 'json' || fileExtension === 'json5') {
|
|
404
|
+
let parsed = JSON5.parse(valueToPopulate)
|
|
405
|
+
// Convert $[...] to main config's syntax for .tf.json files
|
|
406
|
+
if (relativePath.endsWith('.tf.json')) {
|
|
407
|
+
parsed = convertHclVarSyntax(parsed, ctx.varPrefix, ctx.varSuffix)
|
|
408
|
+
}
|
|
409
|
+
valueToPopulate = JSON.stringify(parsed)
|
|
410
|
+
}
|
|
368
411
|
// console.log('deep', variableString)
|
|
369
412
|
// console.log('matchedFileString', matchedFileString)
|
|
370
413
|
const deepPropertiesStr = variableString.replace(matchedFileString, '')
|
|
@@ -396,6 +439,16 @@ Please use ":" or "." to reference sub properties. ${deepPropertiesStr}`
|
|
|
396
439
|
|
|
397
440
|
if (fileExtension === 'json' || fileExtension === 'json5') {
|
|
398
441
|
valueToPopulate = JSON5.parse(valueToPopulate)
|
|
442
|
+
// Convert $[...] to main config's syntax for .tf.json files (Terraform JSON format)
|
|
443
|
+
if (relativePath.endsWith('.tf.json')) {
|
|
444
|
+
valueToPopulate = convertHclVarSyntax(valueToPopulate, ctx.varPrefix, ctx.varSuffix)
|
|
445
|
+
}
|
|
446
|
+
return Promise.resolve(valueToPopulate)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (fileExtension === 'tf' || fileExtension === 'hcl') {
|
|
450
|
+
// Parse HCL and convert $[...] to main config's syntax for variable resolution
|
|
451
|
+
valueToPopulate = convertHclVarSyntax(HCL.parse(valueToPopulate, relativePath), ctx.varPrefix, ctx.varSuffix)
|
|
399
452
|
return Promise.resolve(valueToPopulate)
|
|
400
453
|
}
|
|
401
454
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/* ${if(...)} syntax - alias for eval() with more intuitive name for conditionals */
|
|
2
|
+
const { resolver: evalResolver } = require('./valueFromEval')
|
|
3
|
+
const { findOutsideQuotes } = require('../utils/strings/quoteAware')
|
|
4
|
+
|
|
5
|
+
// Match both:
|
|
6
|
+
// if(condition ? trueVal : falseVal) - ternary inside
|
|
7
|
+
// if(condition) ? trueVal : falseVal - ternary outside
|
|
8
|
+
const ifRefSyntax = RegExp(/^if\s*\(.*\)(\s*\?.*)?/g)
|
|
9
|
+
|
|
10
|
+
async function getValueFromIf(variableString) {
|
|
11
|
+
if (process.env.DEBUG_IF) console.log('if resolver input:', variableString)
|
|
12
|
+
|
|
13
|
+
// Validate: check for empty condition
|
|
14
|
+
const emptyConditionMatch = variableString.match(/^if\s*\(\s*\)/)
|
|
15
|
+
if (emptyConditionMatch) {
|
|
16
|
+
throw new Error('Empty condition in ${if()}. Expected: ${if(condition) ? trueVal : falseVal}')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Check for external ternary: if(condition) ? trueVal : falseVal
|
|
20
|
+
// Must properly balance parentheses to find where if() ends
|
|
21
|
+
const match = variableString.match(/^if\s*\(/)
|
|
22
|
+
if (match) {
|
|
23
|
+
const afterIf = variableString.substring(match[0].length)
|
|
24
|
+
let depth = 1
|
|
25
|
+
let i = 0
|
|
26
|
+
|
|
27
|
+
// Find the matching closing paren
|
|
28
|
+
while (i < afterIf.length && depth > 0) {
|
|
29
|
+
if (afterIf[i] === '(') depth++
|
|
30
|
+
else if (afterIf[i] === ')') depth--
|
|
31
|
+
if (depth > 0) i++
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (depth === 0) {
|
|
35
|
+
// Check what comes after the if() block
|
|
36
|
+
const afterCondition = afterIf.substring(i + 1).trim()
|
|
37
|
+
|
|
38
|
+
if (afterCondition.startsWith('?')) {
|
|
39
|
+
// External ternary: if(condition) ? trueVal : falseVal
|
|
40
|
+
const condition = afterIf.substring(0, i)
|
|
41
|
+
const ternaryPart = afterCondition.substring(1).trim() // after ?
|
|
42
|
+
|
|
43
|
+
// Find the colon separating trueVal and falseVal (outside quotes and encoded patterns)
|
|
44
|
+
const colonIdx = findOutsideQuotes(ternaryPart, (str, idx) => {
|
|
45
|
+
if (str[idx] !== ':') return 0
|
|
46
|
+
// Skip colons inside encoded patterns __OBJ:...__ or __ARR:...__
|
|
47
|
+
const before = str.substring(0, idx)
|
|
48
|
+
if (/__(?:OBJ|ARR|VAL\d+)$/.test(before)) return 0
|
|
49
|
+
return 1
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if (colonIdx !== -1) {
|
|
53
|
+
const trueVal = ternaryPart.substring(0, colonIdx).trim()
|
|
54
|
+
const falseVal = ternaryPart.substring(colonIdx + 1).trim()
|
|
55
|
+
const expression = `(${condition}) ? ${trueVal} : ${falseVal}`
|
|
56
|
+
if (process.env.DEBUG_IF) console.log('if resolver external ternary:', expression)
|
|
57
|
+
return evalResolver(`eval(${expression})`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Standard syntax: if(condition ? trueVal : falseVal) or if(boolExpr)
|
|
64
|
+
const converted = variableString.replace(/^if\s*\(/, 'eval(')
|
|
65
|
+
if (process.env.DEBUG_IF) console.log('if resolver standard syntax:', converted)
|
|
66
|
+
return evalResolver(converted)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
type: 'if',
|
|
71
|
+
source: 'readonly',
|
|
72
|
+
description: '${if(condition) ? "yes" : "no"} - Conditional expressions',
|
|
73
|
+
match: ifRefSyntax,
|
|
74
|
+
resolver: getValueFromIf
|
|
75
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/* Tests for ${if(...)} syntax - alias for eval */
|
|
2
|
+
const { test } = require('uvu')
|
|
3
|
+
const assert = require('uvu/assert')
|
|
4
|
+
const configorama = require('../../src')
|
|
5
|
+
|
|
6
|
+
test('if() basic ternary', async () => {
|
|
7
|
+
const result = await configorama({
|
|
8
|
+
yes: '${if(5 > 3 ? "yes" : "no")}',
|
|
9
|
+
no: '${if(3 > 5 ? "yes" : "no")}'
|
|
10
|
+
})
|
|
11
|
+
assert.is(result.yes, 'yes')
|
|
12
|
+
assert.is(result.no, 'no')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('if() with parentheses around condition', async () => {
|
|
16
|
+
const result = await configorama({
|
|
17
|
+
result: '${if((10 < 20) ? "smaller" : "bigger")}'
|
|
18
|
+
})
|
|
19
|
+
assert.is(result.result, 'smaller')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('if() boolean result', async () => {
|
|
23
|
+
const result = await configorama({
|
|
24
|
+
isTrue: '${if(10 == 10)}',
|
|
25
|
+
isFalse: '${if(10 == 5)}'
|
|
26
|
+
})
|
|
27
|
+
assert.is(result.isTrue, true)
|
|
28
|
+
assert.is(result.isFalse, false)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('if() with variables', async () => {
|
|
32
|
+
const result = await configorama({
|
|
33
|
+
threshold: 50,
|
|
34
|
+
value: 75,
|
|
35
|
+
status: '${if(${self:value} > ${self:threshold} ? "above" : "below")}'
|
|
36
|
+
})
|
|
37
|
+
assert.is(result.status, 'above')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('if() nested ternary', async () => {
|
|
41
|
+
const result = await configorama({
|
|
42
|
+
score: 85,
|
|
43
|
+
grade: '${if(${self:score} >= 90 ? "A" : ${self:score} >= 80 ? "B" : "C")}'
|
|
44
|
+
})
|
|
45
|
+
assert.is(result.grade, 'B')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('if() with logical operators', async () => {
|
|
49
|
+
const result = await configorama({
|
|
50
|
+
both: '${if(true && true)}',
|
|
51
|
+
either: '${if(false || true)}',
|
|
52
|
+
neither: '${if(false && false)}'
|
|
53
|
+
})
|
|
54
|
+
assert.is(result.both, true)
|
|
55
|
+
assert.is(result.either, true)
|
|
56
|
+
assert.is(result.neither, false)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('if() arithmetic in condition', async () => {
|
|
60
|
+
const result = await configorama({
|
|
61
|
+
result: '${if((5 + 5) > 8 ? "big" : "small")}'
|
|
62
|
+
})
|
|
63
|
+
assert.is(result.result, 'big')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test.run()
|