parse-hcl 0.1.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/LICENSE +201 -0
- package/README.md +749 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +91 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +74 -0
- package/dist/parsers/genericParser.d.ts +167 -0
- package/dist/parsers/genericParser.js +268 -0
- package/dist/parsers/localsParser.d.ts +30 -0
- package/dist/parsers/localsParser.js +43 -0
- package/dist/parsers/outputParser.d.ts +25 -0
- package/dist/parsers/outputParser.js +44 -0
- package/dist/parsers/variableParser.d.ts +62 -0
- package/dist/parsers/variableParser.js +249 -0
- package/dist/services/artifactParsers.d.ts +12 -0
- package/dist/services/artifactParsers.js +157 -0
- package/dist/services/terraformJsonParser.d.ts +16 -0
- package/dist/services/terraformJsonParser.js +212 -0
- package/dist/services/terraformParser.d.ts +91 -0
- package/dist/services/terraformParser.js +191 -0
- package/dist/types/artifacts.d.ts +210 -0
- package/dist/types/artifacts.js +5 -0
- package/dist/types/blocks.d.ts +419 -0
- package/dist/types/blocks.js +28 -0
- package/dist/utils/common/errors.d.ts +46 -0
- package/dist/utils/common/errors.js +54 -0
- package/dist/utils/common/fs.d.ts +5 -0
- package/dist/utils/common/fs.js +48 -0
- package/dist/utils/common/logger.d.ts +5 -0
- package/dist/utils/common/logger.js +17 -0
- package/dist/utils/common/valueHelpers.d.ts +4 -0
- package/dist/utils/common/valueHelpers.js +23 -0
- package/dist/utils/graph/graphBuilder.d.ts +33 -0
- package/dist/utils/graph/graphBuilder.js +373 -0
- package/dist/utils/lexer/blockScanner.d.ts +36 -0
- package/dist/utils/lexer/blockScanner.js +143 -0
- package/dist/utils/lexer/hclLexer.d.ts +119 -0
- package/dist/utils/lexer/hclLexer.js +525 -0
- package/dist/utils/parser/bodyParser.d.ts +26 -0
- package/dist/utils/parser/bodyParser.js +81 -0
- package/dist/utils/parser/valueClassifier.d.ts +21 -0
- package/dist/utils/parser/valueClassifier.js +434 -0
- package/dist/utils/serialization/serializer.d.ts +9 -0
- package/dist/utils/serialization/serializer.js +63 -0
- package/dist/utils/serialization/yaml.d.ts +1 -0
- package/dist/utils/serialization/yaml.js +81 -0
- package/package.json +66 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Value classifier for HCL expressions.
|
|
4
|
+
* Classifies raw value strings into typed Value structures and extracts references.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.classifyValue = classifyValue;
|
|
8
|
+
const hclLexer_1 = require("../lexer/hclLexer");
|
|
9
|
+
/**
|
|
10
|
+
* Pattern for matching traversal expressions (e.g., aws_instance.web.id).
|
|
11
|
+
* Handles indexed access like resource[0].attr and splat expressions like resource[*].attr.
|
|
12
|
+
*/
|
|
13
|
+
const TRAVERSAL_PATTERN = /[A-Za-z_][\w-]*(?:\[(?:[^[\]]*|\*)])?(?:\.[A-Za-z_][\w-]*(?:\[(?:[^[\]]*|\*)])?)+/g;
|
|
14
|
+
/**
|
|
15
|
+
* Pattern for matching splat expressions (e.g., aws_instance.web[*].id).
|
|
16
|
+
*/
|
|
17
|
+
const SPLAT_PATTERN = /\[\*]/g;
|
|
18
|
+
/**
|
|
19
|
+
* Classifies a raw HCL value string into a typed Value structure.
|
|
20
|
+
* Supports literals, quoted strings, arrays, objects, and expressions.
|
|
21
|
+
*
|
|
22
|
+
* @param raw - The raw value string to classify
|
|
23
|
+
* @returns The classified Value with type information and extracted references
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* classifyValue('true') // LiteralValue { type: 'literal', value: true }
|
|
28
|
+
* classifyValue('"hello"') // LiteralValue { type: 'literal', value: 'hello' }
|
|
29
|
+
* classifyValue('var.region') // ExpressionValue with variable reference
|
|
30
|
+
* classifyValue('[1, 2, 3]') // ArrayValue with parsed elements
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
function classifyValue(raw) {
|
|
34
|
+
const trimmed = raw.trim();
|
|
35
|
+
// Try literal classification first (booleans, numbers, null)
|
|
36
|
+
const literal = classifyLiteral(trimmed);
|
|
37
|
+
if (literal) {
|
|
38
|
+
return literal;
|
|
39
|
+
}
|
|
40
|
+
// Handle quoted strings
|
|
41
|
+
if (isQuotedString(trimmed)) {
|
|
42
|
+
const inner = unquote(trimmed);
|
|
43
|
+
if (inner.includes('${')) {
|
|
44
|
+
return classifyExpression(inner, 'template');
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
type: 'literal',
|
|
48
|
+
value: inner,
|
|
49
|
+
raw: trimmed
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Handle heredocs
|
|
53
|
+
if (trimmed.startsWith('<<')) {
|
|
54
|
+
return classifyExpression(trimmed, 'template');
|
|
55
|
+
}
|
|
56
|
+
// Handle arrays with recursive parsing
|
|
57
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
58
|
+
return classifyArray(trimmed);
|
|
59
|
+
}
|
|
60
|
+
// Handle objects with recursive parsing
|
|
61
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
62
|
+
return classifyObject(trimmed);
|
|
63
|
+
}
|
|
64
|
+
// Everything else is an expression
|
|
65
|
+
return classifyExpression(trimmed);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Classifies a raw value as a literal (boolean, number, or null).
|
|
69
|
+
* @param raw - The trimmed raw value
|
|
70
|
+
* @returns LiteralValue if it's a literal, null otherwise
|
|
71
|
+
*/
|
|
72
|
+
function classifyLiteral(raw) {
|
|
73
|
+
// Boolean literals
|
|
74
|
+
if (raw === 'true' || raw === 'false') {
|
|
75
|
+
return {
|
|
76
|
+
type: 'literal',
|
|
77
|
+
value: raw === 'true',
|
|
78
|
+
raw
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// Numeric literals (integer and float)
|
|
82
|
+
if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(raw)) {
|
|
83
|
+
return {
|
|
84
|
+
type: 'literal',
|
|
85
|
+
value: Number(raw),
|
|
86
|
+
raw
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// Null literal
|
|
90
|
+
if (raw === 'null') {
|
|
91
|
+
return {
|
|
92
|
+
type: 'literal',
|
|
93
|
+
value: null,
|
|
94
|
+
raw
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Classifies and parses an array value with recursive element parsing.
|
|
101
|
+
* @param raw - The raw array string including brackets
|
|
102
|
+
* @returns ArrayValue with parsed elements and extracted references
|
|
103
|
+
*/
|
|
104
|
+
function classifyArray(raw) {
|
|
105
|
+
const elements = (0, hclLexer_1.splitArrayElements)(raw);
|
|
106
|
+
const parsedElements = elements.map((elem) => classifyValue(elem));
|
|
107
|
+
const references = collectReferences(parsedElements);
|
|
108
|
+
return {
|
|
109
|
+
type: 'array',
|
|
110
|
+
value: parsedElements.length > 0 ? parsedElements : undefined,
|
|
111
|
+
raw,
|
|
112
|
+
references: references.length > 0 ? references : undefined
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Classifies and parses an object value with recursive entry parsing.
|
|
117
|
+
* @param raw - The raw object string including braces
|
|
118
|
+
* @returns ObjectValue with parsed entries and extracted references
|
|
119
|
+
*/
|
|
120
|
+
function classifyObject(raw) {
|
|
121
|
+
const entries = (0, hclLexer_1.splitObjectEntries)(raw);
|
|
122
|
+
const parsedEntries = {};
|
|
123
|
+
for (const [key, value] of entries) {
|
|
124
|
+
parsedEntries[key] = classifyValue(value);
|
|
125
|
+
}
|
|
126
|
+
const references = collectReferences(Object.values(parsedEntries));
|
|
127
|
+
return {
|
|
128
|
+
type: 'object',
|
|
129
|
+
value: Object.keys(parsedEntries).length > 0 ? parsedEntries : undefined,
|
|
130
|
+
raw,
|
|
131
|
+
references: references.length > 0 ? references : undefined
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Collects all references from an array of values.
|
|
136
|
+
* @param values - Array of Value objects
|
|
137
|
+
* @returns Deduplicated array of references
|
|
138
|
+
*/
|
|
139
|
+
function collectReferences(values) {
|
|
140
|
+
const refs = [];
|
|
141
|
+
for (const value of values) {
|
|
142
|
+
if (value.type === 'expression' || value.type === 'array' || value.type === 'object') {
|
|
143
|
+
if (value.references) {
|
|
144
|
+
refs.push(...value.references);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Recursively collect from nested structures
|
|
148
|
+
if (value.type === 'array' && value.value) {
|
|
149
|
+
refs.push(...collectReferences(value.value));
|
|
150
|
+
}
|
|
151
|
+
if (value.type === 'object' && value.value) {
|
|
152
|
+
refs.push(...collectReferences(Object.values(value.value)));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return uniqueReferences(refs);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Classifies an expression and extracts its references.
|
|
159
|
+
* @param raw - The raw expression string
|
|
160
|
+
* @param forcedKind - Optional forced expression kind
|
|
161
|
+
* @returns ExpressionValue with kind and references
|
|
162
|
+
*/
|
|
163
|
+
function classifyExpression(raw, forcedKind) {
|
|
164
|
+
const kind = forcedKind || detectExpressionKind(raw);
|
|
165
|
+
const references = extractExpressionReferences(raw, kind);
|
|
166
|
+
return {
|
|
167
|
+
type: 'expression',
|
|
168
|
+
kind,
|
|
169
|
+
raw,
|
|
170
|
+
references: references.length > 0 ? references : undefined
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Detects the kind of an expression based on its syntax.
|
|
175
|
+
* @param raw - The raw expression string
|
|
176
|
+
* @returns The detected ExpressionKind
|
|
177
|
+
*/
|
|
178
|
+
function detectExpressionKind(raw) {
|
|
179
|
+
// Template interpolation
|
|
180
|
+
if (raw.includes('${')) {
|
|
181
|
+
return 'template';
|
|
182
|
+
}
|
|
183
|
+
// Conditional (ternary) expression
|
|
184
|
+
if (hasConditionalOperator(raw)) {
|
|
185
|
+
return 'conditional';
|
|
186
|
+
}
|
|
187
|
+
// Function call
|
|
188
|
+
if (/^[\w.-]+\(/.test(raw)) {
|
|
189
|
+
return 'function_call';
|
|
190
|
+
}
|
|
191
|
+
// For expression (list or map comprehension)
|
|
192
|
+
if (/^\[\s*for\s+.+\s+in\s+.+:\s+/.test(raw) || /^\{\s*for\s+.+\s+in\s+.+:\s+/.test(raw)) {
|
|
193
|
+
return 'for_expr';
|
|
194
|
+
}
|
|
195
|
+
// Splat expression
|
|
196
|
+
if (SPLAT_PATTERN.test(raw)) {
|
|
197
|
+
return 'splat';
|
|
198
|
+
}
|
|
199
|
+
// Simple traversal (e.g., var.name, local.value)
|
|
200
|
+
if (/^[\w.-]+(\[[^\]]*])?$/.test(raw)) {
|
|
201
|
+
return 'traversal';
|
|
202
|
+
}
|
|
203
|
+
return 'unknown';
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Checks if an expression contains a conditional (ternary) operator.
|
|
207
|
+
* Handles nested expressions and strings correctly.
|
|
208
|
+
* @param raw - The raw expression string
|
|
209
|
+
* @returns True if the expression is a conditional
|
|
210
|
+
*/
|
|
211
|
+
function hasConditionalOperator(raw) {
|
|
212
|
+
let depth = 0;
|
|
213
|
+
let inString = false;
|
|
214
|
+
let stringChar = null;
|
|
215
|
+
let questionMarkFound = false;
|
|
216
|
+
let questionMarkDepth = -1;
|
|
217
|
+
for (let i = 0; i < raw.length; i++) {
|
|
218
|
+
const char = raw[i];
|
|
219
|
+
if (!inString) {
|
|
220
|
+
if (char === '"' || char === "'") {
|
|
221
|
+
inString = true;
|
|
222
|
+
stringChar = char;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (char === '(' || char === '[' || char === '{') {
|
|
226
|
+
depth++;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (char === ')' || char === ']' || char === '}') {
|
|
230
|
+
depth--;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
// Look for ? at depth 0
|
|
234
|
+
if (char === '?' && depth === 0) {
|
|
235
|
+
questionMarkFound = true;
|
|
236
|
+
questionMarkDepth = depth;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
// Look for : after ? at the same depth
|
|
240
|
+
if (char === ':' && questionMarkFound && depth === questionMarkDepth) {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
if (char === stringChar && !(0, hclLexer_1.isEscaped)(raw, i)) {
|
|
246
|
+
inString = false;
|
|
247
|
+
stringChar = null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Extracts references from an expression.
|
|
255
|
+
* @param raw - The raw expression string
|
|
256
|
+
* @param kind - The expression kind
|
|
257
|
+
* @returns Array of extracted references
|
|
258
|
+
*/
|
|
259
|
+
function extractExpressionReferences(raw, kind) {
|
|
260
|
+
const baseRefs = extractReferencesFromText(raw);
|
|
261
|
+
// For templates, also extract from interpolated expressions
|
|
262
|
+
if (kind === 'template') {
|
|
263
|
+
const interpolationMatches = raw.match(/\${([^}]+)}/g) || [];
|
|
264
|
+
const innerRefs = interpolationMatches.flatMap((expr) => extractReferencesFromText(expr.replace(/^\${|}$/g, '')));
|
|
265
|
+
return uniqueReferences([...baseRefs, ...innerRefs]);
|
|
266
|
+
}
|
|
267
|
+
return baseRefs;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Extracts all references from a text string.
|
|
271
|
+
* Supports: var.*, local.*, module.*, data.*, resource references,
|
|
272
|
+
* path.*, each.*, count.*, self.*
|
|
273
|
+
*
|
|
274
|
+
* @param raw - The raw text to extract references from
|
|
275
|
+
* @returns Array of extracted references
|
|
276
|
+
*/
|
|
277
|
+
function extractReferencesFromText(raw) {
|
|
278
|
+
const refs = [];
|
|
279
|
+
// Extract special references first (each, count, self)
|
|
280
|
+
const specialRefs = extractSpecialReferences(raw);
|
|
281
|
+
refs.push(...specialRefs);
|
|
282
|
+
// Extract traversal-based references
|
|
283
|
+
const matches = raw.match(TRAVERSAL_PATTERN) || [];
|
|
284
|
+
for (const match of matches) {
|
|
285
|
+
// Remove index notation for parsing, but track if it has splat
|
|
286
|
+
const hasSplat = match.includes('[*]');
|
|
287
|
+
const parts = match.split('.').map((part) => part.replace(/\[.*?]/g, ''));
|
|
288
|
+
// var.name
|
|
289
|
+
if (parts[0] === 'var' && parts[1]) {
|
|
290
|
+
refs.push({ kind: 'variable', name: parts[1] });
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
// local.name
|
|
294
|
+
if (parts[0] === 'local' && parts[1]) {
|
|
295
|
+
refs.push({ kind: 'local', name: parts[1] });
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
// module.name.output
|
|
299
|
+
if (parts[0] === 'module' && parts[1]) {
|
|
300
|
+
const attribute = parts.slice(2).join('.') || parts[1];
|
|
301
|
+
refs.push({ kind: 'module_output', module: parts[1], name: attribute });
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
// data.type.name
|
|
305
|
+
if (parts[0] === 'data' && parts[1] && parts[2]) {
|
|
306
|
+
const attribute = parts.slice(3).join('.') || undefined;
|
|
307
|
+
refs.push({
|
|
308
|
+
kind: 'data',
|
|
309
|
+
data_type: parts[1],
|
|
310
|
+
name: parts[2],
|
|
311
|
+
attribute,
|
|
312
|
+
splat: hasSplat || undefined
|
|
313
|
+
});
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
// path.module, path.root, path.cwd
|
|
317
|
+
if (parts[0] === 'path' && parts[1]) {
|
|
318
|
+
refs.push({ kind: 'path', name: parts[1] });
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
// Skip special references (handled separately)
|
|
322
|
+
if (parts[0] === 'each' || parts[0] === 'count' || parts[0] === 'self') {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
// resource.type.name (e.g., aws_instance.web.id)
|
|
326
|
+
if (parts.length >= 2) {
|
|
327
|
+
const [resourceType, resourceName, ...rest] = parts;
|
|
328
|
+
const attribute = rest.length ? rest.join('.') : undefined;
|
|
329
|
+
refs.push({
|
|
330
|
+
kind: 'resource',
|
|
331
|
+
resource_type: resourceType,
|
|
332
|
+
name: resourceName,
|
|
333
|
+
attribute,
|
|
334
|
+
splat: hasSplat || undefined
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return uniqueReferences(refs);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Extracts special references: each.key, each.value, count.index, self.*
|
|
342
|
+
* @param raw - The raw text to extract from
|
|
343
|
+
* @returns Array of special references
|
|
344
|
+
*/
|
|
345
|
+
function extractSpecialReferences(raw) {
|
|
346
|
+
const refs = [];
|
|
347
|
+
// each.key and each.value
|
|
348
|
+
const eachMatches = raw.match(/\beach\.(key|value)\b/g) || [];
|
|
349
|
+
for (const match of eachMatches) {
|
|
350
|
+
const property = match.split('.')[1];
|
|
351
|
+
refs.push({ kind: 'each', property });
|
|
352
|
+
}
|
|
353
|
+
// count.index
|
|
354
|
+
if (/\bcount\.index\b/.test(raw)) {
|
|
355
|
+
refs.push({ kind: 'count', property: 'index' });
|
|
356
|
+
}
|
|
357
|
+
// self.* (in provisioners)
|
|
358
|
+
const selfMatches = raw.match(/\bself\.[\w-]+/g) || [];
|
|
359
|
+
for (const match of selfMatches) {
|
|
360
|
+
const attribute = match.split('.')[1];
|
|
361
|
+
refs.push({ kind: 'self', attribute });
|
|
362
|
+
}
|
|
363
|
+
return refs;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Removes duplicate references based on their JSON representation.
|
|
367
|
+
* @param refs - Array of references (may contain duplicates)
|
|
368
|
+
* @returns Deduplicated array of references
|
|
369
|
+
*/
|
|
370
|
+
function uniqueReferences(refs) {
|
|
371
|
+
const seen = new Set();
|
|
372
|
+
return refs.filter((ref) => {
|
|
373
|
+
const key = JSON.stringify(ref);
|
|
374
|
+
if (seen.has(key)) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
seen.add(key);
|
|
378
|
+
return true;
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Checks if a value is a quoted string (single or double quotes).
|
|
383
|
+
* @param value - The value to check
|
|
384
|
+
* @returns True if the value is a quoted string
|
|
385
|
+
*/
|
|
386
|
+
function isQuotedString(value) {
|
|
387
|
+
return (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"));
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Removes quotes from a quoted string and handles escape sequences.
|
|
391
|
+
* @param value - The quoted string
|
|
392
|
+
* @returns The unquoted string with escape sequences processed
|
|
393
|
+
*/
|
|
394
|
+
function unquote(value) {
|
|
395
|
+
const quote = value[0];
|
|
396
|
+
const inner = value.slice(1, -1);
|
|
397
|
+
// Process escape sequences
|
|
398
|
+
let result = '';
|
|
399
|
+
let i = 0;
|
|
400
|
+
while (i < inner.length) {
|
|
401
|
+
if (inner[i] === '\\' && i + 1 < inner.length) {
|
|
402
|
+
const next = inner[i + 1];
|
|
403
|
+
switch (next) {
|
|
404
|
+
case 'n':
|
|
405
|
+
result += '\n';
|
|
406
|
+
i += 2;
|
|
407
|
+
continue;
|
|
408
|
+
case 't':
|
|
409
|
+
result += '\t';
|
|
410
|
+
i += 2;
|
|
411
|
+
continue;
|
|
412
|
+
case 'r':
|
|
413
|
+
result += '\r';
|
|
414
|
+
i += 2;
|
|
415
|
+
continue;
|
|
416
|
+
case '\\':
|
|
417
|
+
result += '\\';
|
|
418
|
+
i += 2;
|
|
419
|
+
continue;
|
|
420
|
+
case quote:
|
|
421
|
+
result += quote;
|
|
422
|
+
i += 2;
|
|
423
|
+
continue;
|
|
424
|
+
default:
|
|
425
|
+
result += inner[i];
|
|
426
|
+
i++;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
result += inner[i];
|
|
431
|
+
i++;
|
|
432
|
+
}
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TerraformDocument } from '../../types/blocks';
|
|
2
|
+
import { TerraformExport } from '../../types/artifacts';
|
|
3
|
+
export interface SerializeOptions {
|
|
4
|
+
pruneEmpty?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function toJson(document: TerraformDocument | unknown, options?: SerializeOptions): string;
|
|
7
|
+
export declare function toJsonExport(document: TerraformDocument, options?: SerializeOptions): string;
|
|
8
|
+
export declare function toExport(document: TerraformDocument, options?: SerializeOptions): TerraformExport;
|
|
9
|
+
export declare function toYamlDocument(document: TerraformDocument | unknown, options?: SerializeOptions): string;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toJson = toJson;
|
|
4
|
+
exports.toJsonExport = toJsonExport;
|
|
5
|
+
exports.toExport = toExport;
|
|
6
|
+
exports.toYamlDocument = toYamlDocument;
|
|
7
|
+
const graphBuilder_1 = require("../graph/graphBuilder");
|
|
8
|
+
const yaml_1 = require("./yaml");
|
|
9
|
+
function toJson(document, options) {
|
|
10
|
+
const value = shouldPrune(options) && isTerraformDocument(document) ? pruneDocument(document) : document;
|
|
11
|
+
return JSON.stringify(value, null, 2);
|
|
12
|
+
}
|
|
13
|
+
function toJsonExport(document, options) {
|
|
14
|
+
return JSON.stringify(toExport(document, options), null, 2);
|
|
15
|
+
}
|
|
16
|
+
function toExport(document, options) {
|
|
17
|
+
const exportPayload = (0, graphBuilder_1.createExport)(document);
|
|
18
|
+
const prunedDocument = shouldPrune(options) ? pruneDocument(document) : document;
|
|
19
|
+
return { ...exportPayload, document: prunedDocument };
|
|
20
|
+
}
|
|
21
|
+
function toYamlDocument(document, options) {
|
|
22
|
+
const value = shouldPrune(options) && isTerraformDocument(document) ? pruneDocument(document) : document;
|
|
23
|
+
return (0, yaml_1.toYaml)(value);
|
|
24
|
+
}
|
|
25
|
+
function shouldPrune(options) {
|
|
26
|
+
return options?.pruneEmpty !== false;
|
|
27
|
+
}
|
|
28
|
+
function pruneDocument(document) {
|
|
29
|
+
return pruneValue(document) ?? {};
|
|
30
|
+
}
|
|
31
|
+
function pruneValue(value) {
|
|
32
|
+
if (value === null || value === undefined) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
const items = value
|
|
37
|
+
.map((item) => pruneValue(item))
|
|
38
|
+
.filter((item) => item !== undefined);
|
|
39
|
+
return items.length > 0 ? items : undefined;
|
|
40
|
+
}
|
|
41
|
+
if (typeof value === 'object') {
|
|
42
|
+
const entries = Object.entries(value);
|
|
43
|
+
const pruned = {};
|
|
44
|
+
for (const [key, val] of entries) {
|
|
45
|
+
const next = pruneValue(val);
|
|
46
|
+
if (next === undefined) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(next) && next.length === 0) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (typeof next === 'object' && next !== null && Object.keys(next).length === 0) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
pruned[key] = next;
|
|
56
|
+
}
|
|
57
|
+
return Object.keys(pruned).length > 0 ? pruned : undefined;
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
function isTerraformDocument(doc) {
|
|
62
|
+
return Boolean(doc && typeof doc === 'object' && 'resource' in doc);
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function toYaml(value: unknown): string;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toYaml = toYaml;
|
|
4
|
+
const PAD = ' ';
|
|
5
|
+
function toYaml(value) {
|
|
6
|
+
return render(value, 0);
|
|
7
|
+
}
|
|
8
|
+
function render(value, level) {
|
|
9
|
+
if (isScalar(value)) {
|
|
10
|
+
return formatScalar(value);
|
|
11
|
+
}
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
if (value.length === 0) {
|
|
14
|
+
return `${indent(level)}[]`;
|
|
15
|
+
}
|
|
16
|
+
return value
|
|
17
|
+
.map((item) => {
|
|
18
|
+
if (isScalar(item)) {
|
|
19
|
+
return `${indent(level)}- ${formatScalar(item)}`;
|
|
20
|
+
}
|
|
21
|
+
const rendered = render(item, level + 1);
|
|
22
|
+
const lines = rendered.split('\n');
|
|
23
|
+
const first = lines[0].startsWith(indent(level + 1))
|
|
24
|
+
? lines[0].slice(indent(level + 1).length)
|
|
25
|
+
: lines[0];
|
|
26
|
+
const head = `${indent(level)}- ${first}`;
|
|
27
|
+
const tail = lines.length > 1
|
|
28
|
+
? lines
|
|
29
|
+
.slice(1)
|
|
30
|
+
.map((line) => line)
|
|
31
|
+
.join('\n')
|
|
32
|
+
: '';
|
|
33
|
+
return tail ? `${head}\n${tail}` : head;
|
|
34
|
+
})
|
|
35
|
+
.join('\n');
|
|
36
|
+
}
|
|
37
|
+
if (isPlainObject(value)) {
|
|
38
|
+
const entries = Object.entries(value);
|
|
39
|
+
if (entries.length === 0) {
|
|
40
|
+
return `${indent(level)}{}`;
|
|
41
|
+
}
|
|
42
|
+
return entries
|
|
43
|
+
.map(([key, val]) => {
|
|
44
|
+
if (isScalar(val)) {
|
|
45
|
+
return `${indent(level)}${key}: ${formatScalar(val)}`;
|
|
46
|
+
}
|
|
47
|
+
const rendered = render(val, level + 1);
|
|
48
|
+
return `${indent(level)}${key}:\n${rendered}`;
|
|
49
|
+
})
|
|
50
|
+
.join('\n');
|
|
51
|
+
}
|
|
52
|
+
return `${indent(level)}${JSON.stringify(value)}`;
|
|
53
|
+
}
|
|
54
|
+
function formatScalar(value) {
|
|
55
|
+
if (value === null || value === undefined) {
|
|
56
|
+
return 'null';
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === 'string') {
|
|
59
|
+
return needsQuoting(value) ? JSON.stringify(value) : value;
|
|
60
|
+
}
|
|
61
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
62
|
+
return String(value);
|
|
63
|
+
}
|
|
64
|
+
return JSON.stringify(value);
|
|
65
|
+
}
|
|
66
|
+
function indent(level) {
|
|
67
|
+
return PAD.repeat(level);
|
|
68
|
+
}
|
|
69
|
+
function isPlainObject(value) {
|
|
70
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
71
|
+
}
|
|
72
|
+
function isScalar(value) {
|
|
73
|
+
return (value === null ||
|
|
74
|
+
value === undefined ||
|
|
75
|
+
typeof value === 'string' ||
|
|
76
|
+
typeof value === 'number' ||
|
|
77
|
+
typeof value === 'boolean');
|
|
78
|
+
}
|
|
79
|
+
function needsQuoting(value) {
|
|
80
|
+
return /[:{}[\],&*#?|<>=%@`]/.test(value) || value.includes('"') || value.includes("'") || value.includes('\n');
|
|
81
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "parse-hcl",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lightweight HCL parser focused on identifying and classifying blocks. Supports common Terraform blocks (resource, variable, output, locals, etc.) for tooling and automation.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"require": "./dist/index.js",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"parse-hcl": "./dist/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"repository": "https://github.com/sigmoid-hq/parse-hcl",
|
|
19
|
+
"author": "Juan Lee <juan.lee@sigmoid.us>",
|
|
20
|
+
"license": "Apache-2.0",
|
|
21
|
+
"private": false,
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"keywords": [
|
|
28
|
+
"terraform",
|
|
29
|
+
"hcl",
|
|
30
|
+
"parser",
|
|
31
|
+
"tfvars",
|
|
32
|
+
"tfstate",
|
|
33
|
+
"plan"
|
|
34
|
+
],
|
|
35
|
+
"sideEffects": false,
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.json",
|
|
41
|
+
"build:examples": "tsc -p tsconfig.examples.json",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:coverage": "vitest run --coverage",
|
|
44
|
+
"lint": "eslint src",
|
|
45
|
+
"lint:fix": "eslint src --fix",
|
|
46
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
47
|
+
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
48
|
+
"prepublishOnly": "yarn build && yarn test && yarn lint",
|
|
49
|
+
"example": "yarn build && yarn build:examples && node dist/examples/runExample.js",
|
|
50
|
+
"cli": "yarn build && node dist/cli.js",
|
|
51
|
+
"example:usage": "yarn build && yarn build:examples && node dist/examples/usageBasic.js",
|
|
52
|
+
"example:artifacts": "yarn build && yarn build:examples && node dist/examples/usageArtifacts.js"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "^25.0.1",
|
|
56
|
+
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
|
57
|
+
"@typescript-eslint/parser": "^8.50.1",
|
|
58
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
59
|
+
"eslint": "^9.39.2",
|
|
60
|
+
"eslint-config-prettier": "^10.1.8",
|
|
61
|
+
"globals": "^16.5.0",
|
|
62
|
+
"prettier": "^3.7.4",
|
|
63
|
+
"typescript": "^5.9.3",
|
|
64
|
+
"vitest": "^4.0.15"
|
|
65
|
+
}
|
|
66
|
+
}
|