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

aywson

v0.0.16

Published

Modify JSONC while preserving comments and formatting.

Readme

aywson

𝖆𝖗𝖊 𝖞𝖆 𝖜𝖎𝖓𝖓𝖎𝖓𝖌, 𝖘𝖔𝖓?

Are ya winning, son?

Modify JSONC while preserving comments and formatting.

npm install aywson

Usage

import {
  parse, // parse JSONC to object
  modify, // replace fields, delete unlisted
  get, // read value at path
  set, // write value at path (with optional comment)
  remove, // delete field at path
  merge, // update fields, keep unlisted
  replace, // alias for modify
  patch, // alias for merge
  rename, // rename a key
  move, // move field to new path
  getComment, // read comment (above or trailing)
  setComment, // add comment above field
  removeComment, // remove comment above field
  getTrailingComment, // read trailing comment
  setTrailingComment, // add trailing comment
  removeTrailingComment, // remove trailing comment
  sort, // sort object keys
  format // format/prettify JSONC
} from "aywson";

modify

Replace fields, delete unlisted. Comments above deleted fields are also deleted, unless they start with **.

import { modify } from "aywson";

modify('{ /* keep this */ "a": 1, "b": 2 }', { a: 10 });
// → '{ /* keep this */ "a": 10 }' — comment preserved, b deleted

modify uses replace semantics — fields not in changes are deleted. Comments (both above and trailing) on deleted fields are also deleted, unless they start with **.

parse

Parse a JSONC string into a JavaScript value. Unlike JSON.parse(), this handles comments and trailing commas.

import { parse } from "aywson";

parse(`{
  // database config
  "host": "localhost",
  "port": 5432,
}`);
// → { host: "localhost", port: 5432 }

// With TypeScript generics
interface Config {
  host: string;
  port: number;
}
const config = parse<Config>(jsonString);

Path-based Operations

Paths can be specified as either:

  • String paths: "config.database.host" or "items[0].name" (dot-notation, like the CLI)
  • Array paths: ["config", "database", "host"] or ["items", 0, "name"]

Both formats work for all path-based operations.

get(json, path)

Get a value at a path.

// Using string path
get('{ "config": { "enabled": true } }', "config.enabled");
// → true

// Using array path
get('{ "config": { "enabled": true } }', ["config", "enabled"]);
// → true

has(json, path)

Check if a path exists.

has('{ "foo": "bar" }', "foo"); // → true (string path)
has('{ "foo": "bar" }', ["foo"]); // → true (array path)
has('{ "foo": "bar" }', "baz"); // → false

set(json, path, value, comment?)

Set a value at a path, optionally with a comment.

// Using string path
set('{ "foo": "bar" }', "foo", "baz");
// → '{ "foo": "baz" }'

// Using array path
set('{ "foo": "bar" }', ["foo"], "baz");
// → '{ "foo": "baz" }'

// With a comment
set('{ "foo": "bar" }', "foo", "baz", "this is foo");
// → adds "// this is foo" above the field

// Nested paths work with both formats
set('{ "config": {} }', "config.enabled", true);
// or
set('{ "config": {} }', ["config", "enabled"], true);

remove(json, path)

Remove a field. Comments (both above and trailing) are also removed, unless they start with **.

// Using string path
remove(
  `{
  // this is foo
  "foo": "bar",
  "baz": 123
}`,
  "foo"
);
// → '{ "baz": 123 }' — comment removed too

// Using array path
remove(
  `{
  "foo": "bar", // trailing comment
  "baz": 123
}`,
  ["foo"]
);
// → '{ "baz": 123 }' — trailing comment removed too

// Nested paths
remove(json, "config.database.host");
// or
remove(json, ["config", "database", "host"]);

Merge Strategies

merge(json, changes)

Update/add fields, never delete (unless explicit undefined).

merge('{ "a": 1, "b": 2 }', { a: 10 });
// → '{ "a": 10, "b": 2 }' — b preserved

replace(json, changes)

Delete fields not in changes (same as modify).

replace('{ "a": 1, "b": 2 }', { a: 10 });
// → '{ "a": 10 }' — b deleted

patch(json, changes)

Alias for merge. Use undefined to delete.

patch('{ "a": 1, "b": 2 }', { a: undefined });
// → '{ "b": 2 }' — a explicitly deleted

Key Operations

rename(json, path, newKey)

Rename a key while preserving its value.

// Using string path
rename('{ "oldName": 123 }', "oldName", "newName");
// → '{ "newName": 123 }'

// Using array path
rename('{ "oldName": 123 }', ["oldName"], "newName");
// → '{ "newName": 123 }'

// Nested paths
rename(json, "config.oldKey", "newKey");
// or
rename(json, ["config", "oldKey"], "newKey");

move(json, fromPath, toPath)

Move a field to a different location.

// Using string paths
move(
  '{ "source": { "value": 123 }, "target": {} }',
  "source.value",
  "target.value"
);
// → '{ "source": {}, "target": { "value": 123 } }'

// Using array paths
move(
  '{ "source": { "value": 123 }, "target": {} }',
  ["source", "value"],
  ["target", "value"]
);
// → '{ "source": {}, "target": { "value": 123 } }'

// Mixed formats also work
move(json, "source.value", ["target", "value"]);

Sort Operations

sort(json, path?, options?)

Sort object keys alphabetically while preserving comments (both above and trailing) with their respective keys.

sort(`{
  // z comment
  "z": 1,
  // a comment
  "a": 2
}`);
// → '{ "a": 2, "z": 1 }' with comments preserved

// Trailing comments are also preserved
sort(`{
  "z": 1, // z trailing
  "a": 2 // a trailing
}`);
// → '{ "a": 2 // a trailing, "z": 1 // z trailing }'

Path: Specify a path to sort only a nested object (defaults to "" or [] for root).

// Using string path
sort(json, "config.database"); // Sort only the database object

// Using array path
sort(json, ["config", "database"]); // Sort only the database object

// Root level (both equivalent)
sort(json); // or sort(json, "") or sort(json, [])

Options:

  • comparator?: (a: string, b: string) => number — Custom sort function. Defaults to alphabetical.
  • deep?: boolean — Sort nested objects recursively. Defaults to true.
// Custom sort order (reverse alphabetical)
sort(json, "", { comparator: (a, b) => b.localeCompare(a) });

// Only sort top-level keys (not nested objects)
sort(json, "", { deep: false });

// Sort only a specific nested object, non-recursively
sort(json, "config", { deep: false });

## Format Operations

### `format(json, options?)`

Format a JSONC document with consistent indentation. Preserves comments while normalizing whitespace.

```ts
import { format } from "aywson";

// Format minified JSON
format('{"foo":"bar","baz":123}');
// → '{
//   "foo": "bar",
//   "baz": 123
// }'

// Comments are preserved
format('{ /* important */ "foo": "bar" }');
// → '{
//   /* important */
//   "foo": "bar"
// }'

Options:

  • tabSize?: number — Number of spaces per indentation level. Defaults to 2.
  • insertSpaces?: boolean — Use spaces instead of tabs. Defaults to true.
  • eol?: string — End of line character. Defaults to '\n'.
// Use 4 spaces for indentation
format(json, { tabSize: 4 });

// Use tabs instead of spaces
format(json, { insertSpaces: false });

// Use Windows-style line endings
format(json, { eol: "\r\n" });

Comment Operations

setComment(json, path, comment)

Add or update a comment above a field.

// Using string path
setComment(
  `{
  "enabled": true
}`,
  "enabled",
  "controls the feature"
);
// → adds "// controls the feature" above the field

// Using array path
setComment(json, ["config", "enabled"], "controls the feature");

removeComment(json, path)

Remove the comment above a field.

// Using string path
removeComment(
  `{
  // this will be removed
  "foo": "bar"
}`,
  "foo"
);
// → '{ "foo": "bar" }'

// Using array path
removeComment(json, ["config", "enabled"]);

getComment(json, path)

Get the comment associated with a field. First checks for a comment above, then falls back to a trailing comment.

// Using string path
getComment(
  `{
  // this is foo
  "foo": "bar"
}`,
  "foo"
);
// → "this is foo"

// Using array path
getComment(
  `{
  "foo": "bar" // trailing comment
}`,
  ["foo"]
);
// → "trailing comment"

getComment('{ "foo": "bar" }', "foo");
// → null (no comment)

Trailing Comments

Trailing comments are comments on the same line after a field value:

{
  "foo": "bar", // this is a trailing comment
  "baz": 123 // another trailing comment
}

getTrailingComment(json, path)

Get the trailing comment after a field (explicitly, ignoring comments above).

// Using string path
getTrailingComment(
  `{
  "foo": "bar", // trailing comment
  "baz": 123
}`,
  "foo"
);
// → "trailing comment"

// Using array path
getTrailingComment(json, ["config", "database", "host"]);

setTrailingComment(json, path, comment)

Add or update a trailing comment after a field.

// Using string path
setTrailingComment(
  `{
  "foo": "bar",
  "baz": 123
}`,
  "foo",
  "this is foo"
);
// → '{ "foo": "bar" // this is foo, "baz": 123 }'

// Using array path
setTrailingComment(
  `{
  "foo": "bar", // old comment
  "baz": 123
}`,
  ["foo"],
  "new comment"
);
// → replaces "old comment" with "new comment"

removeTrailingComment(json, path)

Remove the trailing comment after a field.

// Using string path
removeTrailingComment(
  `{
  "foo": "bar", // this will be removed
  "baz": 123
}`,
  "foo"
);
// → '{ "foo": "bar", "baz": 123 }'

// Using array path
removeTrailingComment(json, ["config", "database", "host"]);

Comments Above vs Trailing

You can have both a comment above and a trailing comment:

const json = `{
  // comment above
  "foo": "bar", // trailing comment
  "baz": 123
}`;

getComment(json, "foo"); // → "comment above" (prefers above)
getTrailingComment(json, "foo"); // → "trailing comment"

// Set comment above (preserves trailing)
setComment(json, "foo", "new above");
// → both comments preserved, above is updated

// Remove comment above (preserves trailing)
removeComment(json, "foo");
// → trailing comment still there

Preserving Comments

When deleting fields, comments are deleted by default. Start a comment with ** to preserve it:

remove(
  `{
  // this comment will be deleted
  "config": {}
}`,
  "config"
);
// → '{}' — comment deleted with field

remove(
  `{
  // ** this comment will be preserved
  "config": {}
}`,
  "config"
);
// → '{ // ** this comment will be preserved }' — comment kept

Object Iteration & Transformation

Even though aywson works on strings, you can still do full object manipulation:

import { parse, set, remove, merge } from "aywson";

let json = `{
  // Database settings
  "database": {
    "host": "localhost",
    "port": 5432
  },
  // Feature flags
  "features": {
    "darkMode": false,
    "beta": true
  }
}`;

// Parse to iterate/transform
const config = parse<Record<string, unknown>>(json);

// Example: Update all feature flags to false
for (const [key, value] of Object.entries(config.features as object)) {
  if (typeof value === "boolean") {
    json = set(json, `features.${key}`, false); // String path
    // or: json = set(json, ["features", key], false); // Array path
  }
}

// Example: Remove fields based on condition
for (const key of Object.keys(config)) {
  if (key.startsWith("_")) {
    json = remove(json, key); // String path
    // or: json = remove(json, [key]); // Array path
  }
}

// Example: Bulk update from transformed object
const updates = Object.fromEntries(
  Object.entries(config.database as object).map(([k, v]) => [
    k,
    typeof v === "string" ? v.toUpperCase() : v
  ])
);
json = merge(json, { database: updates });

The key insight: use parse() to read and decide what to change, then use set()/remove()/merge() to apply changes while preserving formatting and comments.

Building JSONC from Scratch

You can build a JSONC file from scratch using set() with comments:

import { set } from "aywson";

let json = "{}";

// Build up the structure with comments (using string paths)
json = set(json, "database", {}, "Database configuration");
json = set(json, "database.host", "localhost", "Primary database host");
json = set(json, "database.port", 5432);
json = set(json, "database.ssl", true, "Enable SSL in production");

json = set(json, "features", {}, "Feature flags");
json = set(json, "features.darkMode", false);
json = set(json, "features.beta", true, "Beta features - use with caution");
// Note: Array paths like ["database", "host"] are also supported

console.log(json);

Output:

{
  // Database configuration
  "database": {
    // Primary database host
    "host": "localhost",
    "port": 5432,
    // Enable SSL in production
    "ssl": true
  },
  // Feature flags
  "features": {
    "darkMode": false,
    // Beta features - use with caution
    "beta": true
  }
}

For more complex construction, you can also use merge():

import { merge, setComment } from "aywson";

let json = "{}";

// Add multiple fields at once
json = merge(json, {
  name: "my-app",
  version: "1.0.0",
  scripts: {
    build: "tsc",
    test: "vitest"
  }
});

// Add comments where needed
json = setComment(json, "scripts", "Available npm scripts");
// Note: Array paths like ["scripts"] are also supported

CLI

# Parse JSONC to JSON (strips comments, handles trailing commas)
aywson parse config.jsonc

# Read a value
aywson get config.json database.host

# Set a value (shows diff and writes to file)
aywson set config.json database.port 5433

# Preview without writing
aywson set --dry-run config.json database.port 5433

# Modify with replace semantics
aywson modify config.json '{"database": {"host": "prod.db.com"}}'

# Merge without deleting existing fields
aywson merge config.json '{"newField": true}'

# Remove a field
aywson remove config.json database.debug

# Sort object keys alphabetically
aywson sort config.json

# Sort only a specific nested object
aywson sort config.json dependencies

# Sort without recursing into nested objects
aywson sort config.json --no-deep

# Format/prettify JSONC
aywson format config.json

# Format with 4-space indentation
aywson format config.json --tab-size 4

# Format with tabs instead of spaces
aywson format config.json --tabs

# Get a comment (above, or trailing as fallback)
aywson comment config.json database.host

# Set a comment above a field
aywson comment config.json database.host "production database"

# Remove a comment above a field
aywson uncomment config.json database.host

# Get a trailing comment explicitly
aywson comment --trailing config.json database.port

# Set a trailing comment
aywson comment --trailing config.json database.port "default: 5432"

# Remove a trailing comment
aywson uncomment --trailing config.json database.port

Mutating commands always show a colored diff. Use --dry-run (-n) to preview without writing.

Path syntax: The CLI uses dot-notation: config.database.host or bracket notation for indices: items[0].name. The API supports both string paths (same as CLI) and array paths: ["config", "database", "host"].

Security Options

# Path validation (prevents path traversal attacks)
aywson get config.json database.host  # ✅ Works
aywson get ../etc/passwd root          # ❌ Blocked by default
aywson get --allow-path-traversal ../etc/passwd root  # ✅ Override (not recommended)

# File size limits (default: 50MB)
aywson parse large.json  # ✅ Works if < 50MB
aywson parse --max-file-size 100000000 large.json  # ✅ Custom limit (100MB)
aywson parse --no-file-size-limit huge.json  # ✅ Disable limit (not recommended)

# JSON parsing limits (via environment variables)
AYWSON_MAX_JSON_SIZE=20000000 aywson modify config.json '{"large": "data"}'
AYWSON_MAX_JSON_DEPTH=200 aywson merge config.json '{"deep": {"nested": {...}}}'

Security

aywson includes several security features to protect against common attacks when processing untrusted input:

Path Validation

By default, the CLI prevents path traversal attacks by validating that all file paths stay within the current working directory. This prevents access to files outside the intended directory (e.g., ../etc/passwd).

Override: Use the --allow-path-traversal flag to bypass this protection (not recommended for untrusted input).

# Blocked by default
aywson get ../sensitive-file.json key

# Override (use with caution)
aywson get --allow-path-traversal ../sensitive-file.json key

File Size Limits

To prevent memory exhaustion attacks, file size is limited by default to 50MB. Files larger than this limit will be rejected.

Override: Use --max-file-size <bytes> to set a custom limit, or --no-file-size-limit to disable the limit entirely.

# Default 50MB limit
aywson parse large.json

# Custom limit (100MB)
aywson parse --max-file-size 104857600 large.json

# No limit (not recommended)
aywson parse --no-file-size-limit huge.json

Note: Stdin (-) is exempt from file size limits.

JSON Parsing Limits

JSON input is validated for both size and nesting depth to prevent denial-of-service attacks:

  • Default max size: 10MB
  • Default max depth: 100 levels

Override: Set environment variables to customize these limits:

# Increase JSON size limit to 20MB
AYWSON_MAX_JSON_SIZE=20971520 aywson modify config.json '{"large": "data"}'

# Increase depth limit to 200 levels
AYWSON_MAX_JSON_DEPTH=200 aywson merge config.json '{"deep": {...}}'

# Both limits
AYWSON_MAX_JSON_SIZE=20971520 AYWSON_MAX_JSON_DEPTH=200 aywson modify config.json '...'

These limits apply to JSON arguments in set, modify, and merge commands.

Security Best Practices

  1. Don't disable security features unless you fully trust your input sources
  2. Use appropriate limits for your use case rather than disabling them entirely
  3. Validate input before passing it to aywson when processing untrusted data
  4. Run with least privilege - don't run aywson as root or with elevated permissions
  5. Keep dependencies updated - regularly update aywson and its dependencies for security patches

Comparison with comment-json

comment-json is another popular package for working with JSON files that contain comments. Here's how the two packages differ:

Architecture

| Aspect | aywson | comment-json | | ------------------- | ------------------------------------- | -------------------------------------- | | Approach | String-in, string-out | Parse to object, modify, stringify | | Formatting | Preserves original formatting exactly | Re-stringifies (may change formatting) | | Mutations | Immutable (returns new string) | Mutable (modifies object in place) | | Comment storage | Stays in the string | Symbol properties on object |

Feature Set

| Category | aywson | comment-json | | --------------------- | ------------------------------------------------------------ | ------------------------------------ | | Core | parse() | parse(), stringify(), assign() | | Path operations | get(), has(), set(), remove() | Object/array access | | Bulk updates | merge(), modify() | assign() | | Key manipulation | rename(), move(), sort() | ❌ | | Comment API | getComment(), setComment(), getTrailingComment(), etc. | Symbol-based access | | Comment positions | Above field and trailing (same line) | Many (before, after, inline, etc.) | | Extras | CLI, ** prefix to preserve comments | CommentArray for array operations |

When to use aywson

  • You need exact formatting preservation (whitespace, indentation, trailing commas)
  • You want surgical edits without re-serializing the entire file
  • You prefer immutable operations that return new strings
  • You need high-level operations like rename, move, or sort
  • You want explicit comment manipulation with a simple API

When to use comment-json

  • You want to work with a JavaScript object and make many modifications before writing back
  • You're comfortable with Symbol-based comment access
  • Re-stringifying the entire file is acceptable for your use case

Example comparison

comment-json:

const { parse, stringify, assign } = require("comment-json");

const obj = parse(jsonString);
obj.database.port = 5433;
assign(obj.database, { ssl: true });
const result = stringify(obj, null, 2);

aywson:

import { set, merge } from "aywson";

let result = set(jsonString, "database.port", 5433);
result = merge(result, { database: { ssl: true } });
// Original formatting preserved exactly