@cruglobal/js-hcl2 - v0.1.1
    Preparing search index...

    @cruglobal/js-hcl2 - v0.1.1

    @cruglobal/js-hcl2

    npm license

    Status: AI-generated, not actively maintained. This library was authored primarily by an AI assistant against the specification in docs/design.md and is not on anyone's active roadmap. Dependabot keeps dependencies and security advisories up to date automatically (patch + minor bumps auto-merge; majors require manual review), but feature work, bug fixes, and other changes happen on a best-effort basis. Pull requests and issues are welcome — they may take time to be reviewed. See CONTRIBUTING.md for the contribution workflow.

    Parse and encode HashiCorp Configuration Language v2 (HCL2) in TypeScript. Unlike every other npm HCL2 reader, this library supports both directions — reading HCL into JS values and emitting HCL from JS values — plus a lossless round-trip Document API that preserves comments and formatting across edits.

    import * as HCL from "@cruglobal/js-hcl2";

    // Parse
    HCL.parse('name = "demo"\nport = 8080\n');
    // → { name: "demo", port: 8080 }

    // Emit
    HCL.stringify({ name: "demo", port: 8080 });
    // → 'name = "demo"\nport = 8080\n'

    // Edit while preserving trivia
    const doc = HCL.parseDocument('# greeting\nname = "demo"\n');
    doc.set("name", "production");
    doc.toString();
    // → '# greeting\nname = "production"\n'

    Runs on Node.js, Bun, Deno, and modern browsers. Zero runtime dependencies.


    npm install @cruglobal/js-hcl2
    

    The package ships both ESM and CJS builds plus TypeScript .d.tss. Pick whichever your bundler or runtime prefers:

    // ESM
    import { parse, stringify, parseDocument } from "@cruglobal/js-hcl2";

    // default export — same namespace
    import HCL from "@cruglobal/js-hcl2";
    HCL.parse(source);
    // CJS
    const { parse, stringify, parseDocument } = require("@cruglobal/js-hcl2");

    import { parse } from "@cruglobal/js-hcl2";

    parse(`
    terraform_version = "1.5.0"
    enabled = true
    regions = ["us-east-1", "us-west-2"]
    `);
    /* {
    terraform_version: "1.5.0",
    enabled: true,
    regions: ["us-east-1", "us-west-2"],
    }
    */

    Blocks — including Terraform's resource "type" "name" { … } shape — project into nested objects. Repeated blocks with identical labels collect into arrays:

    parse(`
    resource "aws_s3_bucket" "a" { acl = "private" }
    resource "aws_s3_bucket" "b" { acl = "public" }
    `);
    /* {
    resource: {
    aws_s3_bucket: {
    a: { acl: "private" },
    b: { acl: "public" },
    },
    },
    }
    */

    Expressions that involve variables, operators, calls, or interpolated templates don't collapse to primitives — they come back as an opaque Expression wrapper with the original source and structural AST preserved:

    import { isExpression, parse } from "@cruglobal/js-hcl2";

    const v = parse('tags = merge(var.a, { env = "dev" })\n');
    const expr = (v as Record<string, unknown>).tags;
    if (isExpression(expr)) {
    expr.source; // → 'merge(var.a, { env = "dev" })'
    expr.kind; // → "function-call"
    expr.ast; // → full FunctionCallNode
    }
    import { stringify } from "@cruglobal/js-hcl2";

    stringify({
    resource: {
    aws_s3_bucket: {
    a: { acl: "private" },
    b: { acl: "public" },
    },
    },
    });
    /* resource "aws_s3_bucket" "a" {
    acl = "private"
    }
    resource "aws_s3_bucket" "b" {
    acl = "public"
    }
    */

    stringify accepts JSON-style options:

    stringify(value, {
    indent: 4, // spaces per nesting level (default 2)
    sortKeys: true, // alphabetize body and object keys
    trailingNewline: false,
    replacer: (key, val) => (key === "secret" ? undefined : val),
    });
    import { parseDocument } from "@cruglobal/js-hcl2";

    const doc = parseDocument(`
    # Production database
    resource "aws_db_instance" "main" {
    engine = "postgres"
    engine_version = "15.3" # pinned to match prod
    }
    `);

    doc.set(["resource", "aws_db_instance", "main", "engine_version"], "16.1");
    doc.set(["resource", "aws_db_instance", "main", "skip_final_snapshot"], true);

    console.log(doc.toString());
    // # Production database
    // resource "aws_db_instance" "main" {
    // engine = "postgres"
    // engine_version = "16.1" # pinned to match prod
    // skip_final_snapshot = true
    // }

    parseDocument(source).toString() === source for any unedited input (byte-identical). Edits preserve leading/trailing trivia around the node being replaced or deleted.


    High-level surface. Full signatures and JSDoc in the generated TypeDoc site.

    Entry point Purpose
    parse(source, options?) Parse HCL text into a plain Value.
    stringify(value, options?) Emit canonical HCL text from a Value.
    parseDocument(source, options?) Parse into a trivia-aware Document supporting lossless round-trip + edits.
    Document#toString() Re-emit the CST (byte-identical when unedited).
    Document#toValue() Same shape as parse().
    Document#get(path) Resolve a dotted / array path to a CST node.
    Document#set(path, value) Replace an attribute's value or insert a new attribute.
    Document#delete(path) Remove an attribute or whole block, cleaning surrounding trivia.

    Lower-level building blocks are also exported — SourceFile, lex, Parser, parseExpr, print, toValue, exprToValue, HCLParseError, the full CST node type union (BodyNode, AttributeNode, BlockNode, ExprNode, TemplateNode, etc.), and the Expression wrapper type. See docs/design.md for how they fit together.

    Both parse and parseDocument throw HCLParseError on malformed input. Each error carries filename, line, column, offset, range, and a caret-marked snippet:

    import { HCLParseError, parse } from "@cruglobal/js-hcl2";

    try {
    parse("x = \n", { filename: "main.tf" });
    } catch (e) {
    if (e instanceof HCLParseError) {
    console.error(`${e.filename}:${e.line}:${e.column}: ${e.message}`);
    console.error(e.snippet);
    }
    }

    Pass { bail: false } to collect every error in one pass (thrown as an aggregate HCLParseError whose errors[] has one entry per failure).


    Feature Supported Notes
    Attributes
    Blocks (0 / 1 / 2 / 3+ labels)
    One-liner blocks (block { k = v })
    Line comments (#, //)
    Block comments (/* … */)
    Primitive literals: number, bool, null, string Numbers are finite JS doubles; NaN/Infinity encode as null on emit.
    Quoted strings with escapes (\n \t \" \\ \uNNNN)
    Heredocs (<<EOT … EOT)
    Heredoc strip form (<<-EOT) Recognised structurally; body content stored verbatim (strip happens at evaluation time — see below).
    Tuple and object literals (with trailing commas)
    Traversal (.attr, [expr], legacy .digit)
    Attribute splat (a.*.b) and full splat (a[*].b)
    Function calls (f(a, b, c...))
    Unary - / !
    Binary + - * / % == != < <= > >= && ||
    Conditional cond ? then : else
    For expressions (tuple + object form with if, ...)
    Template interpolation (${…}) in strings + heredocs
    Template control directives (%{if}, %{for})
    Strip markers (${~ ~}, %{~ ~})
    Unicode identifiers (UAX #31) + dash in ID_Continue
    Feature Status Tracked as
    Expression evaluation Future milestone — everything non-literal is returned as an Expression wrapper rather than reduced to a primitive.
    Standard function library (jsonencode, merge, …) Requires evaluator.
    JSON-syntax HCL (.tf.json) Planned for v0.2.
    Schema-directed decoding (Zod-style) Later sub-package.
    Runtime Supported CI-enforced
    Node.js 18+ ✅ (24.x)
    Bun 1.x
    Deno 2.x
    Modern browsers (ES2022) Smoke via happy-dom

    This repo uses asdf to pin the exact Node.js version (see .tool-versions). After cloning:

    asdf plugin add nodejs   # one-time, if not already set up
    asdf install
    npm install
    npm test

    See CONTRIBUTING.md for the full workflow and docs/design.md for the architectural overview.


    BSD-3-Clause. Test fixtures vendored from external projects retain their original licenses; see NOTICES.md for attributions. Vendored fixtures are excluded from the published npm tarball.