npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hiscojs/yaml-updater

v1.0.20

Published

Type-safe, immutable YAML updates with comment preservation, YAML/JSON formatting control, per-item array formatting, multi-document support, and advanced array merging strategies

Readme

@hiscojs/yaml-updater

Type-safe, immutable YAML updates with comment preservation, multi-document support, and advanced array merging strategies.

Built on top of @hiscojs/object-updater for powerful object manipulation capabilities.

Installation

npm install @hiscojs/yaml-updater

Quick Start

import { updateYaml } from '@hiscojs/yaml-updater';

const yamlString = `
apiVersion: v1
kind: ConfigMap
data:
  database: localhost
`;

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.data,
      merge: () => ({ database: 'db.production.com' })
    });
  }
});

console.log(result);
// apiVersion: v1
// kind: ConfigMap
// data:
//   database: db.production.com

Features

  • Type-Safe: Full TypeScript support with generic type parameters
  • Comment Preservation: Automatically preserves existing YAML comments
  • Comment Manipulation: Add, remove, or update comments programmatically
  • Complete YAML Sub-Documents: Extract and insert YAML blocks with all formatting preserved (new!)
  • Document Headers: Add, extract, and manage headers at the top of YAML documents
  • YAML Anchors & Aliases: Create reusable content with anchors, aliases, and merge keys
  • Anchor Renaming: Rename anchors and update all references document-wide
  • Schema-Level Instructions: Define structure metadata separately from data (OpenAPI-style)
  • YAML vs JSON Formatting: Control output format with flow and flowItems instructions
  • Per-Item Array Formatting: Format each array item as JSON or YAML individually
  • Empty File Support: Handle empty YAML strings gracefully
  • Multi-Document Support: Handle YAML files with multiple documents
  • Immutable: Original YAML strings are never modified
  • Advanced Array Merging: Multiple strategies for merging arrays
  • Proxy-Based Path Tracking: Automatic path detection
  • Formatting Preservation: Maintains indentation and spacing

API Reference

updateYaml<T>(options)

Updates a YAML string immutably with type safety and comment preservation.

Parameters

interface UpdateYamlOptions<T> {
  yamlString: string;
  selectDocument?: (yamlDocuments: Document[]) => number;
  schema?: YAMLSchema;  // Optional schema-level instructions
  documentHeader?: DocumentHeader;  // Optional document header
  annotate?: (annotator: {
    change: <L>(options: ChangeOptions<T, L>) => void;
    setYamlNode: <L>(options: SetYamlNodeOptions<T, L>) => void;
    getYamlNode: <L>(options: GetYamlNodeOptions<T, L>) => string;
  }) => void;
  defaultFlow?: boolean;  // Default flow style for all nodes
}

interface YAMLSchema {
  properties?: Record<string, SchemaProperty>;
}

interface SchemaProperty {
  // Comment instructions
  comment?: string;
  commentBefore?: string;
  commentAfter?: string;
  removeComment?: boolean;

  // Formatting instructions
  flow?: boolean;
  flowItems?: boolean[];

  // Anchor instructions
  anchor?: string;
  renameFrom?: string;
  alias?: string;
  mergeAnchor?: string;
  anchors?: Record<number, string>;
  aliases?: string[];

  // Nested structure
  type?: 'array';
  properties?: Record<string, SchemaProperty>;
  items?: SchemaProperty[];
}

Returns

interface YamlEdit<T> {
  result: string;           // Updated YAML string
  resultParsed: T;          // Parsed updated object
  originalParsed: T;        // Original parsed object
  comments: Array<{         // Comments that were added
    path: (string | number)[];
    comment: string;
  }>;
  extractedHeader?: ExtractedHeader;  // Extracted document header (if present)
}

change<L>(options)

Defines a single change operation with optional commenting.

interface ChangeOptions<T, L> {
  findKey: (parsed: T) => L;
  merge: (originalValue: L) => Partial<L>;
  comment?: (previousComment?: string) => string | undefined;
}

setYamlNode<L>(options)

Inserts a complete YAML sub-document with all formatting, comments, and anchors preserved exactly as-is.

interface SetYamlNodeOptions<T, L> {
  findKey: (parsed: T) => L;
  yamlString: string;  // YAML string to insert
  comment?: (previousComment?: string) => string | undefined;
}

getYamlNode<L>(options)

Extracts a complete YAML sub-document as a string with all formatting, comments, and anchors preserved.

interface GetYamlNodeOptions<T, L> {
  findKey: (parsed: T) => L;
}

// Returns: string (YAML with all metadata preserved)

Basic Usage

Simple Property Update

const yamlString = `
server:
  host: localhost
  port: 3000
`;

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.server,
      merge: () => ({ port: 8080 })
    });
  }
});

// server:
//   host: localhost
//   port: 8080

Type-Safe Updates

interface Config {
  server: {
    host: string;
    port: number;
  };
  database: {
    host: string;
    port: number;
  };
}

const { result, resultParsed } = updateYaml<Config>({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.server,  // Fully typed!
      merge: () => ({ port: 8080 })
    });
  }
});

console.log(resultParsed.server.port);  // Type-safe access

Document Headers

Add, extract, and manage headers at the top of YAML documents for documentation, warnings, or metadata.

Three Header Types

1. Simple Headers

Each line prefixed with # :

const { result } = updateYaml({
  yamlString: myYaml,
  documentHeader: {
    type: 'simple',
    content: ['API Gateway configuration', 'Owner: platform-team']
  },
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.data,
      merge: () => ({ database: 'db.production.com' })
    });
  }
});

// Output:
// # API Gateway configuration
// # Owner: platform-team
// apiVersion: v1
// kind: ConfigMap
// data:
//   database: db.production.com

2. Multi-line Headers

Bordered headers with customizable borders and width:

const { result } = updateYaml({
  yamlString: myYaml,
  documentHeader: {
    type: 'multi-line',
    content: [
      'PRODUCTION KUBERNETES CONFIG',
      'DO NOT MODIFY WITHOUT APPROVAL',
      'Contact: [email protected]'
    ],
    border: '#',   // Optional: default is '#'
    width: 60      // Optional: auto-calculated from content if not specified
  }
});

// Output:
// ########################################
// # PRODUCTION KUBERNETES CONFIG
// # DO NOT MODIFY WITHOUT APPROVAL
// # Contact: [email protected]
// ########################################
// apiVersion: v1
// kind: Service

3. Raw Headers

User-controlled exact formatting:

const { result } = updateYaml({
  yamlString: myYaml,
  documentHeader: {
    type: 'raw',
    content: `###
### Custom styled header
### With any format you want
###`
  }
});

// Output:
// ###
// ### Custom styled header
// ### With any format you want
// ###
// apiVersion: v1
// kind: ConfigMap

Extracting Existing Headers

Extract headers from YAML files with formatting stripped based on type:

const yamlWithHeader = `# Configuration File
# Version: 1.0
# Owner: DevOps
apiVersion: v1
kind: ConfigMap`;

const { extractedHeader } = updateYaml({
  yamlString: yamlWithHeader,
  documentHeader: {
    type: 'simple',
    content: []  // Empty content triggers extraction
  }
});

console.log(extractedHeader?.content);
// ['Configuration File', 'Version: 1.0', 'Owner: DevOps']

console.log(extractedHeader?.raw);
// '# Configuration File\n# Version: 1.0\n# Owner: DevOps'

Replacing Headers

Headers are always replaced with new ones:

const { result } = updateYaml({
  yamlString: yamlWithOldHeader,
  documentHeader: {
    type: 'simple',
    content: ['Updated Configuration', 'Version: 2.0']
  }
});

// Old header is removed, new header added

Removing Headers

Provide empty content to remove headers:

const { result } = updateYaml({
  yamlString: yamlWithHeader,
  documentHeader: {
    type: 'simple',
    content: []  // Empty array removes header
  }
});

Headers with Multi-Document YAML

Headers are applied to the document being modified (respects selectDocument):

const { result } = updateYaml({
  yamlString: multiDocYaml,
  selectDocument: () => 1,  // Second document
  documentHeader: {
    type: 'simple',
    content: ['Second document configuration']
  },
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.metadata,
      merge: () => ({ namespace: 'production' })
    });
  }
});

// Header applied only to second document

Real-World Header Examples

Production Warning Header

const { result } = updateYaml({
  yamlString: deploymentYaml,
  documentHeader: {
    type: 'multi-line',
    content: [
      'PRODUCTION ENVIRONMENT',
      'DO NOT EDIT MANUALLY',
      'Managed by GitOps - Changes will be overwritten',
      'Contact: [email protected]'
    ]
  }
});

Configuration Metadata

const { result } = updateYaml({
  yamlString: configYaml,
  documentHeader: {
    type: 'simple',
    content: [
      'Application Configuration',
      `Generated: ${new Date().toISOString()}`,
      'Environment: production',
      'Version: 2.1.0'
    ]
  }
});

Custom Format for Documentation

const { result } = updateYaml({
  yamlString: valuesYaml,
  documentHeader: {
    type: 'raw',
    content: `# ============================================
#  Helm Chart Values
#  Chart: myapp
#  Version: 1.0.0
# ============================================`
  }
});

DocumentHeader Type Reference

interface DocumentHeader {
  type: 'simple' | 'multi-line' | 'raw';
  content: string | string[];
  border?: string;      // For multi-line only (default: '#')
  width?: number;       // For multi-line only (default: auto-calculated)
}

interface ExtractedHeader {
  type: 'simple' | 'multi-line' | 'raw';
  content: string[];    // Parsed content with formatting stripped
  raw: string;          // Raw header string as it appeared
}

For more details, see DOCUMENT-HEADERS.md.

Comment Management

Adding Comments

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.spec,
      merge: () => ({ replicas: 3 }),
      comment: () => 'Scaled to 3 replicas for high availability'
    });
  }
});

// spec:
//   # Scaled to 3 replicas for high availability
//   replicas: 3

Dynamic Comments

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.spec,
      merge: (originalValue) => ({
        replicas: originalValue.replicas * 2
      }),
      comment: (prevComment) =>
        `Scaled from ${originalValue.replicas} to ${originalValue.replicas * 2}`
    });
  }
});

Using addInstructions

The addInstructions helper provides advanced control over properties including comments, merge strategies, and formatting:

import { updateYaml, addInstructions } from '@hiscojs/yaml-updater';

// Available options:
addInstructions({
  prop: 'propertyName',           // Property to apply instructions to

  // Comment options:
  comment: 'Comment text',        // Add block comment above the property
  removeComment: true,            // Remove existing comment
  commentBefore: 'Text',          // Alternative to comment (same as comment)
  commentAfter: 'Text',           // Add inline comment (same line as property)

  // Array merge strategies:
  mergeByContents: true,          // Deduplicate by deep equality
  mergeByName: true,              // Merge by 'name' property
  mergeByProp: 'customProp',      // Merge by custom property
  deepMerge: true,                // Deep merge nested objects

  // Formatting options (YAML-specific):
  flow: true,                     // true = JSON format, false = YAML format
  flowItems: [true, false, true], // Per-item format for arrays
  hideNull: true                   // Hide null values (renders as "key:" not "key: null")
})

Example with comments:

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'data',
          comment: 'Configuration data section'
        }),
        data: {
          key: 'value'
        }
      })
    });
  }
});

// # Configuration data section
// data:
//   key: value

Example with inline comments (commentAfter):

const { result } = updateYaml({
  yamlString: '',
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'enabled',
          commentAfter: 'toggle deployment'
        }),
        enabled: null,  // null values with inline comments render as "enabled: # comment"
        ...addInstructions({
          prop: 'replicas',
          commentAfter: 'number of replicas'
        }),
        replicas: 3  // Non-null values render as "replicas: 3 # comment"
      })
    });
  }
});

// enabled: # toggle deployment
// replicas: 3 # number of replicas

Note on inline comments with objects: When a property has an object/array value (not a scalar), the inline comment appears as a block comment at the start of the content:

const { result } = updateYaml({
  yamlString: '',
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'metadata',
          commentAfter: 'metadata comment'
        }),
        metadata: {
          name: 'test'
        }
      })
    });
  }
});

// metadata:
//   # metadata comment
//   name: test

Example with hideNull:

const { result } = updateYaml({
  yamlString: stringify({
    charts: { sealedSecrets: {} },
    sealedSecrets: null
  }),
  annotate: ({ change }) => {
    change({
      findKey: (obj) => obj.charts.sealedSecrets,
      merge: () => ({
        ...addInstructions({
          prop: 'enabled',
          commentAfter: 'toggle deployment',
          hideNull: true  // Hides null, shows "enabled:" not "enabled: null"
        }),
        enabled: null
      })
    });

    change({
      findKey: (obj) => obj,
      merge: () => ({
        ...addInstructions({
          prop: 'sealedSecrets',
          hideNull: true  // Hides value, shows "sealedSecrets:" not "sealedSecrets: null"
        }),
        sealedSecrets: null
      })
    });
  }
});

// charts:
//   sealedSecrets:
//     enabled: # toggle deployment
// sealedSecrets:

Removing Comments

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'data',
          removeComment: true
        }),
        data: { key: 'value' }
      })
    });
  }
});

Working with Complete YAML Sub-Documents

The getYamlNode() and setYamlNode() functions allow you to extract and insert complete YAML sub-documents with all formatting, comments, and anchors preserved exactly as-is.

Why Use These Functions?

vs. change() + merge():

  • change() works with plain JavaScript objects - loses all YAML metadata (comments, anchors, formatting)
  • getYamlNode() + setYamlNode() preserve everything: comments, anchors, aliases, flow style, indentation

Use cases:

  • Extract configuration sections from large YAML files
  • Copy/move YAML blocks between locations
  • Move sub-documents to root level
  • Migrate YAML sections to separate files
  • Template generation with full formatting control

Extracting YAML Sub-Documents

Use getYamlNode() to extract a complete YAML sub-document as a string:

const yamlString = `
database:
  # Production database configuration
  host: db.production.com
  port: 5432
  credentials:
    # Stored in secrets manager
    username: admin
    password: secret
`;

let extractedYaml = '';
updateYaml({
  yamlString,
  annotate: ({ getYamlNode }) => {
    extractedYaml = getYamlNode({
      findKey: (parsed) => parsed.database
    });
  }
});

console.log(extractedYaml);
// Output:
// # Production database configuration
// host: db.production.com
// port: 5432
// credentials:
//   # Stored in secrets manager
//   username: admin
//   password: secret

All metadata is preserved:

  • ✓ All comments (block and inline)
  • ✓ Anchors and aliases
  • ✓ Flow style formatting
  • ✓ Exact indentation and spacing

Inserting YAML Sub-Documents

Use setYamlNode() to insert a complete YAML string:

const mainYaml = `
application:
  name: myapp
database: {}
`;

const databaseConfig = `
# Database configuration
host: localhost
port: 5432
credentials:
  # Sensitive data
  username: admin
  password: secret
`;

const { result } = updateYaml({
  yamlString: mainYaml,
  annotate: ({ setYamlNode }) => {
    setYamlNode({
      findKey: (parsed) => parsed.database,
      yamlString: databaseConfig,
      comment: () => 'Complete database configuration'
    });
  }
});

// Output:
// application:
//   name: myapp
// # Complete database configuration
// database:
//   # Database configuration
//   host: localhost
//   port: 5432
//   credentials:
//     # Sensitive data
//     username: admin
//     password: secret

Copy/Move Operations

Combine getYamlNode() and setYamlNode() for powerful copy/move operations:

const yamlString = `
environments:
  staging:
    # Staging environment config
    replicas: 2
    resources:
      memory: 1Gi
      cpu: 500m
    features:
      - feature-flags
      - debug-mode  # Enable debug logs
  production:
    replicas: 5
`;

const { result } = updateYaml({
  yamlString,
  annotate: ({ getYamlNode, setYamlNode }) => {
    // Extract staging resources
    const stagingResources = getYamlNode({
      findKey: (parsed) => parsed.environments.staging.resources
    });

    // Copy to production (all formatting preserved)
    setYamlNode({
      findKey: (parsed) => parsed.environments.production.resources,
      yamlString: stagingResources,
      comment: () => 'Copied from staging'
    });
  }
});

// Output:
// environments:
//   staging:
//     # Staging environment config
//     replicas: 2
//     resources:
//       memory: 1Gi
//       cpu: 500m
//     features:
//       - feature-flags
//       - debug-mode # Enable debug logs
//   production:
//     replicas: 5
//     # Copied from staging
//     resources:
//       memory: 1Gi
//       cpu: 500m

Moving Sub-Document to Root Level

Extract a sub-document and make it the entire document:

const yamlString = `
# Top level comment
application:
  name: myapp
  version: 1.0.0

database:
  # Database configuration
  host: localhost
  port: 5432
  credentials:
    # Sensitive data
    username: admin
    password: secret
  settings:
    pool_size: 10  # Connection pool
    timeout: 30

cache:
  enabled: true
`;

const { result } = updateYaml({
  yamlString,
  annotate: ({ getYamlNode, setYamlNode }) => {
    // Extract database configuration
    const databaseConfig = getYamlNode({
      findKey: (parsed) => parsed.database
    });

    // Replace entire document with database config
    setYamlNode({
      findKey: (parsed) => parsed,  // Root level
      yamlString: databaseConfig
    });
  }
});

// Output (database is now the entire document):
// # Database configuration
// host: localhost
// port: 5432
// credentials:
//   # Sensitive data
//   username: admin
//   password: secret
// settings:
//   pool_size: 10 # Connection pool
//   timeout: 30

Result:

  • Database configuration becomes the entire document
  • All comments preserved at every level
  • Original top-level fields (application, cache) removed
  • The database: key is gone - its contents are now the root

Extracting Different Types

Extract scalar values:

const yaml = `version: "1.0.0"`;
const version = getYamlNode({ findKey: (p) => p.version });
// Returns: "1.0.0"

Extract arrays with comments:

const yaml = `
items:
  - item1
  - item2  # important item
  - item3
`;
const items = getYamlNode({ findKey: (p) => p.items });
// Returns:
// - item1
// - item2  # important item
// - item3

Extract with anchors:

const yaml = `
defaults: &def
  timeout: 30
  retries: 3
production:
  host: prod.com
`;
const defaults = getYamlNode({ findKey: (p) => p.defaults });
// Returns:
// &def
// timeout: 30
// retries: 3

Error Handling

getYamlNode() throws an error if the path doesn't exist:

try {
  updateYaml({
    yamlString: 'foo: bar',
    annotate: ({ getYamlNode }) => {
      getYamlNode({ findKey: (p) => p.nonexistent });  // Throws error
    }
  });
} catch (error) {
  // Error: Cannot extract YAML node: path ["nonexistent"] not found in document
}

Comparison: change() vs setYamlNode()

| Feature | change() + merge() | setYamlNode() | |---------|------------------------|------------------| | Input | JavaScript object | YAML string | | Comments | Lost (must add manually) | Preserved exactly | | Anchors | Lost (must recreate) | Preserved exactly | | Formatting | New formatting applied | Original formatting kept | | Flow style | Lost | Preserved | | Use case | Modify values | Insert complete YAML blocks |

When to use change():

  • Updating specific property values
  • Merging new data with existing
  • Working with plain JavaScript objects

When to use setYamlNode() + getYamlNode():

  • Copying YAML sections with all formatting
  • Moving configuration blocks
  • Extracting sub-documents to separate files
  • Preserving all metadata exactly

Real-World Example: Configuration Migration

// Extract staging config and promote to production
const { result } = updateYaml({
  yamlString: multiEnvConfig,
  annotate: ({ getYamlNode, setYamlNode }) => {
    // Extract staging configuration
    const stagingConfig = getYamlNode({
      findKey: (parsed) => parsed.environments.staging
    });

    // Archive current production to rollback
    const currentProd = getYamlNode({
      findKey: (parsed) => parsed.environments.production
    });
    setYamlNode({
      findKey: (parsed) => parsed.environments.rollback,
      yamlString: currentProd,
      comment: () => 'Previous production (for rollback)'
    });

    // Promote staging to production
    setYamlNode({
      findKey: (parsed) => parsed.environments.production,
      yamlString: stagingConfig,
      comment: () => 'Promoted from staging'
    });
  }
});

Array Merging Strategies

mergeByContents - Deduplicate by Deep Equality

const yamlString = `
items:
  - item1
  - item2
`;

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'items',
          mergeByContents: true
        }),
        items: ['item2', 'item3']  // item2 deduplicated
      })
    });
  }
});

// items:
//   - item1
//   - item2
//   - item3

mergeByName - Merge by Name Property

const yamlString = `
spec:
  containers:
    - name: app
      image: myapp:1.0
    - name: sidecar
      image: sidecar:1.0
`;

interface Container {
  name: string;
  image: string;
}

interface PodSpec {
  spec: {
    containers: Container[];
  };
}

const { result } = updateYaml<PodSpec>({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.spec,
      merge: () => ({
        ...addInstructions({
          prop: 'containers',
          mergeByName: true
        }),
        containers: [
          { name: 'app', image: 'myapp:2.0' }  // Updates 'app' container
        ]
      })
    });
  }
});

// spec:
//   containers:
//     - name: app
//       image: myapp:2.0      # Updated
//     - name: sidecar
//       image: sidecar:1.0    # Preserved

mergeByProp - Merge by Custom Property

const yamlString = `
services:
  - serviceId: api
    url: http://api.local
  - serviceId: db
    url: postgres://db.local
`;

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'services',
          mergeByProp: 'serviceId'
        }),
        services: [
          { serviceId: 'api', url: 'https://api.prod' }
        ]
      })
    });
  }
});

deepMerge - Deep Merge Nested Objects

const yamlString = `
configs:
  - name: database
    settings:
      timeout: 30
      pool: 10
      ssl: true
`;

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'configs',
          mergeByName: true,
          deepMerge: true
        }),
        configs: [
          {
            name: 'database',
            settings: { timeout: 60 }  // Only update timeout
          }
        ]
      })
    });
  }
});

// configs:
//   - name: database
//     settings:
//       timeout: 60    # Updated
//       pool: 10       # Preserved
//       ssl: true      # Preserved

Using originalValue

Access original values to make conditional updates:

const yamlString = `
apiVersion: apps/v1
kind: Deployment
spec:
  replicas: 2
`;

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.spec,
      merge: (originalValue) => ({
        replicas: originalValue.replicas + 1  // Increment by 1
      }),
      comment: () => `Scaled from ${originalValue.replicas} to ${originalValue.replicas + 1} replicas`
    });
  }
});

// spec:
//   # Scaled from 2 to 3 replicas
//   replicas: 3

Version Bumping

const yamlString = `
version: "1.2.3"
`;

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: (originalValue) => {
        const [major, minor, patch] = originalValue.version.split('.').map(Number);
        return {
          version: `${major}.${minor}.${patch + 1}`
        };
      }
    });
  }
});

// version: "1.2.4"

Multi-Document YAML

Select Document by Index

const yamlString = `---
apiVersion: v1
kind: ConfigMap
data:
  key1: value1
---
apiVersion: v1
kind: Secret
data:
  key2: value2`;

const { result } = updateYaml({
  yamlString,
  selectDocument: () => 1,  // Select second document (Secret)
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.data,
      merge: () => ({ key2: 'updated' })
    });
  }
});

Select Document by Content

const { result } = updateYaml({
  yamlString,
  selectDocument: (docs) => {
    return docs.findIndex(doc => {
      const parsed = doc.toJSON();
      return parsed.kind === 'Deployment';
    });
  },
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.spec,
      merge: () => ({ replicas: 5 })
    });
  }
});

Real-World Examples

Kubernetes Deployment Update

const deploymentYaml = `
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 1
  template:
    spec:
      containers:
        - name: app
          image: myapp:1.0
          env:
            - name: ENV
              value: dev
`;

interface K8sDeployment {
  apiVersion: string;
  kind: string;
  metadata: { name: string };
  spec: {
    replicas: number;
    template: {
      spec: {
        containers: Array<{
          name: string;
          image: string;
          env?: Array<{ name: string; value: string }>;
        }>;
      };
    };
  };
}

const { result } = updateYaml<K8sDeployment>({
  yamlString: deploymentYaml,
  annotate: ({ change }) => {
    // Scale replicas
    change({
      findKey: (parsed) => parsed.spec,
      merge: () => ({ replicas: 3 }),
      comment: () => 'Scaled to 3 replicas for high availability'
    });

    // Update container image
    change({
      findKey: (parsed) => parsed.spec.template.spec,
      merge: () => ({
        ...addInstructions({
          prop: 'containers',
          mergeByName: true
        }),
        containers: [
          {
            name: 'app',
            image: 'myapp:2.0',
            env: [
              { name: 'ENV', value: 'production' }
            ]
          }
        ]
      }),
      comment: () => 'Updated to production environment'
    });
  }
});

ConfigMap Updates

const configMapYaml = `
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  database_host: localhost
  cache_enabled: "false"
`;

const { result } = updateYaml({
  yamlString: configMapYaml,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.data,
      merge: () => ({
        database_host: 'db.production.com',
        cache_enabled: 'true',
        redis_host: 'redis.production.com'
      }),
      comment: () => 'Updated for production environment'
    });
  }
});

// apiVersion: v1
// kind: ConfigMap
// metadata:
//   name: app-config
// data:
//   # Updated for production environment
//   database_host: db.production.com
//   cache_enabled: "true"
//   redis_host: redis.production.com

Helm Values Update

const valuesYaml = `
replicaCount: 1

image:
  repository: myapp
  tag: "1.0.0"
  pullPolicy: IfNotPresent

resources:
  limits:
    cpu: 100m
    memory: 128Mi
  requests:
    cpu: 100m
    memory: 128Mi
`;

const { result } = updateYaml({
  yamlString: valuesYaml,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: (original) => ({
        replicaCount: 3,
        image: {
          ...original.image,
          tag: '2.0.0'
        }
      })
    });

    change({
      findKey: (parsed) => parsed.resources.limits,
      merge: () => ({
        cpu: '500m',
        memory: '512Mi'
      }),
      comment: () => 'Increased for production workload'
    });
  }
});

YAML Anchors and Aliases

YAML anchors (&anchor-name) and aliases (*anchor-name) allow you to reuse content across your YAML document, reducing duplication and ensuring consistency.

Basic Anchor and Alias

const { result } = updateYaml({
  yamlString: '',
  annotate: ({ change }) => {
    change({
      findKey: (parsed: any) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'defaults',
          anchor: 'default-config'
        }),
        defaults: {
          timeout: 30,
          retries: 3
        },
        ...addInstructions({
          prop: 'production',
          alias: 'default-config'
        }),
        production: {}
      })
    });
  }
});

// Output:
// defaults: &default-config
//   timeout: 30
//   retries: 3
// production: *default-config

Merge Keys - Extend and Override

Use mergeAnchor to inherit properties from an anchor while adding or overriding specific values:

const { result } = updateYaml({
  yamlString: '',
  annotate: ({ change }) => {
    change({
      findKey: (parsed: any) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'defaults',
          anchor: 'base-config'
        }),
        defaults: {
          timeout: 30,
          retries: 3,
          logLevel: 'info'
        },
        ...addInstructions({
          prop: 'production',
          mergeAnchor: 'base-config'
        }),
        production: {
          logLevel: 'error',    // Override
          monitoring: true      // Add new
        }
      })
    });
  }
});

// Output:
// defaults: &base-config
//   timeout: 30
//   retries: 3
//   logLevel: info
// production:
//   <<: *base-config
//   logLevel: error
//   monitoring: true

Array Item Anchors and Aliases

Apply anchors and aliases to specific array items:

const { result } = updateYaml({
  yamlString: '',
  annotate: ({ change }) => {
    change({
      findKey: (parsed: any) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'environments',
          anchors: { 0: 'prod-env' },
          aliases: [undefined, 'prod-env', 'prod-env']
        }),
        environments: [
          { name: 'production', cpu: '1000m', memory: '2Gi' },
          { name: 'staging' },
          { name: 'qa' }
        ]
      })
    });
  }
});

// Output:
// environments:
//   - &prod-env
//     name: production
//     cpu: 1000m
//     memory: 2Gi
//   - *prod-env
//   - *prod-env

Renaming Anchors

Rename an anchor and automatically update all references throughout the document:

const yamlString = `
defaults: &old-name
  timeout: 30
api:
  <<: *old-name
worker:
  <<: *old-name
`;

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed: any) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'defaults',
          anchor: 'new-name',
          renameFrom: 'old-name'
        })
      })
    });
  }
});

// Output:
// defaults: &new-name
//   timeout: 30
// api:
//   <<: *new-name
// worker:
//   <<: *new-name

Schema-Level Instructions

For complex documents, define instructions at the schema level instead of inline. This separates formatting concerns (anchors, comments, flow style) from data structure.

Key Concept: The YAML updater schema defines how to format your YAML (anchors, comments, styling), while your OpenAPI schema defines what the data structure is (types, validation). These are complementary but separate concerns.

When to use schema-level instructions:

  • You have a consistent structure that needs the same formatting applied repeatedly
  • You're working with OpenAPI/JSON Schema and want to layer formatting on top
  • You want to reuse formatting rules across multiple updateYaml calls
  • You need to maintain separation between data structure (OpenAPI) and presentation (YAML formatting)

Basic Example:

const { result } = updateYaml({
  yamlString: '',
  schema: {
    properties: {
      server: {
        anchor: 'server-config',
        comment: 'Server configuration',
        properties: {
          host: { commentBefore: 'Production host' }
        }
      },
      database: {
        mergeAnchor: 'server-config',
        comment: 'Database inherits server config'
      },
      cache: {
        mergeAnchor: 'server-config'
      }
    }
  },
  annotate: ({ change }) => {
    change({
      findKey: (parsed: any) => parsed,
      merge: () => ({
        server: { host: 'localhost', port: 8080 },
        database: { dbName: 'myapp' },
        cache: { ttl: 3600 }
      })
    });
  }
});

// Output:
// # Server configuration
// server: &server-config
//   # Production host
//   host: localhost
//   port: 8080
// # Database inherits server config
// database:
//   <<: *server-config
//   dbName: myapp
// cache:
//   <<: *server-config
//   ttl: 3600

Schema for Array Items:

const { result } = updateYaml({
  yamlString: '',
  schema: {
    properties: {
      templates: {
        type: 'array',
        items: [
          { anchor: 'template-1', comment: 'First template' },
          { anchor: 'template-2' }
        ]
      },
      instances: {
        type: 'array',
        items: [
          { alias: 'template-1' },
          { alias: 'template-2' },
          { alias: 'template-1' }
        ]
      }
    }
  },
  annotate: ({ change }) => {
    change({
      findKey: (parsed: any) => parsed,
      merge: () => ({
        templates: [
          { name: 'basic', timeout: 30 },
          { name: 'advanced', timeout: 60 }
        ],
        instances: [{}, {}, {}]
      })
    });
  }
});

// Output:
// templates:
//   # First template
//   - &template-1
//     name: basic
//     timeout: 30
//   - &template-2
//     name: advanced
//     timeout: 60
// instances:
//   - *template-1
//   - *template-2
//   - *template-1

Priority: Inline addInstructions overrides schema:

const { result } = updateYaml({
  yamlString: '',
  schema: {
    properties: {
      server: {
        anchor: 'server-config',
        comment: 'From schema'
      }
    }
  },
  annotate: ({ change }) => {
    change({
      findKey: (parsed: any) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'server',
          anchor: 'override-anchor',
          comment: 'From addInstructions'  // This takes priority
        }),
        server: { host: 'localhost' }
      })
    });
  }
});

// Output:
// # From addInstructions
// server: &override-anchor
//   host: localhost

Real-World Example: Kubernetes Resource Sharing

const { result } = updateYaml({
  yamlString: '',
  schema: {
    properties: {
      resourceDefaults: {
        anchor: 'default-resources',
        comment: 'Default resource limits'
      },
      frontend: {
        properties: {
          resources: { mergeAnchor: 'default-resources' }
        }
      },
      backend: {
        properties: {
          resources: { mergeAnchor: 'default-resources' }
        }
      },
      worker: {
        properties: {
          resources: { mergeAnchor: 'default-resources' }
        }
      }
    }
  },
  annotate: ({ change }) => {
    change({
      findKey: (parsed: any) => parsed,
      merge: () => ({
        resourceDefaults: {
          limits: { cpu: '500m', memory: '512Mi' },
          requests: { cpu: '100m', memory: '128Mi' }
        },
        frontend: {
          replicas: 3,
          resources: { limits: { cpu: '1000m' } }  // Override CPU
        },
        backend: {
          replicas: 2,
          resources: {}  // Use defaults
        },
        worker: {
          replicas: 5,
          resources: { requests: { memory: '256Mi' } }  // Override memory
        }
      })
    });
  }
});

// Output:
// # Default resource limits
// resourceDefaults: &default-resources
//   limits:
//     cpu: 500m
//     memory: 512Mi
//   requests:
//     cpu: 100m
//     memory: 128Mi
// frontend:
//   replicas: 3
//   resources:
//     <<: *default-resources
//     limits:
//       cpu: 1000m
// backend:
//   replicas: 2
//   resources:
//     <<: *default-resources
// worker:
//   replicas: 5
//   resources:
//     <<: *default-resources
//     requests:
//       memory: 256Mi

Using with OpenAPI/JSON Schema

If you already have an OpenAPI specification or JSON Schema defining your data structure, you can create a complementary YAML updater schema to define formatting:

// Your OpenAPI spec defines the data structure
const openApiSpec = {
  components: {
    schemas: {
      ServerConfig: {
        type: 'object',
        properties: {
          host: { type: 'string' },
          port: { type: 'number' },
          timeout: { type: 'number' }
        }
      }
    }
  }
};

// YAML updater schema defines the formatting (separate concern)
const yamlFormattingSchema = {
  properties: {
    defaultServer: {
      anchor: 'server-config',
      comment: 'Default server configuration'
    },
    productionServer: {
      mergeAnchor: 'server-config',
      comment: 'Production overrides defaults'
    },
    stagingServer: {
      mergeAnchor: 'server-config',
      comment: 'Staging overrides defaults'
    }
  }
};

// Use both together
const { result } = updateYaml({
  yamlString,
  schema: yamlFormattingSchema,  // How to format
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        // Data validated against OpenAPI schema
        defaultServer: { host: 'localhost', port: 8080, timeout: 30 },
        productionServer: { host: 'prod.example.com', timeout: 60 },
        stagingServer: { host: 'staging.example.com' }
      })
    });
  }
});

// Output:
// # Default server configuration
// defaultServer: &server-config
//   host: localhost
//   port: 8080
//   timeout: 30
// # Production overrides defaults
// productionServer:
//   <<: *server-config
//   host: prod.example.com
//   timeout: 60
// # Staging overrides defaults
// stagingServer:
//   <<: *server-config
//   host: staging.example.com

Benefits of this approach:

  • Separation of concerns: OpenAPI defines structure/types, YAML schema defines formatting
  • Reusability: Define formatting schema once, reuse across multiple updates
  • Validation + Presentation: Combine OpenAPI validation with YAML formatting
  • Maintainability: Changes to data structure (OpenAPI) don't affect formatting rules

Anchor Instructions Reference

All anchor-related options in addInstructions:

addInstructions({
  prop: 'propertyName',

  // Create an anchor on this property
  anchor: 'anchor-name',

  // Rename existing anchor globally (use with anchor)
  renameFrom: 'old-anchor-name',

  // Create simple alias (replaces entire value)
  alias: 'anchor-name',

  // Merge with anchor (allows overrides)
  mergeAnchor: 'anchor-name',

  // Anchors for array items (by index)
  anchors: { 0: 'first-item', 2: 'third-item' },

  // Aliases for array items (by position)
  aliases: ['anchor-1', 'anchor-2', 'anchor-1']
})

Advanced Features

Multiple Changes

Apply multiple changes in a single update:

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.database,
      merge: () => ({ host: 'db.prod.com' })
    });

    change({
      findKey: (parsed) => parsed.cache,
      merge: () => ({ host: 'cache.prod.com' })
    });

    change({
      findKey: (parsed) => parsed.api,
      merge: () => ({ host: 'api.prod.com' })
    });
  }
});

Conditional Updates

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.spec,
      merge: (originalValue) => {
        const newReplicas = originalValue.replicas < 3
          ? originalValue.replicas * 2
          : originalValue.replicas;

        return { replicas: newReplicas };
      },
      comment: (prevComment) =>
        originalValue.replicas < 3
          ? `Scaled from ${originalValue.replicas} to ${newReplicas}`
          : prevComment  // Keep existing comment
    });
  }
});

Preserve and Extend

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.spec,
      merge: (originalValue) => ({
        ...originalValue,                    // Preserve all existing
        replicas: originalValue.replicas + 1, // Update one field
        newField: 'added'                    // Add new field
      })
    });
  }
});

Comment Preservation

Comments are automatically preserved:

const yamlString = `
# Application configuration
apiVersion: v1
kind: ConfigMap
metadata:
  # Metadata section
  name: my-config
data:
  # Database configuration
  database: localhost
  # Cache configuration
  cache: redis
`;

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.data,
      merge: () => ({ database: 'db.prod.com' })
    });
  }
});

// All comments are preserved!
// # Application configuration
// apiVersion: v1
// kind: ConfigMap
// metadata:
//   # Metadata section
//   name: my-config
// data:
//   # Database configuration
//   database: db.prod.com
//   # Cache configuration
//   cache: redis

Formatting Preservation

Indentation and spacing are maintained:

const yamlString = `
server:
  host: localhost
  port: 3000


database:
  host: localhost
  port: 5432
`;

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.server,
      merge: () => ({ port: 8080 })
    });
  }
});

// Blank lines and indentation preserved
// server:
//   host: localhost
//   port: 8080
//
//
// database:
//   host: localhost
//   port: 5432

YAML vs JSON Formatting

Control Output Format with flow Instruction

The flow instruction allows you to control whether properties are formatted as JSON (flow style) or YAML (block style):

import { updateYaml, addInstructions } from '@hiscojs/yaml-updater';

const yamlString = `
config:
  name: myapp
`;

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.config,
      merge: () => ({
        name: 'myapp',
        // YAML block style (default)
        ...addInstructions({
          prop: 'settings',
          flow: false
        }),
        settings: {
          debug: true,
          timeout: 30
        },
        // JSON flow style
        ...addInstructions({
          prop: 'metadata',
          flow: true
        }),
        metadata: { version: '1.0', env: 'prod' },
        // JSON flow style for arrays
        ...addInstructions({
          prop: 'tags',
          flow: true
        }),
        tags: ['tag1', 'tag2', 'tag3']
      })
    });
  }
});

// Output:
// config:
//   name: myapp
//   settings:           # YAML block style (multi-line)
//     debug: true
//     timeout: 30
//   metadata: { version: "1.0", env: prod }  # JSON flow style (single-line)
//   tags: [ tag1, tag2, tag3 ]               # JSON flow style (single-line)

Control Individual Array Items with flowItems

For fine-grained control, use flowItems to specify the format of each array item individually:

const yamlString = `
items: []
`;

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'items',
          flowItems: [true, false, true]  // First: JSON, Second: YAML, Third: JSON
        }),
        items: [
          { id: 1, name: 'first' },
          { id: 2, name: 'second' },
          { id: 3, name: 'third' }
        ]
      })
    });
  }
});

// Output:
// items:
//   - { id: 1, name: first }   # JSON format
//   - id: 2                     # YAML format
//     name: second
//   - { id: 3, name: third }    # JSON format

Mixed JSON and YAML Example

const { result } = updateYaml({
  yamlString: 'jsons: []',
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'jsons',
          flowItems: [true, false]  // First: JSON, Second: YAML
        }),
        jsons: [
          {},           // Empty object in JSON
          { key: 1 }    // Object with data in YAML
        ]
      })
    });
  }
});

// Output:
// jsons:
//   - {}          # JSON format (compact)
//   - key: 1      # YAML format (expanded)

When to Use Flow vs Block Style

Use Flow Style (JSON) when:

  • Properties have simple, short values
  • You want compact, single-line formatting
  • The property contains metadata or configuration that benefits from being compact
  • Empty objects or simple arrays

Use Block Style (YAML) when:

  • Properties have complex nested structures
  • You want readable, multi-line formatting
  • The property contains important configuration that should be easy to read
  • Large objects or arrays with many items

Combining flow with Comments

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.config,
      merge: () => ({
        ...addInstructions({
          prop: 'metadata',
          comment: 'Compact JSON metadata',
          flow: true
        }),
        metadata: { version: '1.0', env: 'prod' },
        ...addInstructions({
          prop: 'settings',
          comment: 'Detailed YAML settings',
          flow: false
        }),
        settings: {
          debug: true,
          timeout: 30
        }
      })
    });
  }
});

// Output:
// config:
//   # Compact JSON metadata
//   metadata: { version: "1.0", env: prod }
//   # Detailed YAML settings
//   settings:
//     debug: true
//     timeout: 30

Writing to Empty Files

The library handles empty YAML strings gracefully, treating them as empty objects:

const { result } = updateYaml({
  yamlString: '',  // Empty string
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        apiVersion: 'v1',
        kind: 'ConfigMap',
        data: {
          key: 'value'
        }
      })
    });
  }
});

// Output:
// apiVersion: v1
// kind: ConfigMap
// data:
//   key: value

Comment Placement

Comments are always placed above the property they describe:

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'data',
          comment: 'Configuration data'
        }),
        data: {
          database: 'localhost',
          cache: 'redis'
        }
      })
    });
  }
});

// Output:
// # Configuration data
// data:
//   database: localhost
//   cache: redis

Best Practices

1. Use Type Parameters

// ✅ Good - Type safe
const { result } = updateYaml<K8sDeployment>({ ... });

// ❌ Avoid - No type safety
const { result } = updateYaml({ ... });

2. Leverage originalValue

// ✅ Good - Conditional based on original
merge: (originalValue) => ({
  replicas: originalValue.replicas + 1
})

// ❌ Avoid - Hardcoded without context
merge: () => ({ replicas: 3 })

3. Use Merge Strategies for Arrays

// ✅ Good - Explicit merge strategy
...addInstructions({
  prop: 'containers',
  mergeByName: true
})

// ❌ Avoid - Array replacement
containers: newContainers  // Loses existing items

4. Add Meaningful Comments

// ✅ Good - Descriptive comment
comment: () => 'Scaled to 3 replicas for high availability'

// ❌ Avoid - Obvious or missing comments
comment: () => 'Updated replicas'

5. Handle Multi-Document YAMLs

// ✅ Good - Explicit document selection
selectDocument: (docs) => {
  return docs.findIndex(doc =>
    doc.toJSON().kind === 'Deployment'
  );
}

// ❌ Avoid - Assuming single document
// (works but fails silently with multi-doc)

Error Handling

The library will throw descriptive errors for invalid YAML:

try {
  const { result } = updateYaml({
    yamlString: invalidYaml,
    annotate: ({ change }) => { ... }
  });
} catch (error) {
  console.error('YAML parsing failed:', error.message);
}

Performance Considerations

  • Large Files: YAML parsing is memory-intensive. Consider streaming for very large files (>10MB).
  • Many Changes: Each change() call creates proxies and performs deep cloning. Batch related changes when possible.
  • Comment Operations: Adding/removing comments requires AST manipulation. Minimal performance impact for normal use.

Dependencies

  • yaml: YAML 1.2 parser and stringifier
  • deep-diff: Deep object diffing
  • @hiscojs/object-updater: Core object manipulation with type-safe updates

Property Ordering

How Key Order Works

The YAML updater determines property order based on merge object property order, not schema order.

For new files (empty YAML): Keys appear in the exact order they are defined in your merge object:

const { result } = updateYaml({
  yamlString: '',
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        apiVersion: 'v1',           // 1st
        kind: 'ConfigMap',          // 2nd
        metadata: { name: 'test' }, // 3rd
        data: { key: 'value' }      // 4th
      })
    });
  }
});

// Output follows merge object order:
// apiVersion: v1
// kind: ConfigMap
// metadata:
//   name: test
// data:
//   key: value

For existing files:

  • Existing keys maintain their original order (preserved from source YAML)
  • New keys are appended in the order they appear in the merge object
const yamlString = `kind: ConfigMap
apiVersion: v1`;

const { result } = updateYaml({
  yamlString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        metadata: { name: 'test' },  // New
        data: { key: 'value' }       // New
      })
    });
  }
});

// Output preserves original order and appends new keys:
// kind: ConfigMap        (existing, order preserved)
// apiVersion: v1         (existing, order preserved)
// metadata:              (new, added in merge order)
//   name: test
// data:                  (new, added in merge order)
//   key: value

Schema Does NOT Control Ordering

The schema parameter is used for metadata (comments, formatting, anchors), not for ordering:

const { result } = updateYaml({
  yamlString: '',
  schema: {
    properties: {
      apiVersion: { comment: 'API version' },
      kind: { comment: 'Resource type' }
    }
  },
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        // Order comes from HERE, not from schema
        kind: 'ConfigMap',
        apiVersion: 'v1'
      })
    });
  }
});

// Output follows merge object order with schema comments:
// # Resource type
// kind: ConfigMap
// # API version
// apiVersion: v1

Best Practices for Ordering

✅ Control order explicitly via merge object:

// Good: Explicit, predictable order
merge: () => ({
  apiVersion: 'v1',
  kind: 'ConfigMap',
  metadata: { name: 'test' },
  data: { key: 'value' }
})

✅ Use helper functions for consistent ordering:

function createK8sResource(kind: string, name: string, data: any) {
  return {
    apiVersion: 'v1',
    kind,
    metadata: { name },
    data
  };
}

merge: () => createK8sResource('ConfigMap', 'my-config', { key: 'value' })

✅ Nested properties also follow merge object order:

merge: () => ({
  metadata: {
    name: 'test',           // 1st in metadata
    namespace: 'default',   // 2nd in metadata
    labels: { app: 'myapp' } // 3rd in metadata
  }
})

// Output:
// metadata:
//   name: test
//   namespace: default
//   labels:
//     app: myapp

Related Packages

License

MIT

Contributing

Issues and pull requests welcome!

Repository

https://github.com/hisco/yaml-updater