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.
Files changed (65) hide show
  1. package/README.md +156 -5
  2. package/package.json +20 -2
  3. package/src/main.js +268 -105
  4. package/src/parsers/esm.js +0 -14
  5. package/src/parsers/hcl-parse-script.js +40 -0
  6. package/src/parsers/hcl.js +131 -3
  7. package/src/parsers/hcl.slow-test.js +141 -0
  8. package/src/parsers/index.js +3 -1
  9. package/src/parsers/typescript.js +0 -10
  10. package/src/resolvers/valueFromEval.js +69 -11
  11. package/src/resolvers/valueFromFile.js +54 -1
  12. package/src/resolvers/valueFromIf.js +75 -0
  13. package/src/resolvers/valueFromIf.test.js +66 -0
  14. package/src/resolvers/valueFromNumber.js +3 -0
  15. package/src/utils/handleSignalEvents.js +3 -4
  16. package/src/utils/lodash.js +18 -7
  17. package/src/utils/parsing/cloudformationSchema.js +1 -2
  18. package/src/utils/parsing/cloudformationSchema.test.js +14 -0
  19. package/src/utils/parsing/parse.js +11 -1
  20. package/src/utils/parsing/preProcess.js +220 -5
  21. package/src/utils/paths/getFullFilePath.js +6 -2
  22. package/src/utils/paths/getFullFilePath.test.js +18 -0
  23. package/src/utils/regex/index.js +18 -3
  24. package/src/utils/regex/index.test.js +24 -0
  25. package/src/utils/strings/quoteAware.js +141 -0
  26. package/src/utils/strings/replaceAll.js +13 -1
  27. package/src/utils/strings/splitByComma.js +25 -15
  28. package/src/utils/strings/splitByComma.test.js +19 -0
  29. package/src/utils/strings/splitOnPipe.js +30 -0
  30. package/src/utils/strings/splitOnPipe.test.js +68 -0
  31. package/src/utils/validation/isValidValue.test.js +1 -1
  32. package/src/utils/validation/warnIfNotFound.js +1 -1
  33. package/src/utils/variables/findNestedVariables.js +8 -2
  34. package/types/src/main.d.ts +3 -1
  35. package/types/src/main.d.ts.map +1 -1
  36. package/types/src/parsers/esm.d.ts.map +1 -1
  37. package/types/src/parsers/hcl-parse-script.d.ts +3 -0
  38. package/types/src/parsers/hcl-parse-script.d.ts.map +1 -0
  39. package/types/src/parsers/hcl.d.ts +43 -0
  40. package/types/src/parsers/hcl.d.ts.map +1 -1
  41. package/types/src/parsers/hcl.slow-test.d.ts +2 -0
  42. package/types/src/parsers/hcl.slow-test.d.ts.map +1 -0
  43. package/types/src/parsers/typescript.d.ts.map +1 -1
  44. package/types/src/resolvers/valueFromEval.d.ts +1 -0
  45. package/types/src/resolvers/valueFromEval.d.ts.map +1 -1
  46. package/types/src/resolvers/valueFromFile.d.ts +4 -0
  47. package/types/src/resolvers/valueFromFile.d.ts.map +1 -1
  48. package/types/src/resolvers/valueFromIf.d.ts +7 -0
  49. package/types/src/resolvers/valueFromIf.d.ts.map +1 -0
  50. package/types/src/resolvers/valueFromNumber.d.ts.map +1 -1
  51. package/types/src/utils/handleSignalEvents.d.ts.map +1 -1
  52. package/types/src/utils/lodash.d.ts.map +1 -1
  53. package/types/src/utils/parsing/parse.d.ts.map +1 -1
  54. package/types/src/utils/parsing/preProcess.d.ts +5 -1
  55. package/types/src/utils/parsing/preProcess.d.ts.map +1 -1
  56. package/types/src/utils/paths/getFullFilePath.d.ts.map +1 -1
  57. package/types/src/utils/regex/index.d.ts.map +1 -1
  58. package/types/src/utils/strings/quoteAware.d.ts +30 -0
  59. package/types/src/utils/strings/quoteAware.d.ts.map +1 -0
  60. package/types/src/utils/strings/replaceAll.d.ts.map +1 -1
  61. package/types/src/utils/strings/splitByComma.d.ts +1 -1
  62. package/types/src/utils/strings/splitByComma.d.ts.map +1 -1
  63. package/types/src/utils/strings/splitOnPipe.d.ts +8 -0
  64. package/types/src/utils/strings/splitOnPipe.d.ts.map +1 -0
  65. 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()
@@ -1,3 +1,131 @@
1
- // TODO hashicorp HCL
2
- // https://www.npmjs.com/package/hcl-to-json
3
- // https://www.npmjs.com/package/hcl2json
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()
@@ -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
- // console.log('expression', expression)
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
- const processedExpression = expression.replace(/([a-zA-Z0-9_]+)\s*([=!<>]=?)\s*['"]([^'"]+)['"]/g, '"$1"$2"$3"')
23
-
24
- // console.log('processedExpression', processedExpression)
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()