Skip to content

hisco/jsonnet-updater

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@hiscojs/jsonnet-updater

Type-safe, immutable Jsonnet updates with local variable management, function definitions, comment preservation, and advanced array merging strategies.

Features

  • 🔒 Type-safe updates using TypeScript proxies for automatic path detection
  • 🎯 Immutable operations - Original content never modified
  • 📝 Comment preservation - Keep your documentation intact
  • 📄 Document headers - Extract and preserve file headers with multiple formatting styles
  • 🔧 Local variables - Manage local declarations easily (similar to YAML anchors)
  • Function definitions - Create and manage reusable Jsonnet functions
  • 🔄 Advanced array merging - Multiple strategies (by name, by property, by content)
  • 📐 Formatting preservation - Maintains indentation and style
  • 🎨 Clean API - Intuitive, developer-friendly interface

Installation

npm install @hiscojs/jsonnet-updater

Quick Start

Basic Value Update

import { updateJsonnet } from '@hiscojs/jsonnet-updater';

const jsonnetString = `
{
  environment: 'dev',
  replicas: 3,
  image: 'myapp:1.0.0'
}
`;

const { result } = updateJsonnet({
  jsonnetString,
  annotate: ({ change }) => {
    change({
      findKey: (obj) => obj,
      merge: (orig) => ({
        ...orig,
        replicas: 5,
        image: 'myapp:2.0.0'
      })
    });
  }
});

console.log(result);
// Output:
// {
//   environment: 'dev',
//   replicas: 5,
//   image: 'myapp:2.0.0'
// }

Core Concepts

The annotate Pattern

The library uses a change function within annotate to specify updates. This provides type-safety and immutable updates:

annotate: ({ change }) => {
  change({
    findKey: (obj) => obj.parentObject,       // Find the parent object to update
    merge: (original) => ({                    // Return updated parent object
      ...original,                             // Spread original properties
      propertyToUpdate: newValue               // Override specific properties
    })
  });
}

Important: Always find the parent object containing the property you want to update, then return the complete updated parent object using the spread operator.

Return Type

All operations return a JsonnetEdit<T> object:

interface JsonnetEdit<T> {
  result: string;           // Updated Jsonnet string
  resultParsed: T;          // Parsed object (evaluated Jsonnet)
  originalParsed: T;        // Original parsed object
  locals: LocalVariable[];  // Defined local variables
  functions: LocalFunction[]; // Defined local functions
}

Working with Local Variables

Local variables in Jsonnet are like YAML anchors - they allow you to define reusable values.

Adding Local Variables

import { updateJsonnet } from '@hiscojs/jsonnet-updater';

const jsonnetString = `
{
  name: 'myapp',
  version: '1.0.0'
}
`;

const { result } = updateJsonnet({
  jsonnetString,
  locals: [
    {
      name: 'namespace',
      value: 'production'
    },
    {
      name: 'imageTag',
      value: 'v2.0.0'
    }
  ],
  annotate: ({ change }) => {
    change({
      findKey: (obj) => obj.namespace,
      merge: () => '$.namespace'  // Reference the local variable
    });
  }
});

console.log(result);
// Output:
// local namespace = 'production';
// local imageTag = 'v2.0.0';
// {
//   name: 'myapp',
//   version: '1.0.0',
//   namespace: $.namespace
// }

Using Existing Local Variables

const jsonnetString = `
local environment = 'dev';
local replicas = 3;

{
  env: environment,
  count: replicas
}
`;

const { result } = updateJsonnet({
  jsonnetString,
  locals: [
    { name: 'replicas', value: 5 }  // Override the local variable
  ]
});

console.log(result);
// Output:
// local environment = 'dev';
// local replicas = 5;  // <-- Updated
//
// {
//   env: environment,
//   count: replicas
// }

Working with Local Functions

Local functions provide reusable logic, similar to how you might use YAML anchors for complex structures.

Defining Local Functions

import { updateJsonnet } from '@hiscojs/jsonnet-updater';

const jsonnetString = `
{
  services: []
}
`;

const { result } = updateJsonnet({
  jsonnetString,
  functions: [
    {
      name: 'createService',
      params: ['name', 'port'],
      body: `{
  name: name,
  port: port,
  protocol: 'TCP'
}`
    }
  ],
  annotate: ({ change, functions }) => {
    change({
      findKey: (obj) => obj.services,
      merge: () => [
        '$.createService("api", 8080)',
        '$.createService("web", 3000)'
      ]
    });
  }
});

console.log(result);
// Output:
// local createService(name, port) = {
//   name: name,
//   port: port,
//   protocol: 'TCP'
// };
//
// {
//   services: [
//     $.createService("api", 8080),
//     $.createService("web", 3000)
//   ]
// }

Complex Function Example

const { result } = updateJsonnet({
  jsonnetString: '{}',
  functions: [
    {
      name: 'createDeployment',
      params: ['name', 'image', 'replicas'],
      body: `{
  apiVersion: 'apps/v1',
  kind: 'Deployment',
  metadata: {
    name: name
  },
  spec: {
    replicas: replicas,
    template: {
      spec: {
        containers: [{
          name: name,
          image: image
        }]
      }
    }
  }
}`
    }
  ],
  annotate: ({ change }) => {
    change({
      findKey: (obj) => obj.deployment,
      merge: () => '$.createDeployment("myapp", "myapp:2.0", 3)'
    });
  }
});

Advanced Array Merging

Use addInstructions for sophisticated array handling:

import { updateJsonnet, addInstructions } from '@hiscojs/jsonnet-updater';

const jsonnetString = `
{
  services: [
    { name: 'api', port: 8080 },
    { name: 'web', port: 3000 }
  ]
}
`;

const { result } = updateJsonnet({
  jsonnetString,
  annotate: ({ change }) => {
    change({
      findKey: (obj) => obj.services,
      merge: (current) => [
        ...current,
        ...addInstructions({
          prop: 'services',
          mergeByName: true  // Merge by 'name' property
        }),
        { name: 'api', port: 8081, replicas: 3 },  // Updates existing
        { name: 'cache', port: 6379 }              // Adds new
      ]
    });
  }
});

console.log(result);
// Output:
// {
//   services: [
//     { name: 'api', port: 8081, replicas: 3 },  // Merged by name
//     { name: 'web', port: 3000 },
//     { name: 'cache', port: 6379 }              // Added
//   ]
// }

Merge Strategies

...addInstructions({
  prop: 'arrayName',
  mergeByName: true,        // Merge by 'name' property
  // OR
  mergeByProp: 'id',        // Merge by specific property
  // OR
  mergeByContents: true,    // Merge by full content comparison

  deepMerge: true           // Deep merge objects (default: false)
})

Property Deletion

Delete properties using the exclude helper function:

import { updateJsonnet, exclude } from '@hiscojs/jsonnet-updater';

const jsonnetString = `
{
  name: 'myapp',
  version: '1.0.0',
  deprecated: true,
  legacy: 'old-value'
}
`;

const { result } = updateJsonnet({
  jsonnetString,
  annotate: ({ change }) => {
    change({
      findKey: (obj) => obj,
      merge: (orig) => exclude(orig, 'deprecated', 'legacy')
    });
  }
});

console.log(result);
// Output:
// {
//   name: 'myapp',
//   version: '1.0.0'
// }

Deleting Properties with Updates

Combine exclude with the spread operator for partial updates:

const { result } = updateJsonnet({
  jsonnetString,
  annotate: ({ change }) => {
    change({
      findKey: (obj) => obj,
      merge: (orig) => ({
        ...exclude(orig, 'deprecated'),
        version: '2.0.0',           // Update existing
        environment: 'production'    // Add new
      })
    });
  }
});

Deleting Nested Properties

const { result } = updateJsonnet({
  jsonnetString: `{
  server: {
    host: 'localhost',
    port: 8080,
    oldTimeout: 30
  }
}`,
  annotate: ({ change }) => {
    change({
      findKey: (obj) => obj.server,
      merge: (orig) => ({
        ...exclude(orig, 'oldTimeout'),
        timeout: 60  // Replace with new property
      })
    });
  }
});

Note: For deleting array elements, use standard JavaScript array methods like filter():

change({
  findKey: (obj) => obj.items,
  merge: (orig) => orig.filter(item => item.active)
});

Nested Object Updates

const jsonnetString = `
{
  server: {
    host: 'localhost',
    port: 8080,
    ssl: {
      enabled: false
    }
  }
}
`;

const { result } = updateJsonnet({
  jsonnetString,
  annotate: ({ change }) => {
    change({
      findKey: (obj) => obj.server.ssl,
      merge: (orig) => ({
        ...orig,
        enabled: true,
        cert: '/path/to/cert.pem'
      })
    });
  }
});

console.log(result);
// Output:
// {
//   server: {
//     host: 'localhost',
//     port: 8080,
//     ssl: {
//       enabled: true,
//       cert: '/path/to/cert.pem'
//     }
//   }
// }

Document Headers

Preserve and manage document headers (comments at the top of the file):

Simple Headers

const jsonnetString = `# Application Configuration
# Version: 1.0
# Author: DevOps Team
{
  name: 'myapp',
  version: '1.0.0'
}`;

const { result, extractedHeader } = updateJsonnet({
  jsonnetString,
  documentHeader: {
    type: 'simple',
    content: ['Application Configuration', 'Version: 1.0', 'Author: DevOps Team']
  },
  annotate: ({ change }) => {
    change({
      findKey: (obj) => obj,
      merge: (orig) => ({
        ...orig,
        version: '2.0.0'
      })
    });
  }
});

// Output:
// # Application Configuration
// # Version: 1.0
// # Author: DevOps Team
// {
//   name: 'myapp',
//   version: '2.0.0'
// }

Multi-line Bordered Headers

const { result } = updateJsonnet({
  jsonnetString: '{ service: "api" }',
  documentHeader: {
    type: 'multi-line',
    content: ['Service Configuration', 'Owner: Platform Team'],
    border: '#',
    width: 50
  }
});

// Output:
// ##################################################
// # Service Configuration
// # Owner: Platform Team
// ##################################################
// {
//   service: 'api'
// }

Raw Headers

const { result } = updateJsonnet({
  jsonnetString: '{ generated: true }',
  documentHeader: {
    type: 'raw',
    content: '// Custom header format\n// DO NOT EDIT - Generated file'
  }
});

// Output:
// // Custom header format
// // DO NOT EDIT - Generated file
// {
//   generated: true
// }

Header Extraction

When a documentHeader is provided, the library automatically extracts existing headers:

const { extractedHeader } = updateJsonnet({
  jsonnetString: withHeader,
  documentHeader: { type: 'simple', content: [] }
});

console.log(extractedHeader);
// {
//   type: 'simple',
//   content: ['Application Configuration', 'Version: 1.0'],
//   raw: '# Application Configuration\n# Version: 1.0'
// }

Format Options

Control how the output is formatted:

const { result } = updateJsonnet({
  jsonnetString,
  formatOptions: {
    indent: 2,                    // Number of spaces (or '\t')
    preserveIndentation: true,    // Auto-detect from source (default)
    trailingNewline: true         // Add newline at end (default)
  },
  annotate: ({ change }) => {
    // Your updates...
  }
});

Real-World Examples

Kubernetes Manifest Generation

import { updateJsonnet } from '@hiscojs/jsonnet-updater';

const kubernetesTemplate = `
{
  apiVersion: 'v1',
  kind: 'ConfigMap',
  metadata: {
    name: 'app-config'
  },
  data: {}
}
`;

const { result } = updateJsonnet({
  jsonnetString: kubernetesTemplate,
  locals: [
    { name: 'environment', value: 'production' },
    { name: 'region', value: 'us-west-2' }
  ],
  annotate: ({ change }) => {
    change({
      findKey: (obj) => obj.metadata,
      merge: (orig) => ({
        ...orig,
        namespace: '$.environment'
      })
    });

    change({
      findKey: (obj) => obj,
      merge: (orig) => ({
        ...orig,
        data: {
          DATABASE_URL: 'postgres://prod-db:5432',
          REGION: '$.region',
          LOG_LEVEL: 'info'
        }
      })
    });
  }
});

Multi-Service Configuration

const { result } = updateJsonnet({
  jsonnetString: '{}',
  functions: [
    {
      name: 'createService',
      params: ['name', 'image', 'port', 'env'],
      body: `{
  name: name,
  image: image,
  ports: [{ containerPort: port }],
  env: env
}`
    }
  ],
  locals: [
    { name: 'dbHost', value: 'postgres.example.com' },
    { name: 'cacheHost', value: 'redis.example.com' }
  ],
  annotate: ({ change }) => {
    change({
      findKey: (obj) => obj.services,
      merge: () => [
        `$.createService(
          "api",
          "api:2.0",
          8080,
          [{ name: "DB_HOST", value: $.dbHost }]
        )`,
        `$.createService(
          "worker",
          "worker:1.5",
          8081,
          [
            { name: "DB_HOST", value: $.dbHost },
            { name: "CACHE_HOST", value: $.cacheHost }
          ]
        )`
      ]
    });
  }
});

API Reference

updateJsonnet<T>(options): JsonnetEdit<T>

Main function for updating Jsonnet content.

Options:

{
  jsonnetString: string;              // Input Jsonnet content
  annotate?: (ctx: AnnotateContext) => void;  // Update callback
  formatOptions?: FormatOptions;      // Formatting preferences
  locals?: LocalVariable[];           // Local variable definitions
  functions?: LocalFunction[];        // Local function definitions
  documentHeader?: DocumentHeader;    // Document header configuration
}

Return Type:

interface JsonnetEdit<T> {
  result: string;                     // Updated Jsonnet string
  resultParsed: T;                    // Parsed updated object
  originalParsed: T;                  // Original parsed object
  locals: LocalVariable[];            // Defined local variables
  functions: LocalFunction[];         // Defined local functions
  extractedHeader?: ExtractedHeader;  // Extracted document header (if present)
}

AnnotateContext:

{
  change: (instruction: ChangeInstruction) => void;
  locals: Record<string, any>;        // Access to local variables
  functions: Record<string, Function>; // Access to local functions
}

ChangeInstruction:

{
  findKey: (proxy: T) => any;         // Path selector with type-safety
  merge: (current: any) => any;       // Update function
}

DocumentHeader:

{
  type: 'simple' | 'multi-line' | 'raw';  // Header formatting style
  content: string | string[];              // Header content
  border?: string;                         // Border char for multi-line (default: '#')
  width?: number;                          // Width for multi-line (default: auto)
}

ExtractedHeader:

{
  type: 'simple' | 'multi-line' | 'raw';  // Detected header type
  content: string[];                       // Parsed content lines
  raw: string;                             // Raw header string
}

addInstructions(options): Instruction[]

Generate instructions for advanced array merging (re-exported from @hiscojs/object-updater).

Options:

{
  prop: string;              // Array property name
  mergeByName?: boolean;     // Merge by 'name' property
  mergeByProp?: string;      // Merge by specific property
  mergeByContents?: boolean; // Merge by content comparison
  deepMerge?: boolean;       // Deep merge objects
}

exclude<T>(obj: T, ...keys: (keyof T)[]): Partial<T>

Helper function for deleting properties from objects (re-exported from @hiscojs/object-updater).

Parameters:

  • obj: The source object
  • keys: Property names to exclude/delete (variable number of arguments)

Returns: A new object with specified properties set to undefined, signaling deletion

Example:

import { updateJsonnet, exclude } from '@hiscojs/jsonnet-updater';

// Delete single property
merge: (orig) => exclude(orig, 'deprecated')

// Delete multiple properties
merge: (orig) => exclude(orig, 'deprecated', 'legacy', 'old')

// Combine with spread for updates
merge: (orig) => ({
  ...exclude(orig, 'deprecated'),
  version: '2.0.0',
  newProp: 'value'
})

TypeScript Support

Full TypeScript support with generic types:

interface MyConfig {
  server: {
    host: string;
    port: number;
  };
  features: string[];
}

const { result, resultParsed } = updateJsonnet<MyConfig>({
  jsonnetString,
  annotate: ({ change }) => {
    change({
      findKey: (obj) => obj.server.port,  // Type-safe!
      merge: () => 9000
    });
  }
});

// resultParsed is typed as MyConfig
console.log(resultParsed.server.host);

Known Limitations

While jsonnet-updater is powerful for many use cases, it has some limitations:

Parsing Limitations

  • Cannot parse Jsonnet files with function calls in data values (e.g., person1: Person())
  • Complex Jsonnet expressions may not parse correctly
  • Best used with static JSON-like Jsonnet structures

Function Evaluation

  • Functions are not evaluated (treated as templates)
  • Function calls are represented as string references ($.functionName())

Comment Preservation

  • Comments are tracked but not fully re-inserted
  • Basic comment preservation implementation

Recommended Use Cases

✅ Best For

  • Creating Jsonnet files from scratch with functions and local variables
  • Updating static JSON-like Jsonnet structures with predictable schemas
  • Managing local variables and functions as reusable templates
  • Kubernetes manifest generation with templating
  • Configuration file templating for multi-environment setups

⚠️ Not Ideal For

  • Complex Jsonnet with heavy use of function calls in data
  • Files with computed values and conditionals that require evaluation
  • Interactive Jsonnet evaluation or runtime value computation

Comparison with Other Updaters

Feature jsonnet-updater yaml-updater json-updater
Type-safe updates
Comment preservation
Local variables ✅ (native) ✅ (anchors)
Functions ✅ (native)
Array merging
Multi-document
Format detection

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT

Related Libraries

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published