npm package: @hiscojs/json-updater
Description: Type-safe, immutable JSON/JSONC updates with automatic formatting detection, comment preservation, and advanced array merging strategies
Latest version: 1.1.0

# @hiscojs/json-updater

Type-safe, immutable JSON updates with automatic formatting detection and advanced array merging strategies.

## Installation

```bash
npm install @hiscojs/json-updater
```

## Quick Start

```typescript
import { updateJson } from '@hiscojs/json-updater';

const jsonString = `{
  "server": {
    "host": "localhost",
    "port": 3000
  }
}`;

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

console.log(result);
// {
//   "server": {
//     "host": "localhost",
//     "port": 8080
//   }
// }
```

## Features

- **Type-Safe**: Full TypeScript support with generic type parameters
- **Immutable**: Original JSON strings are never modified
- **JSONC Support**: Parse and update JSON with Comments (tsconfig.json, settings.json, etc.)
- **Automatic Formatting Detection**: Preserves indentation (spaces/tabs) and trailing newlines
- **Custom Formatting**: Override with custom indentation if needed
- **Advanced Array Merging**: Multiple strategies for merging arrays
- **Proxy-Based Path Tracking**: Automatic path detection
- **Works with all JSON files**: package.json, tsconfig.json, configuration files, etc.

## API Reference

### `updateJson<T>(options)`

Updates a JSON string immutably with type safety and formatting preservation.

#### Parameters

```typescript
interface UpdateJsonOptions<T> {
  jsonString: string;
  annotate?: (annotator: {
    change: <L>(options: ChangeOptions<T, L>) => void;
  }) => void;
  formatOptions?: JsonFormatOptions;
}

interface JsonFormatOptions {
  indent?: number | string;        // Number of spaces or '\t' for tabs
  preserveIndentation?: boolean;   // Auto-detect from original (default: true)
  trailingNewline?: boolean;       // Add \n at end (default: auto-detect)
  allowComments?: boolean;         // Parse JSONC (JSON with Comments) (default: false)
}
```

#### Returns

```typescript
interface JsonEdit<T> {
  result: string;           // Updated JSON string
  resultParsed: T;          // Parsed updated object
  originalParsed: T;        // Original parsed object
}
```

### `change<L>(options)`

Defines a single change operation.

```typescript
interface ChangeOptions<T, L> {
  findKey: (parsed: T) => L;
  merge: (originalValue: L) => Partial<L>;
}
```

## Basic Usage

### Simple Property Update

```typescript
const jsonString = `{
  "name": "my-app",
  "version": "1.0.0"
}`;

const { result } = updateJson({
  jsonString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({ version: '1.0.1' })
    });
  }
});

// {
//   "name": "my-app",
//   "version": "1.0.1"
// }
```

### Type-Safe Updates

```typescript
interface PackageJson {
  name: string;
  version: string;
  dependencies: Record<string, string>;
  devDependencies?: Record<string, string>;
}

const { result, resultParsed } = updateJson<PackageJson>({
  jsonString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.dependencies,  // Fully typed!
      merge: (deps) => ({
        ...deps,
        'new-package': '^1.0.0'
      })
    });
  }
});

console.log(resultParsed.dependencies);  // Type-safe access!
```

## Formatting Options

### Automatic Detection (Default)

By default, formatting is automatically detected and preserved:

```typescript
// 2-space indentation
const jsonString = `{
  "key": "value"
}`;

// 4-space indentation
const jsonString = `{
    "key": "value"
}`;

// Tab indentation
const jsonString = `{
\t"key": "value"
}`;

// All preserved automatically!
const { result } = updateJson({ jsonString, ... });
```

### Custom Formatting

Override automatic detection with custom options:

```typescript
const { result } = updateJson({
  jsonString,
  formatOptions: {
    indent: 4,                    // Use 4 spaces
    trailingNewline: true         // Add newline at end
  },
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({ key: 'value' })
    });
  }
});
```

### Tab Indentation

```typescript
const { result } = updateJson({
  jsonString,
  formatOptions: {
    indent: '\t'  // Use tabs
  },
  annotate: ({ change }) => { ... }
});
```

## JSONC Support (JSON with Comments)

Enable JSONC support to work with configuration files that include comments like `tsconfig.json`, VSCode `settings.json`, etc.

### Basic JSONC Usage

```typescript
const jsoncString = `{
  // TypeScript configuration
  "compilerOptions": {
    "target": "ES2018", // Target version
    "module": "commonjs",
    "strict": true
  }
}`;

const { result } = updateJson({
  jsonString: jsoncString,
  formatOptions: {
    allowComments: true  // Enable JSONC support
  },
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.compilerOptions,
      merge: (original) => ({
        ...original,
        target: 'ES2020'
      })
    });
  }
});

// Comments are preserved in the output!
```

### Update tsconfig.json

```typescript
import fs from 'fs';
import { updateJson } from '@hiscojs/json-updater';

const tsconfigPath = 'tsconfig.json';
const tsconfig = fs.readFileSync(tsconfigPath, 'utf-8');

const { result } = updateJson({
  jsonString: tsconfig,
  formatOptions: {
    allowComments: true  // tsconfig.json uses JSONC format
  },
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.compilerOptions,
      merge: (opts) => ({
        ...opts,
        target: 'ES2022',
        lib: ['ES2022']
      })
    });
  }
});

fs.writeFileSync(tsconfigPath, result);
```

### Update VSCode settings.json

```typescript
const settingsJson = `{
  // Editor settings
  "editor.fontSize": 14,
  "editor.tabSize": 2,
  /* Theme configuration
     using dark theme */
  "workbench.colorTheme": "Dark+"
}`;

const { result } = updateJson({
  jsonString: settingsJson,
  formatOptions: {
    allowComments: true
  },
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        'editor.fontSize': 16,
        'editor.formatOnSave': true
      })
    });
  }
});

// Both single-line (//) and multi-line (/* */) comments are preserved
```

### JSONC Comment Preservation

All comment styles are automatically preserved when editing:

```typescript
const jsonString = `{
  // Server configuration
  "server": {
    "host": "localhost", // Current host
    "port": 3000
  }
}`;

const { result } = updateJson({
  jsonString,
  formatOptions: { allowComments: true },
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.server,
      merge: () => ({ host: 'production.example.com' })
    });
  }
});

// Output:
// {
//   // Server configuration
//   "server": {
//     "host": "production.example.com", // Current host
//     "port": 3000
//   }
// }
```

**Supported comment styles:**
- Single-line: `// comment`
- Multi-line: `/* comment */`
- Inline: `"key": "value" // comment`
- Mixed indentation (spaces/tabs)
- Blank lines and spacing preserved

**Note:** Comments inside string values (like URLs with `//`) are never stripped

## Array Merging Strategies

### `mergeByContents` - Deduplicate by Deep Equality

```typescript
import { updateJson, addInstructions } from '@hiscojs/json-updater';

const jsonString = `{
  "items": ["a", "b", "c"]
}`;

const { result } = updateJson({
  jsonString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'items',
          mergeByContents: true
        }),
        items: ['b', 'c', 'd']  // 'b' and 'c' deduplicated
      })
    });
  }
});

// { "items": ["a", "b", "c", "d"] }
```

### `mergeByName` - Merge by Name Property

```typescript
const jsonString = `{
  "containers": [
    { "name": "app", "image": "app:1.0" },
    { "name": "sidecar", "image": "sidecar:1.0" }
  ]
}`;

const { result } = updateJson({
  jsonString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'containers',
          mergeByName: true
        }),
        containers: [
          { name: 'app', image: 'app:2.0' }  // Updates 'app'
        ]
      })
    });
  }
});

// {
//   "containers": [
//     { "name": "app", "image": "app:2.0" },      // Updated
//     { "name": "sidecar", "image": "sidecar:1.0" } // Preserved
//   ]
// }
```

### `mergeByProp` - Merge by Custom Property

```typescript
const jsonString = `{
  "users": [
    { "id": 1, "name": "Alice" },
    { "id": 2, "name": "Bob" }
  ]
}`;

const { result } = updateJson({
  jsonString,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed,
      merge: () => ({
        ...addInstructions({
          prop: 'users',
          mergeByProp: 'id'
        }),
        users: [
          { id: 1, name: 'Alice Smith' },  // Updates id=1
          { id: 3, name: 'Charlie' }       // Adds new
        ]
      })
    });
  }
});

// {
//   "users": [
//     { "id": 1, "name": "Alice Smith" },  // Updated
//     { "id": 2, "name": "Bob" },          // Preserved
//     { "id": 3, "name": "Charlie" }       // Added
//   ]
// }
```

### `deepMerge` - Deep Merge Nested Objects

```typescript
const jsonString = `{
  "configs": [
    {
      "name": "database",
      "settings": {
        "timeout": 30,
        "pool": 10,
        "ssl": true
      }
    }
  ]
}`;

const { result } = updateJson({
  jsonString,
  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:

```typescript
const jsonString = `{
  "version": "1.2.3",
  "buildNumber": 42
}`;

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

// { "version": "1.2.4", "buildNumber": 43 }
```

## Real-World Examples

### package.json Updates

```typescript
const packageJson = `{
  "name": "my-service",
  "version": "1.0.0",
  "dependencies": {
    "express": "^4.18.0",
    "lodash": "^4.17.21"
  }
}`;

const { result } = updateJson({
  jsonString: packageJson,
  annotate: ({ change }) => {
    change({
      findKey: (parsed) => parsed.dependencies,
      merge: (deps) => ({
        ...deps,
        express: '^4.19.0',  // Patch update
        axios: '^1.6.0'      // Add new
      })
    });
  }
});
```

### Configuration Files

```typescript
const configJson = `{
  "server": {
    "port": 3000,
    "host": "localhost"
  },
  "database": {
    "host": "localhost",
    "port": 5432
  }
}`;

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

    change({
      findKey: (parsed) => parsed.database,
      merge: () => ({ host: 'db.production.com' })
    });
  }
});
```

## Best Practices

### 1. Use Type Parameters

```typescript
// ✅ Good - Type safe
const { result } = updateJson<PackageJson>({ ... });

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

### 2. Leverage `originalValue`

```typescript
// ✅ Good - Conditional based on original
merge: (originalValue) => ({
  version: bumpVersion(originalValue.version)
})

// ❌ Avoid - Hardcoded
merge: () => ({ version: '2.0.0' })
```

### 3. Use Merge Strategies for Arrays

```typescript
// ✅ Good - Explicit merge strategy
...addInstructions({
  prop: 'dependencies',
  mergeByProp: 'name'
})

// ❌ Avoid - Array replacement
dependencies: newDeps  // Loses existing items
```

## Dependencies

- [@hiscojs/object-updater](https://www.npmjs.com/package/@hiscojs/object-updater): Core object manipulation

## License

MIT

## Repository

[https://github.com/hisco/json-updater](https://github.com/hisco/json-updater)

## Contributing

Issues and pull requests welcome!