@hiscojs/jsonnet-updater
v1.4.0
Published
Type-safe, immutable Jsonnet updates with local variable management, function definitions, comment preservation, and advanced array merging strategies
Downloads
620
Maintainers
Readme
@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
localdeclarations 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-updaterQuick 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 objectkeys: 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
Programmatic Generation
For cases where you need to generate Jsonnet from scratch rather than update existing files, the library provides two approaches:
1. JavaScript-Native API (Recommended ⭐)
Write plain JavaScript objects and use helpers only when needed - similar to yaml-updater's addInstructions.
import {
objectToJsonnet,
identifier,
extVar,
concat,
addInstructions,
} from '@hiscojs/jsonnet-updater';
const template = objectToJsonnet(
{
apiVersion: 'argoproj.io/v1alpha1',
kind: 'AppProject',
metadata: {
name: identifier('projectName'), // Variable reference
namespace: 'argocd',
},
spec: {
description: concat('Project: ', identifier('projectName')), // String concatenation
sourceRepos: ['*'],
// Explicit array formatting control (like yaml-updater)
...addInstructions({ prop: 'destinations', multiline: true }),
destinations: [
{
server: identifier('clusterAddress'),
namespace: '*',
},
],
},
},
{
locals: [
{ name: 'projectName', value: extVar('projectName') },
{ name: 'clusterAddress', value: extVar('clusterAddress') },
],
}
);
// Output:
// {
// local projectName = std.extVar("projectName");
// local clusterAddress = std.extVar("clusterAddress");
//
// apiVersion: "argoproj.io/v1alpha1",
// kind: "AppProject",
// metadata: {
// name: projectName,
// namespace: "argocd",
// },
// spec: {
// description: "Project: " + projectName,
// sourceRepos: ["*"],
// destinations: [
// {
// server: clusterAddress,
// namespace: "*",
// },
// ],
// },
// }Helpers:
identifier(name)- Variable reference:identifier('var')→varextVar(name)- External variable:extVar('name')→std.extVar("name")concat(left, right)- String concatenation:concat('a', 'b')→"a" + "b"addInstructions({ prop, multiline })- Array formatting control (like yaml-updater)
See OBJECT_API.md for complete documentation.
2. AST Builders (Low-Level)
For cases where you need to generate Jsonnet from scratch rather than update existing files, the library provides AST builder helpers.
Quick Example
import { generate, Document } from '@hiscojs/jsonnet-updater';
import * as b from '@hiscojs/jsonnet-updater/builders';
// Build the AST
const ast: Document = {
type: 'Document',
body: b.object(
[
b.field('apiVersion', b.string('v1')),
b.field('kind', b.string('ConfigMap')),
b.field('metadata', b.object([
b.field('name', b.identifier('appName')),
b.field('namespace', b.string('default')),
])),
b.field('data', b.object([
b.field('config.json', b.string('{"key": "value"}')),
])),
],
// Local variables (second parameter)
[
b.local('appName', b.extVar('appName')),
]
),
};
// Generate Jsonnet code
const result = generate(ast, {
indent: ' ',
trailingCommas: true,
});
console.log(result);
// Output:
// {
// local appName = std.extVar("appName");
//
// apiVersion: "v1",
// kind: "ConfigMap",
// metadata: {
// name: appName,
// namespace: "default",
// },
// data: {
// "config.json": "{\"key\": \"value\"}",
// },
// }Builder API
| Builder | Output | Description |
|---------|--------|-------------|
| b.string('text') | "text" | String literal |
| b.number(42) | 42 | Number literal |
| b.boolean(true) | true | Boolean literal |
| b.identifier('name') | name | Variable reference |
| b.extVar('var') | std.extVar("var") | External variable |
| b.object([...]) | { ... } | Object |
| b.array([...]) | [...] | Array |
| b.field('key', value) | key: value | Object field |
| b.local('name', value) | local name = value | Local variable |
| b.binary('+', a, b) | a + b | Binary expression |
| b.call(func, [args]) | func(args) | Function call |
| b.member(obj, 'prop') | obj.prop | Member access |
Array Formatting Control
Control whether arrays are formatted inline or multi-line:
// Auto-detect (default): inline for simple values, multi-line for objects
b.array([b.string('a'), b.string('b')]) // ["a", "b"]
b.array([b.object([...])]) // [{\n ...\n}]
// Force multi-line (explicit control)
b.array(
[b.string('dev'), b.string('staging'), b.string('prod')],
{ multiline: true }
)
// Output:
// [
// "dev",
// "staging",
// "prod",
// ]
// Force inline
b.array([...], { multiline: false }) // [...]Array Formatting Options:
multiline: true- Force multi-line formatmultiline: false- Force inline formatundefined- Auto-detect based on element types (default)
Complete Example: ArgoCD AppProject
See appproject-template.jsonnet for a complete example of generating a complex Jsonnet template programmatically.
import { generate, Document } from '@hiscojs/jsonnet-updater';
import * as b from '@hiscojs/jsonnet-updater/builders';
const ast: Document = {
type: 'Document',
body: b.object(
[
b.field('apiVersion', b.string('argoproj.io/v1alpha1')),
b.field('kind', b.string('AppProject')),
b.field('metadata', b.object([
b.field('name', b.identifier('projectName')),
b.field('namespace', b.string('argocd')),
])),
b.field('spec', b.object([
b.field(
'description',
b.binary('+', b.string('Project: '), b.identifier('projectName'))
),
b.field('sourceRepos', b.array([b.string('*')])),
b.field(
'destinations',
b.array(
[
b.object([
b.field('server', b.identifier('clusterAddress')),
b.field('namespace', b.string('*')),
]),
],
{ multiline: true } // Explicit multi-line formatting
)
),
])),
],
[
b.local('projectName', b.extVar('projectName')),
b.local('clusterAddress', b.extVar('clusterAddress')),
]
),
};
const result = generate(ast, { indent: ' ', trailingCommas: true });When to Use Programmatic Generation
✅ Use AST builders when:
- Creating Jsonnet files from scratch
- Building templates dynamically from data
- Need type-safe construction with validation
- Generating consistent structures programmatically
✅ Use updateJsonnet() when:
- Updating existing Jsonnet files
- Preserving comments and formatting
- Making incremental changes to existing content
Related Libraries
- @hiscojs/yaml-updater - YAML updates with comment preservation
- @hiscojs/json-updater - JSON/JSONC updates with formatting
- @hiscojs/dotenv-updater - .env file updates
- @hiscojs/object-updater - Core update logic engine
