diff --git a/README.md b/README.md index 6a77876..ea12ce0 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,13 @@ ![Node](https://img.shields.io/badge/node-%3E=14-blue.svg?style=flat-square) [![npm version](https://badge.fury.io/js/json-codemod.svg)](https://badge.fury.io/js/json-codemod) -A utility to patch JSON strings while preserving the original formatting, including comments and whitespace. +Modify JSON strings with a fluent chainable API while preserving formatting, comments, and whitespace. ## ✨ Features - 🎨 **Format Preservation** - Maintains comments, whitespace, and original formatting -- 🔄 **Precise Modifications** - Replace, delete, and insert values while leaving everything else intact -- ⚡ **Unified Patch API** - Apply multiple operations efficiently in a single call +- 🔗 **Chainable API** - Fluent interface for readable modifications +- ⚡ **Sequential Operations** - Apply multiple changes in order - 🚀 **Fast & Lightweight** - Zero dependencies, minimal footprint - 📦 **Dual module support** - Works with both ESM and CommonJS - 💪 **TypeScript Support** - Full type definitions included @@ -33,639 +33,365 @@ pnpm add json-codemod ## 🚀 Quick Start -### Using Patch (Recommended for Multiple Operations) - ```js -import { batch } from "json-codemod"; +import jsonmod from "json-codemod"; -const source = '{"name": "Alice", "age": 30, "items": [1, 2]}'; +const source = '{"name": "Alice", "age": 30, "items": [1, 2, 3]}'; -// Apply multiple operations at once (most efficient) -const result = batch(source, [ - { path: "age", value: "31" }, // Replace - { path: "name" }, // Delete (no value means delete) - { path: "items", position: 2, value: "3" }, // Insert -]); +const result = jsonmod(source) + .replace("name", '"Bob"') + .replace("age", "31") + .delete("items[1]") + .insert("items", 2, "4") + .apply(); -console.log(result); -// Output: {"age": 31, "items": [1, 2, 3]} +// Result: {"name": "Bob", "age": 31, "items": [1, 4, 3]} ``` -### Replace Values +### With Value Helpers + +Use `formatValue` for automatic type handling: ```js -import { replace } from "json-codemod"; +import jsonmod, { formatValue } from "json-codemod"; -const source = '{"name": "Alice", "age": 30}'; +const source = '{"name": "Alice", "age": 30, "active": false}'; -// Replace a single value -const result = replace(source, [{ path: "age", value: "31" }]); +const result = jsonmod(source) + .replace("name", formatValue("Bob")) // Strings quoted automatically + .replace("age", formatValue(31)) // Numbers handled correctly + .replace("active", formatValue(true)) // Booleans too + .apply(); -console.log(result); -// Output: {"name": "Alice", "age": 31} +// Result: {"name": "Bob", "age": 31, "active": true} ``` -### Delete Properties and Elements - -```js -import { remove } from "json-codemod"; +## 📖 API Reference -const source = '{"name": "Alice", "age": 30, "city": "Beijing"}'; +### `jsonmod(sourceText)` -// Delete a property -const result = remove(source, [{ path: "age" }]); +Creates a chainable instance for JSON modifications. -console.log(result); -// Output: {"name": "Alice", "city": "Beijing"} -``` +**Parameters:** +- `sourceText` (string): JSON string to modify -### Insert Properties and Elements +**Returns:** `JsonMod` instance +**Example:** ```js -import { insert } from "json-codemod"; - -// Insert into object -const source1 = '{"name": "Alice"}'; -const result1 = insert(source1, [{ path: "", key: "age", value: "30" }]); -console.log(result1); -// Output: {"name": "Alice", "age": 30} - -// Insert into array -const source2 = '{"numbers": [1, 3, 4]}'; -const result2 = insert(source2, [{ path: "numbers", position: 1, value: "2" }]); -console.log(result2); -// Output: {"numbers": [1, 2, 3, 4]} +const mod = jsonmod('{"name": "Alice"}'); ``` -### Preserving Format and Comments - -```js -const source = `{ - // User information - "name": "Alice", - "age": 30, /* years old */ - "city": "Beijing" -}`; - -const result = replace(source, [{ path: "age", value: "31" }]); - -console.log(result); -// Output: { -// // User information -// "name": "Alice", -// "age": 31, /* years old */ -// "city": "Beijing" -// } -``` +### `.replace(path, value)` -## 📖 Usage Examples +Replace a value at the specified path. -### Replace Operations +**Parameters:** +- `path` (string | string[]): JSON path +- `value` (string): New value as JSON string -#### Modifying Nested Objects +**Returns:** `this` (chainable) +**Examples:** ```js -const source = '{"user": {"name": "Alice", "profile": {"age": 30}}}'; - -const result = replace(source, [{ path: "user.profile.age", value: "31" }]); +// Simple replacement +jsonmod(source).replace("name", '"Bob"').apply(); -// Result: {"user": {"name": "Alice", "profile": {"age": 31}}} -``` +// Nested path +jsonmod(source).replace("user.profile.age", "31").apply(); -#### Modifying Array Elements - -```js -const source = '{"scores": [85, 90, 95]}'; +// Array element +jsonmod(source).replace("items[1]", "99").apply(); -const result = replace(source, [{ path: "scores[1]", value: "92" }]); - -// Result: {"scores": [85, 92, 95]} +// Using formatValue +jsonmod(source).replace("name", formatValue("Bob")).apply(); ``` -#### Using JSON Pointer +### `.delete(path)` / `.remove(path)` -```js -const source = '{"data": {"items": [1, 2, 3]}}'; +Delete a property or array element. -const result = replace(source, [{ path: "/data/items/2", value: "99" }]); +**Parameters:** +- `path` (string | string[]): JSON path -// Result: {"data": {"items": [1, 2, 99]}} -``` - -#### Batch Modifications +**Returns:** `this` (chainable) +**Examples:** ```js -const source = '{"x": 1, "y": 2, "arr": [3, 4]}'; - -const result = replace(source, [ - { path: "x", value: "10" }, - { path: "y", value: "20" }, - { path: "arr[0]", value: "30" }, -]); +// Delete property +jsonmod(source).delete("age").apply(); -// Result: {"x": 10, "y": 20, "arr": [30, 4]} -``` +// Delete array element +jsonmod(source).delete("items[0]").apply(); -#### Modifying String Values +// Delete nested property +jsonmod(source).delete("user.email").apply(); -```js -const source = '{"message": "Hello"}'; +// Multiple deletions (remove is alias) +jsonmod(source) + .delete("a") + .remove("b") + .apply(); +``` -const result = replace(source, [{ path: "message", value: '"World"' }]); +### `.insert(path, keyOrPosition, value)` -// Result: {"message": "World"} -// Note: value needs to include quotes for strings -``` +Insert into objects or arrays. -### Delete Operations +**Parameters:** +- `path` (string | string[]): Path to container +- `keyOrPosition` (string | number): Property name (object) or index (array) +- `value` (string): Value as JSON string -#### Deleting Object Properties +**Returns:** `this` (chainable) +**Examples:** ```js -import { remove } from "json-codemod"; +// Insert into object +jsonmod(source) + .insert("", "email", '"test@example.com"') + .apply(); -const source = '{"name": "Alice", "age": 30, "city": "Beijing"}'; +// Insert into array at position +jsonmod(source) + .insert("items", 0, '"first"') + .apply(); -// Delete a single property -const result = remove(source, [{ path: "age" }]); +// Append to array +jsonmod(source) + .insert("items", 3, '"last"') + .apply(); -// Result: {"name": "Alice", "city": "Beijing"} +// Using formatValue +jsonmod(source) + .insert("user", "age", formatValue(30)) + .apply(); ``` -#### Deleting Array Elements +### `.apply()` -```js -const source = '{"items": [1, 2, 3, 4, 5]}'; - -// Delete an element by index -const result = remove(source, [{ path: "items[2]" }]); - -// Result: {"items": [1, 2, 4, 5]} -``` +Execute all queued operations and return modified JSON. -#### Deleting Nested Properties +**Returns:** Modified JSON string +**Example:** ```js -const source = '{"user": {"name": "Alice", "age": 30, "email": "alice@example.com"}}'; - -const result = remove(source, [{ path: "user.email" }]); - -// Result: {"user": {"name": "Alice", "age": 30}} +const result = jsonmod(source) + .replace("a", "1") + .delete("b") + .insert("", "c", "3") + .apply(); // Execute and return result ``` -#### Batch Deletions - -```js -const source = '{"a": 1, "b": 2, "c": 3, "d": 4}'; - -const result = remove(source, [{ path: "b" }, { path: "d" }]); +### `formatValue(value)` -// Result: {"a": 1, "c": 3} -``` +Convert JavaScript values to JSON strings automatically. -### Insert Operations +**Parameters:** +- `value` (any): JavaScript value -#### Inserting into Objects +**Returns:** JSON string representation +**Examples:** ```js -import { insert } from "json-codemod"; - -const source = '{"name": "Alice"}'; +import { formatValue } from "json-codemod"; -// Insert a new property (key is required for objects) -const result = insert(source, [{ path: "", key: "age", value: "30" }]); - -// Result: {"name": "Alice", "age": 30} +formatValue(42) // "42" +formatValue("hello") // '"hello"' +formatValue(true) // "true" +formatValue(null) // "null" +formatValue({a: 1}) // '{"a":1}' +formatValue([1, 2, 3]) // '[1,2,3]' ``` -#### Inserting into Arrays - -```js -const source = '{"numbers": [1, 2, 4, 5]}'; - -// Insert at specific position -const result = insert(source, [{ path: "numbers", position: 2, value: "3" }]); +## 🎯 Examples -// Result: {"numbers": [1, 2, 3, 4, 5]} -``` - -#### Inserting at Array Start +### Configuration File Updates ```js -const source = '{"list": [2, 3, 4]}'; - -const result = insert(source, [{ path: "list", position: 0, value: "1" }]); - -// Result: {"list": [1, 2, 3, 4]} -``` - -#### Appending to Array +import jsonmod, { formatValue } from "json-codemod"; +import { readFileSync, writeFileSync } from "fs"; -```js -const source = '{"list": [1, 2, 3]}'; +const config = readFileSync("tsconfig.json", "utf-8"); -// Omit position to append at the end -const result = insert(source, [{ path: "list", value: "4" }]); +const updated = jsonmod(config) + .replace("compilerOptions.target", formatValue("ES2022")) + .replace("compilerOptions.strict", formatValue(true)) + .delete("compilerOptions.experimentalDecorators") + .insert("compilerOptions", "moduleResolution", formatValue("bundler")) + .apply(); -// Result: {"list": [1, 2, 3, 4]} +writeFileSync("tsconfig.json", updated); ``` -#### Inserting into Nested Structures +### Preserving Comments and Formatting ```js -const source = '{"data": {"items": [1, 2]}}'; +const source = `{ + // User configuration + "name": "Alice", + "age": 30, /* years */ + "active": true +}`; -// Insert into nested array -const result = insert(source, [{ path: "data.items", position: 1, value: "99" }]); +const result = jsonmod(source) + .replace("age", "31") + .replace("active", "false") + .apply(); -// Result: {"data": {"items": [1, 99, 2]}} +// Comments and formatting preserved! ``` -### Modifying Complex Values +### Complex Nested Operations ```js -const source = '{"config": {"timeout": 3000}}'; +const data = '{"user": {"name": "Alice", "settings": {"theme": "dark"}}}'; -// Replace with an object -const result1 = replace(source, [{ path: "config", value: '{"timeout": 5000, "retry": 3}' }]); - -// Replace with an array -const result2 = replace(source, [{ path: "config", value: "[1, 2, 3]" }]); +const result = jsonmod(data) + .replace("user.name", formatValue("Bob")) + .replace("user.settings.theme", formatValue("light")) + .insert("user.settings", "language", formatValue("en")) + .apply(); ``` -### Handling Special Characters in Keys - -Use JSON Pointer to handle keys with special characters: +### Array Manipulations ```js -const source = '{"a/b": {"c~d": 5}}'; - -// In JSON Pointer: -// ~0 represents ~ -// ~1 represents / -const result = replace(source, [{ path: "/a~1b/c~0d", value: "42" }]); - -// Result: {"a/b": {"c~d": 42}} -``` - -## 📚 API Documentation - -### `batch(sourceText, patches)` ⭐ Recommended - -Applies multiple operations (replace, delete, insert) in a single call. This is the most efficient way to apply multiple changes as it only parses the source once. - -#### Parameters - -- **sourceText** (`string`): The original JSON string -- **patches** (`Array`): Array of mixed operations to apply - -#### Batch Types - -The function automatically detects the operation type based on the batch properties: - -```typescript -// Replace: has value but no key/position -{ path: string, value: string } - -// Delete: no value, key, or position -{ path: string } +const source = '{"items": [1, 2, 3, 4, 5]}'; -// Insert (object): has key and value -{ path: string, key: string, value: string } +const result = jsonmod(source) + .delete("items[1]") // Remove second item + .delete("items[2]") // Remove what is now third item + .insert("items", 0, "0") // Insert at beginning + .apply(); -// Insert (array): has position and value -{ path: string, position: number, value: string } +// Result: {"items": [0, 1, 4, 5]} ``` -#### Return Value - -Returns the modified JSON string with all patches applied. - -#### Example +### Conditional Operations ```js -const result = batch('{"a": 1, "b": 2, "items": [1, 2]}', [ - { path: "a", value: "10" }, // Replace - { path: "b" }, // Delete - { path: "items", position: 2, value: "3" }, // Insert -]); -// Returns: '{"a": 10, "items": [1, 2, 3]}' -``` +let mod = jsonmod(config); ---- - -### `replace(sourceText, patches)` - -Modifies values in a JSON string. - -#### Parameters - -- **sourceText** (`string`): The original JSON string -- **patches** (`Array`): Array of modifications to apply - -#### Patch Object - -```typescript -interface ReplacePatch { - /** - * A JSON path where the replacement should occur. - */ - path: string; - /** - * The value to insert at the specified path. - */ - value: string; +if (isDevelopment) { + mod = mod.replace("debug", "true"); } -``` - -#### Return Value - -Returns the modified JSON string. -#### Error Handling - -- If a path doesn't exist, that modification is silently ignored without throwing an error -- If multiple modifications have conflicting (overlapping) paths, an error is thrown - ---- - -### `remove(sourceText, patches)` - -Deletes properties from objects or elements from arrays in a JSON string. - -#### Parameters - -- **sourceText** (`string`): The original JSON string -- **patches** (`Array`): Array of deletions to apply - -#### DeletePatch Object - -```typescript -interface DeletePatch { - /** - * A JSON path to delete. - */ - path: string; +if (needsUpdate) { + mod = mod.replace("version", formatValue("2.0.0")); } -``` -#### Return Value - -Returns the modified JSON string with specified paths removed. - -#### Error Handling - -- If a path doesn't exist, the deletion is silently ignored -- Whitespace and commas are automatically handled to maintain valid JSON - ---- - -### `insert(sourceText, patches)` - -Inserts new properties into objects or elements into arrays in a JSON string. - -#### Parameters - -- **sourceText** (`string`): The original JSON string -- **patches** (`Array`): Array of insertions to apply - -#### InsertPatch Object - -```typescript -interface InsertPatch { - /** - * A JSON path where the insertion should occur. - * For arrays: the path should point to the array, and position specifies the index. - * For objects: the path should point to the object, and key specifies the property name. - */ - path: string; - /** - * The value to insert. - */ - value: string; - /** - * For array insertion: the index where to insert the value. - * If omitted, the value is appended to the end. - */ - position?: number; - /** - * For object insertion: the key name for the new property. - * Required when inserting into objects. - */ - key?: string; -} +const result = mod.apply(); ``` -#### Return Value - -Returns the modified JSON string with new values inserted. - -#### Error Handling - -- For object insertions, `key` is required -- For object insertions, if the key already exists, an error is thrown -- For array insertions, position must be within valid bounds (0 to array.length) - ---- - -### Path Syntax - -Two path syntaxes are supported for all operations: - -1. **Dot Notation** (recommended for simple cases) +## 📚 Path Syntax - - Object properties: `"user.name"` - - Array indices: `"items[0]"` - - Nested paths: `"data.users[0].name"` - -2. **JSON Pointer** (RFC 6901) - - Format: starts with `/` - - Object properties: `"/user/name"` - - Array indices: `"/items/0"` - - Escape sequences: - - `~0` represents `~` - - `~1` represents `/` - - Example: `"/a~1b/c~0d"` refers to the `c~d` property of the `a/b` object - -### Value Format - -The `value` parameter must be a string representation of a JSON value: - -- Numbers: `"42"`, `"3.14"` -- Strings: `'"hello"'` (must include quotes) -- Booleans: `"true"`, `"false"` -- null: `"null"` -- Objects: `'{"key": "value"}'` -- Arrays: `'[1, 2, 3]'` - -## 🎯 Use Cases - -### Configuration File Modification - -Perfect for modifying configuration files with comments (like `tsconfig.json`, `package.json`, etc.): +### Dot Notation ```js -import { readFileSync, writeFileSync } from "fs"; -import { replace, remove, insert } from "json-codemod"; - -// Read configuration file -const config = readFileSync("tsconfig.json", "utf-8"); - -// Modify configuration -const updated = replace(config, [ - { path: "compilerOptions.target", value: '"ES2020"' }, - { path: "compilerOptions.strict", value: "true" }, -]); - -// Save configuration (preserving original format and comments) -writeFileSync("tsconfig.json", updated); +jsonmod(source).replace("user.profile.name", '"Bob"').apply(); ``` -### Managing Dependencies +### Bracket Notation for Arrays ```js -import { readFileSync, writeFileSync } from "fs"; -import { insert, remove } from "json-codemod"; - -const pkg = readFileSync("package.json", "utf-8"); - -// Add a new dependency -const withNewDep = insert(pkg, [{ path: "dependencies", key: "lodash", value: '"^4.17.21"' }]); - -// Remove a dependency -const cleaned = remove(pkg, [{ path: "dependencies.old-package" }]); - -writeFileSync("package.json", cleaned); +jsonmod(source).replace("items[0]", "1").apply(); +jsonmod(source).delete("items[2]").apply(); ``` -### JSON Data Transformation +### JSON Pointer ```js -// Batch update JSON data -const data = fetchDataAsString(); - -const updated = replace(data, [ - { path: "metadata.version", value: '"2.0"' }, - { path: "metadata.updatedAt", value: `"${new Date().toISOString()}"` }, -]); +jsonmod(source).replace("/user/profile/name", '"Bob"').apply(); ``` -### Array Manipulation +### Special Characters -```js -import { insert, remove } from "json-codemod"; - -const data = '{"tasks": ["task1", "task2", "task4"]}'; - -// Insert a task in the middle -const withTask = insert(data, [{ path: "tasks", position: 2, value: '"task3"' }]); - -// Remove a completed task -const updated = remove(withTask, [{ path: "tasks[0]" }]); -``` - -### Automation Scripts +For keys with special characters, use JSON Pointer: ```js -// Automated version number updates -const pkg = readFileSync("package.json", "utf-8"); -const version = "1.2.3"; +// Key with slash: "a/b" +jsonmod(source).replace("/a~1b", "value").apply(); -const updated = replace(pkg, [{ path: "version", value: `"${version}"` }]); - -writeFileSync("package.json", updated); +// Key with tilde: "a~b" +jsonmod(source).replace("/a~0b", "value").apply(); ``` ## 💻 TypeScript Support -The package includes full TypeScript type definitions: +Full TypeScript support with type definitions: ```typescript -import { replace, remove, insert, Patch, DeletePatch, InsertPatch } from "json-codemod"; - -const source: string = '{"count": 0}'; +import jsonmod, { JsonMod, formatValue } from "json-codemod"; -// Replace -const patches: Patch[] = [{ path: "count", value: "1" }]; -const result: string = replace(source, patches); +const source = '{"name": "Alice", "age": 30}'; -// Delete -const deletePatches: DeletePatch[] = [{ path: "count" }]; -const deleted: string = remove(source, deletePatches); +const instance: JsonMod = jsonmod(source); -// Insert -const insertPatches: InsertPatch[] = [{ path: "", key: "name", value: '"example"' }]; -const inserted: string = insert(source, insertPatches); +const result: string = instance + .replace("name", formatValue("Bob")) + .delete("age") + .apply(); ``` ## 🔧 How It Works -json-codemod uses Concrete Syntax Tree (CST) technology: - -1. **Tokenization** (Tokenizer): Breaks down the JSON string into tokens, including values, whitespace, and comments -2. **Parsing** (CSTBuilder): Builds a syntax tree that preserves all formatting information -3. **Path Resolution** (PathResolver): Locates the node to modify based on the path -4. **Precise Replacement**: Replaces only the target value, preserving everything else - -This approach ensures that everything except the modified values (including whitespace, comments, and formatting) remains unchanged. +1. **Parse:** Creates a Concrete Syntax Tree (CST) preserving all formatting +2. **Queue:** Operations are queued, not executed immediately +3. **Execute:** `.apply()` runs operations sequentially, re-parsing after each +4. **Return:** Returns the modified JSON string with formatting preserved ## ❓ FAQ -### Q: Why does the value parameter need to be a string? - -A: For flexibility and precision. You have complete control over the output format, including quotes, spacing, etc. +### Why use formatValue? +**Without formatValue:** ```js -// Numbers don't need quotes -replace(source, [{ path: "age", value: "30" }]); - -// Strings need quotes -replace(source, [{ path: "name", value: '"Alice"' }]); - -// You can control formatting -replace(source, [{ path: "data", value: '{\n "key": "value"\n}' }]); +.replace("name", '"Bob"') // Must remember quotes +.replace("age", "30") // No quotes for numbers +.replace("active", "true") // No quotes for booleans ``` -### Q: How are non-existent paths handled? - -A: If a path doesn't exist, that modification is automatically ignored without throwing an error. The original string remains unchanged. +**With formatValue:** +```js +.replace("name", formatValue("Bob")) // Automatic +.replace("age", formatValue(30)) // Automatic +.replace("active", formatValue(true)) // Automatic +``` -### Q: What JSON extensions are supported? +### How are comments preserved? -A: Supported: +The library parses JSON into a Concrete Syntax Tree that includes comments and whitespace as tokens. Modifications only change value tokens, leaving everything else intact. -- ✅ Single-line comments `//` -- ✅ Block comments `/* */` -- ✅ All standard JSON syntax +### What about performance? -Not supported: +Operations are applied sequentially with re-parsing between each. This ensures correctness but means: +- Fast for small to medium JSON files +- For large files with many operations, consider batching similar changes -- ❌ Other JSON5 features (like unquoted keys, trailing commas, etc.) +### Can I reuse a JsonMod instance? -### Q: How is the performance? +No, call `.apply()` returns a string and operations are cleared. Create a new instance for new modifications: -A: json-codemod is specifically designed for precise modifications with excellent performance. For large files (hundreds of KB), parsing and modification typically complete in milliseconds. +```js +const result1 = jsonmod(source).replace("a", "1").apply(); +const result2 = jsonmod(result1).replace("b", "2").apply(); +``` ## 🤝 Contributing -Contributions are welcome! If you'd like to contribute to the project: - -1. Fork the repository -2. Create your feature branch (`git checkout -b feature/AmazingFeature`) -3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the branch (`git push origin feature/AmazingFeature`) -5. Open a Pull Request +Contributions are welcome! Please feel free to submit a Pull Request. ## 📄 License -This project is licensed under the [Anti 996 License](LICENSE). +[Anti 996 License](https://github.com/996icu/996.ICU/blob/master/LICENSE) ## 🔗 Links -- [npm package](https://www.npmjs.com/package/json-codemod) -- [GitHub repository](https://github.com/axetroy/json-codemod) -- [Issue tracker](https://github.com/axetroy/json-codemod/issues) +- [GitHub](https://github.com/axetroy/json-codemod) +- [npm](https://www.npmjs.com/package/json-codemod) +- [API Documentation](./CHAINABLE_API.md) ## 🌟 Star History -If this project helps you, please give it a ⭐️! +[![Star History Chart](https://api.star-history.com/svg?repos=axetroy/json-codemod&type=Date)](https://star-history.com/#axetroy/json-codemod&Date) diff --git a/fixtures/cjs/index.cjs b/fixtures/cjs/index.cjs index eb861f4..51ad0c1 100644 --- a/fixtures/cjs/index.cjs +++ b/fixtures/cjs/index.cjs @@ -1,3 +1,3 @@ -const jsoncst = require("jsoncst"); +const { default: jsonmod, formatValue } = require("jsoncst"); -console.log(jsoncst, jsoncst.replace); +console.log(jsonmod, formatValue); diff --git a/fixtures/esm/index.mjs b/fixtures/esm/index.mjs index e3a1de9..b8e602a 100644 --- a/fixtures/esm/index.mjs +++ b/fixtures/esm/index.mjs @@ -1,3 +1,3 @@ -import jsoncst, { replace } from "jsoncst"; +import jsonmod, { formatValue } from "jsoncst"; -console.log(jsoncst, replace); +console.log(jsonmod, formatValue); diff --git a/package.json b/package.json index 2752c05..f765d72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "json-codemod", - "version": "1.1.0", + "version": "2.0.0", "private": false, "description": "A utility to patch the JSON string and preserve the original formatting, including comments and whitespace.", "sideEffects": false, diff --git a/rslib.config.js b/rslib.config.js index 73537ba..896e5fc 100644 --- a/rslib.config.js +++ b/rslib.config.js @@ -41,7 +41,6 @@ class RspackDtsCopyPlugin { }; copyDts(path.join(projectDir, "src")); - copyDts(path.join(projectDir, "src", "function")); }); } } diff --git a/src/JsonMod.d.ts b/src/JsonMod.d.ts new file mode 100644 index 0000000..342ba63 --- /dev/null +++ b/src/JsonMod.d.ts @@ -0,0 +1,65 @@ +/** + * JsonMod - A chainable API for modifying JSON strings while preserving formatting + */ +export declare class JsonMod { + /** + * Creates a new JsonMod instance + * @param sourceText - The JSON string to modify + */ + constructor(sourceText: string); + + /** + * Replace a value at the specified path + * @param path - The JSON path or array of path segments + * @param value - The new value as a JSON string + * @returns Returns this for chaining + * @example + * jsonmod(source).replace("user.name", '"Bob"').apply() + * jsonmod(source).replace(["user", "name"], '"Bob"').apply() + */ + replace(path: string | string[], value: string): JsonMod; + + /** + * Delete a property or array element at the specified path + * @param path - The JSON path or array of path segments + * @returns Returns this for chaining + * @example + * jsonmod(source).delete("user.age").apply() + * jsonmod(source).remove(["user", "age"]).apply() + */ + delete(path: string | string[]): JsonMod; + + /** + * Alias for delete() + * @param path - The JSON path or array of path segments + * @returns Returns this for chaining + */ + remove(path: string | string[]): JsonMod; + + /** + * Insert a new property into an object or element into an array + * @param path - The JSON path pointing to the object/array + * @param keyOrPosition - For objects: property name; For arrays: index + * @param value - The value to insert as a JSON string + * @returns Returns this for chaining + * @example + * jsonmod(source).insert("user", "email", '"test@example.com"').apply() + * jsonmod(source).insert("items", 0, '"newItem"').apply() + */ + insert(path: string | string[], keyOrPosition: string | number, value: string): JsonMod; + + /** + * Apply all queued operations and return the modified JSON string + * @returns The modified JSON string + */ + apply(): string; +} + +/** + * Factory function to create a new JsonMod instance + * @param sourceText - The JSON string to modify + * @returns A new JsonMod instance + * @example + * jsonmod(source).replace("a", "10").delete("b").apply() + */ +export declare function jsonmod(sourceText: string): JsonMod; diff --git a/src/JsonMod.js b/src/JsonMod.js new file mode 100644 index 0000000..d5a28c5 --- /dev/null +++ b/src/JsonMod.js @@ -0,0 +1,376 @@ +import { Tokenizer } from "./Tokenizer.js"; +import { CSTBuilder } from "./CSTBuilder.js"; +import { resolvePath } from "./PathResolver.js"; +import { parsePath, extractString } from "./helper.js"; + +/** + * JsonMod - A chainable API for modifying JSON strings while preserving formatting + * @class + */ +export class JsonMod { + /** + * Creates a new JsonMod instance + * @param {string} sourceText - The JSON string to modify + */ + constructor(sourceText) { + this.sourceText = sourceText; + this.operations = []; + } + + /** + * Replace a value at the specified path + * @param {string|string[]} path - The JSON path or array of path segments + * @param {string} value - The new value as a JSON string + * @returns {JsonMod} - Returns this for chaining + * @example + * jsonmod(source).replace("user.name", '"Bob"').apply() + * jsonmod(source).replace(["user", "name"], '"Bob"').apply() + */ + replace(path, value) { + this.operations.push({ + type: "replace", + path: Array.isArray(path) ? path : path, + value, + }); + return this; + } + + /** + * Delete a property or array element at the specified path + * @param {string|string[]} path - The JSON path or array of path segments + * @returns {JsonMod} - Returns this for chaining + * @example + * jsonmod(source).delete("user.age").apply() + * jsonmod(source).remove(["user", "age"]).apply() + */ + delete(path) { + this.operations.push({ + type: "delete", + path: Array.isArray(path) ? path : path, + }); + return this; + } + + /** + * Alias for delete() + * @param {string|string[]} path - The JSON path or array of path segments + * @returns {JsonMod} - Returns this for chaining + */ + remove(path) { + return this.delete(path); + } + + /** + * Insert a new property into an object or element into an array + * @param {string|string[]} path - The JSON path pointing to the object/array + * @param {string|number} keyOrPosition - For objects: property name; For arrays: index + * @param {string} value - The value to insert as a JSON string + * @returns {JsonMod} - Returns this for chaining + * @example + * jsonmod(source).insert("user", "email", '"test@example.com"').apply() + * jsonmod(source).insert("items", 0, '"newItem"').apply() + */ + insert(path, keyOrPosition, value) { + this.operations.push({ + type: "insert", + path: Array.isArray(path) ? path : path, + keyOrPosition, + value, + }); + return this; + } + + /** + * Apply all queued operations and return the modified JSON string + * @returns {string} - The modified JSON string + */ + apply() { + if (this.operations.length === 0) { + return this.sourceText; + } + + let result = this.sourceText; + + // Apply operations sequentially to avoid conflicts + for (const op of this.operations) { + switch (op.type) { + case "replace": + result = this._applySingleReplace(result, op); + break; + case "delete": + result = this._applySingleDelete(result, op); + break; + case "insert": + result = this._applySingleInsert(result, op); + break; + } + } + + return result; + } + + /** + * Internal method to apply a single replace operation + * @private + */ + _applySingleReplace(sourceText, op) { + const tokenizer = new Tokenizer(sourceText); + const tokens = tokenizer.tokenize(); + const builder = new CSTBuilder(tokens); + const root = builder.build(); + + const node = resolvePath(root, op.path, sourceText); + if (!node) { + return sourceText; + } + + return sourceText.slice(0, node.start) + op.value + sourceText.slice(node.end); + } + + /** + * Internal method to apply a single delete operation + * @private + */ + _applySingleDelete(sourceText, op) { + const tokenizer = new Tokenizer(sourceText); + const tokens = tokenizer.tokenize(); + const builder = new CSTBuilder(tokens); + const root = builder.build(); + + const pathParts = parsePath(op.path); + if (pathParts.length === 0) { + return sourceText; + } + + const parentPath = pathParts.slice(0, -1); + const lastKey = pathParts[pathParts.length - 1]; + const parentNode = parentPath.length > 0 ? resolvePath(root, parentPath, sourceText) : root; + + if (!parentNode) { + return sourceText; + } + + return this._deleteFromParent(sourceText, parentNode, lastKey, sourceText); + } + + /** + * Internal method to apply a single insert operation + * @private + */ + _applySingleInsert(sourceText, op) { + const tokenizer = new Tokenizer(sourceText); + const tokens = tokenizer.tokenize(); + const builder = new CSTBuilder(tokens); + const root = builder.build(); + + const node = resolvePath(root, op.path, sourceText); + if (!node) { + return sourceText; + } + + return this._insertIntoNode(sourceText, node, op, sourceText); + } + + _getDeleteStart(parentNode, key, sourceText) { + if (parentNode.type === "Object") { + for (const prop of parentNode.properties) { + const keyStr = extractString(prop.key, sourceText); + if (keyStr === key) { + return prop.key.start; + } + } + } else if (parentNode.type === "Array") { + if (typeof key === "number" && key >= 0 && key < parentNode.elements.length) { + return parentNode.elements[key].start; + } + } + return 0; + } + + _deleteFromParent(sourceText, parentNode, key, originalSource) { + if (parentNode.type === "Object") { + return this._deleteObjectProperty(sourceText, parentNode, key, originalSource); + } else if (parentNode.type === "Array") { + return this._deleteArrayElement(sourceText, parentNode, key, originalSource); + } + return sourceText; + } + + _deleteObjectProperty(sourceText, objectNode, key, originalSource) { + let propIndex = -1; + for (let i = 0; i < objectNode.properties.length; i++) { + const keyStr = extractString(objectNode.properties[i].key, originalSource); + if (keyStr === key) { + propIndex = i; + break; + } + } + + if (propIndex === -1) return sourceText; + + const prop = objectNode.properties[propIndex]; + let deleteStart = prop.key.start; + let deleteEnd = prop.value.end; + + // Handle comma and whitespace + if (propIndex < objectNode.properties.length - 1) { + let pos = deleteEnd; + while ( + pos < sourceText.length && + (sourceText[pos] === " " || sourceText[pos] === "\t" || sourceText[pos] === "\n" || sourceText[pos] === "\r") + ) { + pos++; + } + if (sourceText[pos] === ",") { + deleteEnd = pos + 1; + while ( + deleteEnd < sourceText.length && + (sourceText[deleteEnd] === " " || + sourceText[deleteEnd] === "\t" || + sourceText[deleteEnd] === "\n" || + sourceText[deleteEnd] === "\r") + ) { + deleteEnd++; + } + } + } else if (propIndex > 0) { + let pos = deleteStart - 1; + while (pos >= 0 && (sourceText[pos] === " " || sourceText[pos] === "\t" || sourceText[pos] === "\n" || sourceText[pos] === "\r")) { + pos--; + } + if (sourceText[pos] === ",") { + let commaPos = pos; + pos--; + while ( + pos >= 0 && + (sourceText[pos] === " " || sourceText[pos] === "\t" || sourceText[pos] === "\n" || sourceText[pos] === "\r") + ) { + pos--; + } + deleteStart = pos + 1; + } + } + + return sourceText.slice(0, deleteStart) + sourceText.slice(deleteEnd); + } + + _deleteArrayElement(sourceText, arrayNode, index, originalSource) { + if (typeof index !== "number" || index < 0 || index >= arrayNode.elements.length) { + return sourceText; + } + + const element = arrayNode.elements[index]; + let deleteStart = element.start; + let deleteEnd = element.end; + + // Handle comma and whitespace + if (index < arrayNode.elements.length - 1) { + let pos = deleteEnd; + while ( + pos < sourceText.length && + (sourceText[pos] === " " || sourceText[pos] === "\t" || sourceText[pos] === "\n" || sourceText[pos] === "\r") + ) { + pos++; + } + if (sourceText[pos] === ",") { + deleteEnd = pos + 1; + while ( + deleteEnd < sourceText.length && + (sourceText[deleteEnd] === " " || + sourceText[deleteEnd] === "\t" || + sourceText[deleteEnd] === "\n" || + sourceText[deleteEnd] === "\r") + ) { + deleteEnd++; + } + } + } else if (index > 0) { + let pos = deleteStart - 1; + while (pos >= 0 && (sourceText[pos] === " " || sourceText[pos] === "\t" || sourceText[pos] === "\n" || sourceText[pos] === "\r")) { + pos--; + } + if (sourceText[pos] === ",") { + let commaPos = pos; + pos--; + while ( + pos >= 0 && + (sourceText[pos] === " " || sourceText[pos] === "\t" || sourceText[pos] === "\n" || sourceText[pos] === "\r") + ) { + pos--; + } + deleteStart = pos + 1; + } + } + + return sourceText.slice(0, deleteStart) + sourceText.slice(deleteEnd); + } + + _insertIntoNode(sourceText, node, patch, originalSource) { + if (node.type === "Object") { + return this._insertObjectProperty(sourceText, node, patch, originalSource); + } else if (node.type === "Array") { + return this._insertArrayElement(sourceText, node, patch, originalSource); + } + return sourceText; + } + + _insertObjectProperty(sourceText, objectNode, patch, originalSource) { + const key = patch.keyOrPosition; + if (typeof key !== "string") { + throw new Error("Insert into object requires a string key"); + } + + // Check if key already exists + for (const prop of objectNode.properties) { + const keyStr = extractString(prop.key, originalSource); + if (keyStr === key) { + throw new Error(`Key "${key}" already exists in object`); + } + } + + const newEntry = `"${key}": ${patch.value}`; + + if (objectNode.properties.length === 0) { + const insertPos = objectNode.start + 1; + return sourceText.slice(0, insertPos) + newEntry + sourceText.slice(insertPos); + } else { + const lastProp = objectNode.properties[objectNode.properties.length - 1]; + const insertPos = lastProp.value.end; + return sourceText.slice(0, insertPos) + ", " + newEntry + sourceText.slice(insertPos); + } + } + + _insertArrayElement(sourceText, arrayNode, patch, originalSource) { + const position = typeof patch.keyOrPosition === "number" ? patch.keyOrPosition : arrayNode.elements.length; + + if (position < 0 || position > arrayNode.elements.length) { + throw new Error(`Invalid position ${position} for array of length ${arrayNode.elements.length}`); + } + + if (arrayNode.elements.length === 0) { + const insertPos = arrayNode.start + 1; + return sourceText.slice(0, insertPos) + patch.value + sourceText.slice(insertPos); + } else if (position === 0) { + const insertPos = arrayNode.elements[0].start; + return sourceText.slice(0, insertPos) + patch.value + ", " + sourceText.slice(insertPos); + } else if (position >= arrayNode.elements.length) { + const lastElement = arrayNode.elements[arrayNode.elements.length - 1]; + const insertPos = lastElement.end; + return sourceText.slice(0, insertPos) + ", " + patch.value + sourceText.slice(insertPos); + } else { + const insertPos = arrayNode.elements[position].start; + return sourceText.slice(0, insertPos) + patch.value + ", " + sourceText.slice(insertPos); + } + } +} + +/** + * Factory function to create a new JsonMod instance + * @param {string} sourceText - The JSON string to modify + * @returns {JsonMod} - A new JsonMod instance + * @example + * jsonmod(source).replace("a", "10").delete("b").apply() + */ +export function jsonmod(sourceText) { + return new JsonMod(sourceText); +} diff --git a/src/JsonMod.test.js b/src/JsonMod.test.js new file mode 100644 index 0000000..4764c2a --- /dev/null +++ b/src/JsonMod.test.js @@ -0,0 +1,180 @@ +import test, { describe } from "node:test"; +import assert from "node:assert/strict"; + +import { jsonmod } from "./JsonMod.js"; + +describe("JsonMod Chainable API Tests", () => { + test("simple replace operation", () => { + const source = '{"a":1,"b":true}'; + const result = jsonmod(source).replace("a", "42").apply(); + assert.equal(result, '{"a":42,"b":true}'); + }); + + test("multiple replace operations", () => { + const source = '{"x":1,"y":2}'; + const result = jsonmod(source).replace("x", "10").replace("y", "20").apply(); + assert.equal(result, '{"x":10,"y":20}'); + }); + + test("simple delete operation", () => { + const source = '{"a":1,"b":2,"c":3}'; + const result = jsonmod(source).delete("b").apply(); + assert.equal(result, '{"a":1,"c":3}'); + }); + + test("remove is alias for delete", () => { + const source = '{"a":1,"b":2}'; + const result = jsonmod(source).remove("b").apply(); + assert.equal(result, '{"a":1}'); + }); + + test("multiple delete operations", () => { + const source = '{"a":1,"b":2,"c":3}'; + const result = jsonmod(source).delete("a").delete("c").apply(); + assert.equal(result, '{"b":2}'); + }); + + test("insert into object", () => { + const source = '{"name":"Alice"}'; + const result = jsonmod(source).insert("", "age", "30").apply(); + assert.equal(result, '{"name":"Alice", "age": 30}'); + }); + + test("insert into array at start", () => { + const source = '{"arr":[2,3]}'; + const result = jsonmod(source).insert("arr", 0, "1").apply(); + assert.equal(result, '{"arr":[1, 2,3]}'); + }); + + test("insert into array at end", () => { + const source = '{"arr":[1,2]}'; + const result = jsonmod(source).insert("arr", 2, "3").apply(); + assert.equal(result, '{"arr":[1,2, 3]}'); + }); + + test("chained replace and delete", () => { + const source = '{"a":1,"b":2,"c":3}'; + const result = jsonmod(source).replace("a", "10").delete("b").apply(); + assert.equal(result, '{"a":10,"c":3}'); + }); + + test("chained replace, delete, and insert", () => { + const source = '{"a":1,"b":2}'; + const result = jsonmod(source).replace("a", "10").delete("b").insert("", "c", "3").apply(); + assert.equal(result, '{"a":10, "c": 3}'); + }); + + test("nested object replace", () => { + const source = '{"user":{"name":"Alice","age":30}}'; + const result = jsonmod(source).replace("user.name", '"Bob"').apply(); + assert.equal(result, '{"user":{"name":"Bob","age":30}}'); + }); + + test("array element replace", () => { + const source = '{"arr":[1,2,3]}'; + const result = jsonmod(source).replace("arr[1]", "99").apply(); + assert.equal(result, '{"arr":[1,99,3]}'); + }); + + test("complex chained operations", () => { + const source = '{"user":{"name":"Alice","age":30},"items":[1,2,3]}'; + const result = jsonmod(source) + .replace("user.name", '"Bob"') + .replace("user.age", "31") + .delete("items[1]") + .insert("items", 2, "4") // After delete, array has length 2, so we insert at position 2 + .apply(); + + // After operations: name=Bob, age=31, items=[1,3,4] + assert(result.includes('"name":"Bob"')); + assert(result.includes('"age":31')); + // Note: exact formatting may vary based on operation order + }); + + test("preserves formatting and comments", () => { + const source = `{ + // User info + "name": "Alice", + "age": 30 +}`; + + const result = jsonmod(source).replace("age", "31").apply(); + + assert(result.includes("// User info")); + assert(result.includes('"age": 31')); + }); + + test("no operations returns original", () => { + const source = '{"a":1}'; + const result = jsonmod(source).apply(); + assert.equal(result, source); + }); + + test("using JSON pointer path", () => { + const source = '{"a":{"b":[1,2,3]}}'; + const result = jsonmod(source).replace("/a/b/2", "99").apply(); + assert.equal(result, '{"a":{"b":[1,2,99]}}'); + }); + + test("replace with string value", () => { + const source = '{"greeting":"hello"}'; + const result = jsonmod(source).replace("greeting", '"hi"').apply(); + assert.equal(result, '{"greeting":"hi"}'); + }); + + test("replace with object value", () => { + const source = '{"obj":{"a":1}}'; + const result = jsonmod(source).replace("obj", '{"x":10}').apply(); + assert.equal(result, '{"obj":{"x":10}}'); + }); + + test("replace with array value", () => { + const source = '{"arr":[1,2]}'; + const result = jsonmod(source).replace("arr", "[10,20]").apply(); + assert.equal(result, '{"arr":[10,20]}'); + }); + + test("delete from nested object", () => { + const source = '{"user":{"name":"Alice","age":30,"city":"NYC"}}'; + const result = jsonmod(source).delete("user.age").apply(); + assert(result.includes('"name":"Alice"')); + assert(result.includes('"city":"NYC"')); + assert(!result.includes("age")); + }); + + test("delete from array", () => { + const source = '{"arr":[1,2,3,4,5]}'; + const result = jsonmod(source).delete("arr[2]").apply(); + assert.equal(result, '{"arr":[1,2,4,5]}'); + }); + + test("insert empty object property", () => { + const source = '{"user":{}}'; + const result = jsonmod(source).insert("user", "email", '"test@example.com"').apply(); + assert.equal(result, '{"user":{"email": "test@example.com"}}'); + }); + + test("insert into empty array", () => { + const source = '{"arr":[]}'; + const result = jsonmod(source).insert("arr", 0, '"item"').apply(); + assert.equal(result, '{"arr":["item"]}'); + }); + + test("multiple operations on same path", () => { + const source = '{"a":1}'; + const result = jsonmod(source).replace("a", "2").replace("a", "3").apply(); + // Last operation wins + assert.equal(result, '{"a":3}'); + }); + + test("chainable API returns instance", () => { + const source = '{"a":1}'; + const instance = jsonmod(source); + const result1 = instance.replace("a", "2"); + const result2 = result1.delete("a"); + + assert.equal(typeof instance.apply, "function"); + assert.equal(typeof result1.apply, "function"); + assert.equal(typeof result2.apply, "function"); + }); +}); diff --git a/src/function/batch.d.ts b/src/function/batch.d.ts deleted file mode 100644 index c3a39b2..0000000 --- a/src/function/batch.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ReplacePatch } from "./replace.js"; -import { DeletePatch } from "./delete.js"; -import { InsertPatch } from "./insert.js"; - -export type BatchPatch = ReplacePatch | DeletePatch | InsertPatch; - -/** - * Applies a batch of patches to the source text. - * @param sourceText - The original source text. - * @param patches - An array of patches to apply. - * @returns The modified source text after applying all patches. - */ -export declare function batch(sourceText: string, patches: Array): string; diff --git a/src/function/batch.js b/src/function/batch.js deleted file mode 100644 index 51bd5a8..0000000 --- a/src/function/batch.js +++ /dev/null @@ -1,60 +0,0 @@ -import { replace } from "./replace.js"; -import { remove } from "./delete.js"; -import { insert } from "./insert.js"; -import { Tokenizer } from "../Tokenizer.js"; -import { CSTBuilder } from "../CSTBuilder.js"; - -export function batch(sourceText, patches) { - // Parse the source text once - const tokenizer = new Tokenizer(sourceText); - const tokens = tokenizer.tokenize(); - const builder = new CSTBuilder(tokens); - const root = builder.build(); - - // Categorize patches by operation type - const replacePatches = []; - const deletePatches = []; - const insertPatches = []; - - for (const p of patches) { - // Determine patch type based on properties - if (p.value !== undefined && p.key === undefined && p.position === undefined) { - // Has value but no key/position -> replace operation - replacePatches.push({ path: p.path, value: p.value }); - } else if (p.value === undefined && p.key === undefined && p.position === undefined) { - // No value, key, or position -> delete operation - deletePatches.push({ path: p.path }); - } else if ((p.key !== undefined || p.position !== undefined) && p.value !== undefined) { - // Has key or position with value -> insert operation - insertPatches.push(p); - } else { - // Invalid patch - skip it - continue; - } - } - - // Apply patches in order: replace, insert, delete - // This order ensures that replacements happen first, then inserts, then deletes - let result = sourceText; - - // Apply replacements - if (replacePatches.length > 0) { - result = replace(result, replacePatches, root); - } - - // Apply insertions - if (insertPatches.length > 0) { - // Need to re-parse if we did replacements - const currentRoot = replacePatches.length > 0 ? null : root; - result = insert(result, insertPatches, currentRoot); - } - - // Apply deletions - if (deletePatches.length > 0) { - // Need to re-parse if we did replacements or insertions - const currentRoot = replacePatches.length > 0 || insertPatches.length > 0 ? null : root; - result = remove(result, deletePatches, currentRoot); - } - - return result; -} diff --git a/src/function/delete.d.ts b/src/function/delete.d.ts deleted file mode 100644 index 1b9583f..0000000 --- a/src/function/delete.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Node } from "../CSTBuilder.js"; - -export interface DeletePatch { - /** - * A JSON path to delete. - */ - path: string; -} - -/** - * Deletes nodes from the JSON content based on the provided patches. - * @param sourceText - The original JSON content as a string. - * @param patches - An array of delete patches specifying the paths to remove. - * @param root - Optional CST root node to avoid re-parsing. - */ -export declare function remove(sourceText: string, patches: Array, root?: Node): string; diff --git a/src/function/delete.js b/src/function/delete.js deleted file mode 100644 index a25e4fa..0000000 --- a/src/function/delete.js +++ /dev/null @@ -1,195 +0,0 @@ -import { Tokenizer } from "../Tokenizer.js"; -import { CSTBuilder } from "../CSTBuilder.js"; -import { resolvePath } from "../PathResolver.js"; -import { parsePath, extractString } from "../helper.js"; - -export function remove(sourceText, patches, root) { - if (!root) { - const tokenizer = new Tokenizer(sourceText); - const tokens = tokenizer.tokenize(); - - const builder = new CSTBuilder(tokens); - root = builder.build(); - } - - // 倒叙删除 - const reverseNodes = patches - .map((patch) => { - const pathParts = parsePath(patch.path); - if (pathParts.length === 0) { - return null; // Cannot delete root - } - - // Find parent and the item to delete - const parentPath = pathParts.slice(0, -1); - const lastKey = pathParts[pathParts.length - 1]; - const parentNode = parentPath.length > 0 ? resolvePath(root, parentPath, sourceText) : root; - - if (!parentNode) return null; - - return { - parentNode, - lastKey, - patch, - }; - }) - .filter((v) => v !== null) - .sort((a, b) => { - // Sort by the start position of what we're deleting - const aStart = getDeleteStart(a.parentNode, a.lastKey, sourceText); - const bStart = getDeleteStart(b.parentNode, b.lastKey, sourceText); - return bStart - aStart; - }); - - let result = sourceText; - - for (const { parentNode, lastKey } of reverseNodes) { - result = deleteFromParent(result, parentNode, lastKey, sourceText); - } - - return result; -} - -function getDeleteStart(parentNode, key, sourceText) { - if (parentNode.type === "Object") { - for (const prop of parentNode.properties) { - const keyStr = extractString(prop.key, sourceText); - if (keyStr === key) { - return prop.key.start; - } - } - } else if (parentNode.type === "Array") { - if (typeof key === "number" && key >= 0 && key < parentNode.elements.length) { - return parentNode.elements[key].start; - } - } - return 0; -} - -function deleteObjectProperty(sourceText, objectNode, key, originalSource) { - let propIndex = -1; - for (let i = 0; i < objectNode.properties.length; i++) { - const keyStr = extractString(objectNode.properties[i].key, originalSource); - if (keyStr === key) { - propIndex = i; - break; - } - } - - if (propIndex === -1) return sourceText; - - const prop = objectNode.properties[propIndex]; - let deleteStart = prop.key.start; - let deleteEnd = prop.value.end; - - // Handle comma and whitespace - if (propIndex < objectNode.properties.length - 1) { - // Not the last property, look for comma after - let pos = deleteEnd; - while ( - pos < sourceText.length && - (sourceText[pos] === " " || sourceText[pos] === "\t" || sourceText[pos] === "\n" || sourceText[pos] === "\r") - ) { - pos++; - } - if (sourceText[pos] === ",") { - deleteEnd = pos + 1; - // Skip trailing whitespace after comma - while ( - deleteEnd < sourceText.length && - (sourceText[deleteEnd] === " " || - sourceText[deleteEnd] === "\t" || - sourceText[deleteEnd] === "\n" || - sourceText[deleteEnd] === "\r") - ) { - deleteEnd++; - } - } - } else if (propIndex > 0) { - // Last property, look for comma before (and whitespace before the comma) - let pos = deleteStart - 1; - // Skip whitespace before the property - while (pos >= 0 && (sourceText[pos] === " " || sourceText[pos] === "\t" || sourceText[pos] === "\n" || sourceText[pos] === "\r")) { - pos--; - } - if (sourceText[pos] === ",") { - // Also skip whitespace before the comma - let commaPos = pos; - pos--; - while ( - pos >= 0 && - (sourceText[pos] === " " || sourceText[pos] === "\t" || sourceText[pos] === "\n" || sourceText[pos] === "\r") - ) { - pos--; - } - deleteStart = pos + 1; - } - } - - return sourceText.slice(0, deleteStart) + sourceText.slice(deleteEnd); -} - -function deleteArrayElement(sourceText, arrayNode, index, originalSource) { - if (typeof index !== "number" || index < 0 || index >= arrayNode.elements.length) { - return sourceText; - } - - const element = arrayNode.elements[index]; - let deleteStart = element.start; - let deleteEnd = element.end; - - // Handle comma and whitespace - if (index < arrayNode.elements.length - 1) { - // Not the last element, look for comma after - let pos = deleteEnd; - while ( - pos < sourceText.length && - (sourceText[pos] === " " || sourceText[pos] === "\t" || sourceText[pos] === "\n" || sourceText[pos] === "\r") - ) { - pos++; - } - if (sourceText[pos] === ",") { - deleteEnd = pos + 1; - // Skip trailing whitespace after comma - while ( - deleteEnd < sourceText.length && - (sourceText[deleteEnd] === " " || - sourceText[deleteEnd] === "\t" || - sourceText[deleteEnd] === "\n" || - sourceText[deleteEnd] === "\r") - ) { - deleteEnd++; - } - } - } else if (index > 0) { - // Last element, look for comma before (and whitespace before the comma) - let pos = deleteStart - 1; - // Skip whitespace before the element - while (pos >= 0 && (sourceText[pos] === " " || sourceText[pos] === "\t" || sourceText[pos] === "\n" || sourceText[pos] === "\r")) { - pos--; - } - if (sourceText[pos] === ",") { - // Also skip whitespace before the comma - let commaPos = pos; - pos--; - while ( - pos >= 0 && - (sourceText[pos] === " " || sourceText[pos] === "\t" || sourceText[pos] === "\n" || sourceText[pos] === "\r") - ) { - pos--; - } - deleteStart = pos + 1; - } - } - - return sourceText.slice(0, deleteStart) + sourceText.slice(deleteEnd); -} - -function deleteFromParent(sourceText, parentNode, key, originalSource) { - if (parentNode.type === "Object") { - return deleteObjectProperty(sourceText, parentNode, key, originalSource); - } else if (parentNode.type === "Array") { - return deleteArrayElement(sourceText, parentNode, key, originalSource); - } - return sourceText; -} diff --git a/src/function/insert.d.ts b/src/function/insert.d.ts deleted file mode 100644 index 93c774c..0000000 --- a/src/function/insert.d.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Node } from "../CSTBuilder.js"; - -export interface InsertPatchArray { - /** - * A JSON path where the insertion should occur. - * For arrays: the path should point to the array, and position specifies the index. - * For objects: the path should point to the object, and key specifies the property name. - */ - path: string; - /** - * For array insertion: the index where to insert the value. - */ - position: number; - /** - * The value to insert. - */ - value: string; -} - -export interface InsertPatchObject { - /** - * A JSON path where the insertion should occur. - * For arrays: the path should point to the array, and position specifies the index. - * For objects: the path should point to the object, and key specifies the property name. - */ - path: string; - /** - * For object insertion: the key name for the new property. - */ - key: string; - /** - * The value to insert. - */ - value: string; -} - -export type InsertPatch = InsertPatchArray | InsertPatchObject; - -/** - * Inserts values into a JSON structure at specified paths. - * @param sourceText - The original JSON text. - * @param patches - An array of insertion patches. - * @param root - Optional CST root node to avoid re-parsing. - */ -export declare function insert(sourceText: string, patches: Array, root?: Node): string; diff --git a/src/function/insert.js b/src/function/insert.js deleted file mode 100644 index f263fd1..0000000 --- a/src/function/insert.js +++ /dev/null @@ -1,97 +0,0 @@ -import { Tokenizer } from "../Tokenizer.js"; -import { CSTBuilder } from "../CSTBuilder.js"; -import { resolvePath } from "../PathResolver.js"; -import { extractString } from "../helper.js"; - -export function insert(sourceText, patches, root) { - if (!root) { - const tokenizer = new Tokenizer(sourceText); - const tokens = tokenizer.tokenize(); - - const builder = new CSTBuilder(tokens); - root = builder.build(); - } - - // 倒叙插入 - const reverseNodes = patches - .map((patch) => { - const node = resolvePath(root, patch.path, sourceText); - return { - node, - patch, - }; - }) - .filter((v) => v.node) - .sort((a, b) => b.node.start - a.node.start); - - let result = sourceText; - - for (const { node, patch } of reverseNodes) { - result = insertIntoNode(result, node, patch, sourceText); - } - - return result; -} - -function insertIntoNode(sourceText, node, patch, originalSource) { - if (node.type === "Object") { - return insertObjectProperty(sourceText, node, patch, originalSource); - } else if (node.type === "Array") { - return insertArrayElement(sourceText, node, patch, originalSource); - } - return sourceText; -} - -function insertObjectProperty(sourceText, objectNode, patch, originalSource) { - if (!patch.key) { - throw new Error("Insert into object requires 'key' property"); - } - - // Check if key already exists - for (const prop of objectNode.properties) { - const keyStr = extractString(prop.key, originalSource); - if (keyStr === patch.key) { - throw new Error(`Key "${patch.key}" already exists in object`); - } - } - - const newEntry = `"${patch.key}": ${patch.value}`; - - if (objectNode.properties.length === 0) { - // Empty object - const insertPos = objectNode.start + 1; - return sourceText.slice(0, insertPos) + newEntry + sourceText.slice(insertPos); - } else { - // Insert after last property - const lastProp = objectNode.properties[objectNode.properties.length - 1]; - const insertPos = lastProp.value.end; - return sourceText.slice(0, insertPos) + ", " + newEntry + sourceText.slice(insertPos); - } -} - -function insertArrayElement(sourceText, arrayNode, patch, originalSource) { - const position = patch.position !== undefined ? patch.position : arrayNode.elements.length; - - if (position < 0 || position > arrayNode.elements.length) { - throw new Error(`Invalid position ${position} for array of length ${arrayNode.elements.length}`); - } - - if (arrayNode.elements.length === 0) { - // Empty array - const insertPos = arrayNode.start + 1; - return sourceText.slice(0, insertPos) + patch.value + sourceText.slice(insertPos); - } else if (position === 0) { - // Insert at the beginning - const insertPos = arrayNode.elements[0].start; - return sourceText.slice(0, insertPos) + patch.value + ", " + sourceText.slice(insertPos); - } else if (position >= arrayNode.elements.length) { - // Insert at the end - const lastElement = arrayNode.elements[arrayNode.elements.length - 1]; - const insertPos = lastElement.end; - return sourceText.slice(0, insertPos) + ", " + patch.value + sourceText.slice(insertPos); - } else { - // Insert in the middle - const insertPos = arrayNode.elements[position].start; - return sourceText.slice(0, insertPos) + patch.value + ", " + sourceText.slice(insertPos); - } -} diff --git a/src/function/replace.d.ts b/src/function/replace.d.ts deleted file mode 100644 index 46f0210..0000000 --- a/src/function/replace.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Node } from "../CSTBuilder.js"; - -export interface ReplacePatch { - /** - * A JSON path where the replacement should occur. - */ - path: string; - /** - * The value to insert at the specified path. - */ - value: string; -} - -/** - * Replaces values in a JSON-like string at specified paths with new values. - * @param sourceText - The original JSON content as a string. - * @param patches - An array of replacement instructions. - * @param root - Optional CST root node to avoid re-parsing. - */ -export declare function replace(sourceText: string, patches: Array, root?: Node): string; diff --git a/src/function/replace.js b/src/function/replace.js deleted file mode 100644 index 414949b..0000000 --- a/src/function/replace.js +++ /dev/null @@ -1,43 +0,0 @@ -import { Tokenizer } from "../Tokenizer.js"; -import { CSTBuilder } from "../CSTBuilder.js"; -import { resolvePath } from "../PathResolver.js"; - -export function replace(sourceText, patches, root) { - if (!root) { - const tokenizer = new Tokenizer(sourceText); - const tokens = tokenizer.tokenize(); - - const builder = new CSTBuilder(tokens); - root = builder.build(); - } - - // 倒叙替换 - const reverseNodes = patches - .map((patch) => { - const node = resolvePath(root, patch.path, sourceText); - - return { - node, - patch, - }; - }) - .filter((v) => v.node) - .sort((a, b) => b.node.start - a.node.start); - - // 确保不会冲突 - reverseNodes.reduce((lastEnd, { node }) => { - if (node.end > lastEnd) { - throw new Error(`Patch conflict at path: ${node.path}`); - } - - return node.start; - }, Infinity); - - let result = sourceText; - - for (const { node, patch } of reverseNodes) { - result = result.slice(0, node.start) + patch.value + result.slice(node.end); - } - - return result; -} diff --git a/src/index.d.ts b/src/index.d.ts index 754aa2d..d5f6dbe 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,16 +1,11 @@ -import { replace, ReplacePatch } from "./function/replace.js"; -import { remove, DeletePatch } from "./function/delete.js"; -import { insert, InsertPatch } from "./function/insert.js"; -import { batch, BatchPatch } from "./function/batch.js"; +import { formatValue } from "./value-helpers.js"; +import { jsonmod, JsonMod } from "./JsonMod.js"; -export { ReplacePatch, DeletePatch, InsertPatch, BatchPatch, replace, remove, insert, batch }; -interface JSONCTS { - replace: typeof replace; - remove: typeof remove; - insert: typeof insert; - batch: typeof batch; -} +export { + formatValue, + jsonmod, + JsonMod, +}; -declare const jsoncts: JSONCTS; - -export default jsoncts; +// New chainable API is the default export +export default jsonmod; diff --git a/src/index.js b/src/index.js index bfaef41..118c8d1 100644 --- a/src/index.js +++ b/src/index.js @@ -1,15 +1,9 @@ -import { replace } from "./function/replace.js"; -import { remove } from "./function/delete.js"; -import { insert } from "./function/insert.js"; -import { batch } from "./function/batch.js"; +import { formatValue } from "./value-helpers.js"; +import { jsonmod, JsonMod } from "./JsonMod.js"; -const jsoncst = { - replace: replace, - remove: remove, - insert: insert, - batch: batch, -}; +// Export new chainable API as default +export default jsonmod; -export { replace, remove, insert, batch }; +// Export new API and helper +export { jsonmod, JsonMod, formatValue }; -export default jsoncst; diff --git a/src/index.test.js b/src/index.test.js deleted file mode 100644 index cdf2c37..0000000 --- a/src/index.test.js +++ /dev/null @@ -1,471 +0,0 @@ -import test, { describe } from "node:test"; -import assert from "node:assert/strict"; - -import { replace, remove, insert, batch } from "./index.js"; - -// ===== REPLACE OPERATION TESTS ===== -describe("Replace Operation Tests", () => { - test("replace simple value", () => { - const source = '{"a":1,"b":true}'; - - const replaced = replace(source, [{ path: "a", value: "42" }]); - - assert.equal(replaced, '{"a":42,"b":true}'); - }); - - test("replace array element", () => { - const source = '{"arr":[1,2,3]}'; - - const replaced = replace(source, [{ path: "arr[1]", value: "99" }]); - - assert.equal(replaced, '{"arr":[1,99,3]}'); - }); - - test("replace nested object value", () => { - const source = '{"a":{"b":{"c":1}}}'; - - const replaced = replace(source, [{ path: "a.b.c", value: "123" }]); - - assert.equal(replaced, '{"a":{"b":{"c":123}}}'); - }); - - test("replace multiple paths", () => { - const source = '{"x":1,"y":2,"arr":[3,4]}'; - - const replaced = replace(source, [ - { path: "x", value: "10" }, - { path: "arr[0]", value: "30" }, - ]); - - assert.equal(replaced, '{"x":10,"y":2,"arr":[30,4]}'); - }); - - test("replace using JSON pointer", () => { - const source = '{"a":{"b":[1,2,3]}}'; - - const replaced = replace(source, [{ path: "/a/b/2", value: "99" }]); - - assert.equal(replaced, '{"a":{"b":[1,2,99]}}'); - }); - - test("replace using escaped JSON pointer", () => { - const source = '{"a/b":{"c~d":5}}'; - - const replaced = replace(source, [{ path: "/a~1b/c~0d", value: "42" }]); - - assert.equal(replaced, '{"a/b":{"c~d":42}}'); - }); - - test("replace with string value", () => { - const source = '{"greeting":"hello"}'; - - const replaced = replace(source, [{ path: "greeting", value: '"hi"' }]); - - assert.equal(replaced, '{"greeting":"hi"}'); - }); - - test("replace boolean value", () => { - const source = '{"flag":false}'; - - const replaced = replace(source, [{ path: "flag", value: "true" }]); - - assert.equal(replaced, '{"flag":true}'); - }); - - test("replace null value", () => { - const source = '{"data":null}'; - - const replaced = replace(source, [{ path: "data", value: '"not null"' }]); - - assert.equal(replaced, '{"data":"not null"}'); - }); - - test("replace entire object", () => { - const source = '{"obj":{"a":1,"b":2}}'; - - const replaced = replace(source, [{ path: "obj", value: '{"x":10,"y":20}' }]); - - assert.equal(replaced, '{"obj":{"x":10,"y":20}}'); - }); - - test("replace entire array", () => { - const source = '{"arr":[1,2,3]}'; - - const replaced = replace(source, [{ path: "arr", value: "[10,20,30]" }]); - - assert.equal(replaced, '{"arr":[10,20,30]}'); - }); - - test("replace with whitespace preservation", () => { - const source = '{ "key" : 1 }'; - - const replaced = replace(source, [{ path: "key", value: "42" }]); - - assert.equal(replaced, '{ "key" : 42 }'); - }); - - test("replace with special characters in strings", () => { - const source = '{"text":"Line1\\nLine2"}'; - - const replaced = replace(source, [{ path: "text", value: '"NewLine1\\nNewLine2"' }]); - - assert.equal(replaced, '{"text":"NewLine1\\nNewLine2"}'); - }); - - test("replace numeric keys in object", () => { - const source = '{"1":"one","2":"two"}'; - - const replaced = replace(source, [{ path: "2", value: '"TWO"' }]); - - assert.equal(replaced, '{"1":"one","2":"TWO"}'); - }); - test("replace with empty string", () => { - const source = '{"message":"Hello, World!"}'; - - const replaced = replace(source, [{ path: "message", value: '""' }]); - - assert.equal(replaced, '{"message":""}'); - }); - - test("replace array with different length", () => { - const source = '{"numbers":[1,2,3,4,5]}'; - - const replaced = replace(source, [{ path: "numbers", value: "[10,20]" }]); - - assert.equal(replaced, '{"numbers":[10,20]}'); - }); - - test("replace and keep formatting", () => { - const source = `{ - "name": "Alice", - "age": 30, - "isStudent": false -}`; - - const replaced = replace(source, [{ path: "age", value: "31" }]); - - const expected = `{ - "name": "Alice", - "age": 31, - "isStudent": false -}`; - - assert.equal(replaced, expected); - }); - - test("replace non-existing path", () => { - const source = '{"a":1,"b":2}'; - - const replaced = replace(source, [{ path: "c", value: "3" }]); - - // Should remain unchanged - assert.equal(replaced, '{"a":1,"b":2}'); - }); - - test("replace with multiple patches", () => { - const source = '{"a":1,"b":{"c":2,"d":[3,4,5]}}'; - - const replaced = replace(source, [ - { - path: "b.c", - value: "20", - }, - { path: "b.d[1]", value: "40" }, - ]); - - assert.equal(replaced, '{"a":1,"b":{"c":20,"d":[3,40,5]}}'); - }); - - test("replace with comments preserved", () => { - const source = `{ - // This is a comment - "key": "value" /* inline comment */ -}`; - - const replaced = replace(source, [{ path: "key", value: '"newValue"' }]); - - const expected = `{ - // This is a comment - "key": "newValue" /* inline comment */ -}`; - - assert.equal(replaced, expected); - }); -}); - -// ===== DELETE OPERATION TESTS ===== -describe("Delete Operation Tests", () => { - test("remove simple property from object", () => { - const source = '{"a":1,"b":2,"c":3}'; - - const result = remove(source, [{ path: "b" }]); - - assert.equal(result, '{"a":1,"c":3}'); - }); - - test("remove first property from object", () => { - const source = '{"a":1,"b":2,"c":3}'; - - const result = remove(source, [{ path: "a" }]); - - assert.equal(result, '{"b":2,"c":3}'); - }); - - test("remove last property from object", () => { - const source = '{"a":1,"b":2,"c":3}'; - - const result = remove(source, [{ path: "c" }]); - - assert.equal(result, '{"a":1,"b":2}'); - }); - - test("remove array element", () => { - const source = '{"arr":[1,2,3,4]}'; - - const result = remove(source, [{ path: "arr[1]" }]); - - assert.equal(result, '{"arr":[1,3,4]}'); - }); - - test("remove first array element", () => { - const source = '{"arr":[1,2,3]}'; - - const result = remove(source, [{ path: "arr[0]" }]); - - assert.equal(result, '{"arr":[2,3]}'); - }); - - test("remove last array element", () => { - const source = '{"arr":[1,2,3]}'; - - const result = remove(source, [{ path: "arr[2]" }]); - - assert.equal(result, '{"arr":[1,2]}'); - }); - - test("remove nested object property", () => { - const source = '{"a":{"b":1,"c":2},"d":3}'; - - const result = remove(source, [{ path: "a.b" }]); - - assert.equal(result, '{"a":{"c":2},"d":3}'); - }); - - test("remove with JSON pointer", () => { - const source = '{"a":{"b":[1,2,3]}}'; - - const result = remove(source, [{ path: "/a/b/1" }]); - - assert.equal(result, '{"a":{"b":[1,3]}}'); - }); - - test("remove multiple properties", () => { - const source = '{"a":1,"b":2,"c":3,"d":4}'; - - const result = remove(source, [{ path: "b" }, { path: "d" }]); - - assert.equal(result, '{"a":1,"c":3}'); - }); - - test("remove with whitespace preservation", () => { - const source = '{ "a" : 1 , "b" : 2 }'; - - const result = remove(source, [{ path: "b" }]); - - assert.equal(result, '{ "a" : 1 }'); - }); - - test("remove non-existing property", () => { - const source = '{"a":1,"b":2}'; - - const result = remove(source, [{ path: "c" }]); - - assert.equal(result, '{"a":1,"b":2}'); - }); -}); - -// ===== INSERT OPERATION TESTS ===== -describe("Insert Operation Tests", () => { - test("insert property into object", () => { - const source = '{"a":1,"b":2}'; - - const result = insert(source, [{ path: "", key: "c", value: "3" }]); - - assert.equal(result, '{"a":1,"b":2, "c": 3}'); - }); - - test("insert into empty object", () => { - const source = "{}"; - - const result = insert(source, [{ path: "", key: "a", value: "1" }]); - - assert.equal(result, '{"a": 1}'); - }); - - test("insert element at start of array", () => { - const source = '{"arr":[2,3,4]}'; - - const result = insert(source, [{ path: "arr", position: 0, value: "1" }]); - - assert.equal(result, '{"arr":[1, 2,3,4]}'); - }); - - test("insert element at end of array", () => { - const source = '{"arr":[1,2,3]}'; - - const result = insert(source, [{ path: "arr", position: 3, value: "4" }]); - - assert.equal(result, '{"arr":[1,2,3, 4]}'); - }); - - test("insert element in middle of array", () => { - const source = '{"arr":[1,3,4]}'; - - const result = insert(source, [{ path: "arr", position: 1, value: "2" }]); - - assert.equal(result, '{"arr":[1,2, 3,4]}'); - }); - - test("insert into empty array", () => { - const source = '{"arr":[]}'; - - const result = insert(source, [{ path: "arr", position: 0, value: "1" }]); - - assert.equal(result, '{"arr":[1]}'); - }); - - test("insert without position appends to array", () => { - const source = '{"arr":[1,2,3]}'; - - const result = insert(source, [{ path: "arr", value: "4" }]); - - assert.equal(result, '{"arr":[1,2,3, 4]}'); - }); - - test("insert nested object property", () => { - const source = '{"a":{"b":1}}'; - - const result = insert(source, [{ path: "a", key: "c", value: "2" }]); - - assert.equal(result, '{"a":{"b":1, "c": 2}}'); - }); - - test("insert with JSON pointer", () => { - const source = '{"a":{"arr":[1,2]}}'; - - const result = insert(source, [{ path: "/a/arr", position: 1, value: "99" }]); - - assert.equal(result, '{"a":{"arr":[1,99, 2]}}'); - }); -}); - -// ===== BATCH OPERATION TESTS ===== -describe("Batch Operation Tests", () => { - test("batch with mixed operations", () => { - const source = '{"a": 1, "b": 2, "c": [1, 2, 3]}'; - - const result = batch(source, [ - { path: "a", value: "10" }, // Replace - { path: "b" }, // Delete - { path: "c", position: 1, value: "99" }, // Insert - ]); - - assert.equal(result, '{"a": 10, "c": [1, 99, 2, 3]}'); - }); - - test("batch with only replacements", () => { - const source = '{"x": 1, "y": 2}'; - - const result = batch(source, [ - { path: "x", value: "100" }, - { path: "y", value: "200" }, - ]); - - assert.equal(result, '{"x": 100, "y": 200}'); - }); - - test("batch with only deletions", () => { - const source = '{"a": 1, "b": 2, "c": 3}'; - - const result = batch(source, [{ path: "b" }]); - - assert.equal(result, '{"a": 1, "c": 3}'); - }); - - test("batch with only insertions", () => { - const source = '{"items": [1, 3]}'; - - const result = batch(source, [{ path: "items", position: 1, value: "2" }]); - - assert.equal(result, '{"items": [1, 2, 3]}'); - }); - - test("batch with object operations", () => { - const source = '{"user": {"name": "Alice", "age": 30}}'; - - const result = batch(source, [ - { path: "user.name", value: '"Bob"' }, // Replace - { path: "user.age" }, // Delete - { path: "user", key: "email", value: '"bob@example.com"' }, // Insert - ]); - - assert.equal(result, '{"user": {"name": "Bob", "email": "bob@example.com"}}'); - }); - - test("batch with nested operations", () => { - const source = '{"data": {"items": [1, 2, 3], "count": 3}}'; - - const result = batch(source, [ - { path: "data.count", value: "4" }, // Replace - { path: "data.items", position: 3, value: "4" }, // Insert - ]); - - assert.equal(result, '{"data": {"items": [1, 2, 3, 4], "count": 4}}'); - }); - - test("batch with empty patches array", () => { - const source = '{"a": 1}'; - - const result = batch(source, []); - - assert.equal(result, '{"a": 1}'); - }); - - test("batch preserves formatting", () => { - const source = `{ - "name": "Alice", - "age": 30, - "items": [1, 2] -}`; - - const result = batch(source, [ - { path: "age", value: "31" }, - { path: "items", position: 2, value: "3" }, - ]); - - const expected = `{ - "name": "Alice", - "age": 31, - "items": [1, 2, 3] -}`; - - assert.equal(result, expected); - }); - - test("batch with comments preserved", () => { - const source = `{ - // User info - "name": "Alice", - "age": 30 -}`; - - const result = batch(source, [ - { path: "age", value: "31" }, - { path: "", key: "email", value: '"alice@example.com"' }, - ]); - - assert(result.includes("// User info")); - assert(result.includes('"age": 31')); - assert(result.includes('"email": "alice@example.com"')); - }); -}); diff --git a/src/value-helpers.d.ts b/src/value-helpers.d.ts new file mode 100644 index 0000000..e931dad --- /dev/null +++ b/src/value-helpers.d.ts @@ -0,0 +1,20 @@ +/** + * Helper utility for formatting values to use in patches. + * This function makes it easier to create patch values without manually adding quotes. + */ + +/** + * Formats a JavaScript value into a JSON string representation for use in patches. + * Handles all JavaScript types including strings, numbers, booleans, null, objects, and arrays. + * + * @param value - The value to format + * @returns A JSON string representation + * @example + * formatValue(42) // "42" + * formatValue("hello") // '"hello"' + * formatValue(true) // "true" + * formatValue(null) // "null" + * formatValue({a: 1}) // '{"a":1}' + * formatValue([1, 2, 3]) // '[1,2,3]' + */ +export declare function formatValue(value: any): string; diff --git a/src/value-helpers.js b/src/value-helpers.js new file mode 100644 index 0000000..6ab5b8f --- /dev/null +++ b/src/value-helpers.js @@ -0,0 +1,22 @@ +/** + * Helper utility for formatting values to use in patches. + * This function makes it easier to create patch values without manually adding quotes. + */ + +/** + * Formats a JavaScript value into a JSON string representation for use in patches. + * Handles all JavaScript types including strings, numbers, booleans, null, objects, and arrays. + * + * @param {any} value - The value to format + * @returns {string} - A JSON string representation + * @example + * formatValue(42) // "42" + * formatValue("hello") // '"hello"' + * formatValue(true) // "true" + * formatValue(null) // "null" + * formatValue({a: 1}) // '{"a":1}' + * formatValue([1, 2, 3]) // '[1,2,3]' + */ +export function formatValue(value) { + return JSON.stringify(value); +} diff --git a/test/dist.test.js.snapshot b/test/dist.test.js.snapshot index cbde8c8..d84f8f6 100644 --- a/test/dist.test.js.snapshot +++ b/test/dist.test.js.snapshot @@ -3,18 +3,7 @@ exports[`test cjs output 1`] = ` > cjs@1.0.0 test > node index.cjs -{ - batch: [Function: batch], - default: { - replace: [Function: replace], - remove: [Function: remove], - insert: [Function: insert], - batch: [Function: batch] - }, - insert: [Function: insert], - remove: [Function: remove], - replace: [Function: replace] -} [Function: replace] +[Function: jsonmod] [Function: formatValue] `; @@ -23,11 +12,6 @@ exports[`test esm output 1`] = ` > esm@1.0.0 test > node index.mjs -{ - replace: [Function: replace], - remove: [Function: remove], - insert: [Function: insert], - batch: [Function: batch] -} [Function: replace] +[Function: jsonmod] [Function: formatValue] `; diff --git a/yarn.lock b/yarn.lock index a0aec14..a9d4961 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,54 +2,14 @@ # yarn lockfile v1 -"@ast-grep/napi-darwin-arm64@0.37.0": - version "0.37.0" - resolved "https://registry.npmjs.org/@ast-grep/napi-darwin-arm64/-/napi-darwin-arm64-0.37.0.tgz#5cca85937dbe9a75b7e89ddc0cea595111e2d13d" - integrity sha512-QAiIiaAbLvMEg/yBbyKn+p1gX2/FuaC0SMf7D7capm/oG4xGMzdeaQIcSosF4TCxxV+hIH4Bz9e4/u7w6Bnk3Q== - -"@ast-grep/napi-darwin-x64@0.37.0": - version "0.37.0" - resolved "https://registry.npmjs.org/@ast-grep/napi-darwin-x64/-/napi-darwin-x64-0.37.0.tgz#433c19fb2584c81b2e04b1900430b7b547908319" - integrity sha512-zvcvdgekd4ySV3zUbUp8HF5nk5zqwiMXTuVzTUdl/w08O7JjM6XPOIVT+d2o/MqwM9rsXdzdergY5oY2RdhSPA== - -"@ast-grep/napi-linux-arm64-gnu@0.37.0": - version "0.37.0" - resolved "https://registry.npmjs.org/@ast-grep/napi-linux-arm64-gnu/-/napi-linux-arm64-gnu-0.37.0.tgz#2d7ea5cb97560cb704ff4e73daf5b372335387bf" - integrity sha512-L7Sj0lXy8X+BqSMgr1LB8cCoWk0rericdeu+dC8/c8zpsav5Oo2IQKY1PmiZ7H8IHoFBbURLf8iklY9wsD+cyA== - -"@ast-grep/napi-linux-arm64-musl@0.37.0": - version "0.37.0" - resolved "https://registry.npmjs.org/@ast-grep/napi-linux-arm64-musl/-/napi-linux-arm64-musl-0.37.0.tgz#dc35c759c71f9a3431b6d3764ce50074a6071bef" - integrity sha512-LF9sAvYy6es/OdyJDO3RwkX3I82Vkfsng1sqUBcoWC1jVb1wX5YVzHtpQox9JrEhGl+bNp7FYxB4Qba9OdA5GA== - "@ast-grep/napi-linux-x64-gnu@0.37.0": version "0.37.0" resolved "https://registry.npmjs.org/@ast-grep/napi-linux-x64-gnu/-/napi-linux-x64-gnu-0.37.0.tgz" integrity sha512-TViz5/klqre6aSmJzswEIjApnGjJzstG/SE8VDWsrftMBMYt2PTu3MeluZVwzSqDao8doT/P+6U11dU05UOgxw== -"@ast-grep/napi-linux-x64-musl@0.37.0": - version "0.37.0" - resolved "https://registry.npmjs.org/@ast-grep/napi-linux-x64-musl/-/napi-linux-x64-musl-0.37.0.tgz#58f3d2b7461d23b4a998e8122e58705c555fc887" - integrity sha512-/BcCH33S9E3ovOAEoxYngUNXgb+JLg991sdyiNP2bSoYd30a9RHrG7CYwW6fMgua3ijQ474eV6cq9yZO1bCpXg== - -"@ast-grep/napi-win32-arm64-msvc@0.37.0": - version "0.37.0" - resolved "https://registry.npmjs.org/@ast-grep/napi-win32-arm64-msvc/-/napi-win32-arm64-msvc-0.37.0.tgz#15f330e317c017acb2d4b7862fe3c2b8c96eea15" - integrity sha512-TjQA4cFoIEW2bgjLkaL9yqT4XWuuLa5MCNd0VCDhGRDMNQ9+rhwi9eLOWRaap3xzT7g+nlbcEHL3AkVCD2+b3A== - -"@ast-grep/napi-win32-ia32-msvc@0.37.0": - version "0.37.0" - resolved "https://registry.npmjs.org/@ast-grep/napi-win32-ia32-msvc/-/napi-win32-ia32-msvc-0.37.0.tgz#2fa6802f79c627e59cdd6363f171917b624690f6" - integrity sha512-uNmVka8fJCdYsyOlF9aZqQMLTatEYBynjChVTzUfFMDfmZ0bihs/YTqJVbkSm8TZM7CUX82apvn50z/dX5iWRA== - -"@ast-grep/napi-win32-x64-msvc@0.37.0": - version "0.37.0" - resolved "https://registry.npmjs.org/@ast-grep/napi-win32-x64-msvc/-/napi-win32-x64-msvc-0.37.0.tgz#66007757f0831e36f5576839999fbd265210ed64" - integrity sha512-vCiFOT3hSCQuHHfZ933GAwnPzmL0G04JxQEsBRfqONywyT8bSdDc/ECpAfr3S9VcS4JZ9/F6tkePKW/Om2Dq2g== - "@ast-grep/napi@0.37.0": version "0.37.0" - resolved "https://registry.npmjs.org/@ast-grep/napi/-/napi-0.37.0.tgz#e6a4806dfbbe570f4742fd9d048bdcc87328b29e" + resolved "https://registry.npmjs.org/@ast-grep/napi/-/napi-0.37.0.tgz" integrity sha512-Hb4o6h1Pf6yRUAX07DR4JVY7dmQw+RVQMW5/m55GoiAT/VRoKCWBtIUPPOnqDVhbx1Cjfil9b6EDrgJsUAujEQ== optionalDependencies: "@ast-grep/napi-darwin-arm64" "0.37.0" @@ -64,7 +24,7 @@ "@emnapi/core@^1.5.0": version "1.7.1" - resolved "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz#3a79a02dbc84f45884a1806ebb98e5746bdfaac4" + resolved "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz" integrity sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg== dependencies: "@emnapi/wasi-threads" "1.1.0" @@ -72,26 +32,26 @@ "@emnapi/runtime@^1.5.0": version "1.7.1" - resolved "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz#a73784e23f5d57287369c808197288b52276b791" + resolved "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz" integrity sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA== dependencies: tslib "^2.4.0" "@emnapi/wasi-threads@1.1.0": version "1.1.0" - resolved "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf" + resolved "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz" integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ== dependencies: tslib "^2.4.0" "@module-federation/error-codes@0.21.4": version "0.21.4" - resolved "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.21.4.tgz#037c15c641e67c2c5465d2d3f442724395691f55" + resolved "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.21.4.tgz" integrity sha512-ClpL5MereWNXh+EgDjz7w4RrC1JlisQTvXDa1gLxpviHafzNDfdViVmuhi9xXVuj+EYo8KU70Y999KHhk9424Q== "@module-federation/runtime-core@0.21.4": version "0.21.4" - resolved "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.21.4.tgz#3254ee1be28255f9b115fe02d462672ae0f66584" + resolved "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.21.4.tgz" integrity sha512-SGpmoOLGNxZofpTOk6Lxb2ewaoz5wMi93AFYuuJB04HTVcngEK+baNeUZ2D/xewrqNIJoMY6f5maUjVfIIBPUA== dependencies: "@module-federation/error-codes" "0.21.4" @@ -99,7 +59,7 @@ "@module-federation/runtime-tools@0.21.4": version "0.21.4" - resolved "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.21.4.tgz#dbaf6d0a21ee72fc343f0d9dc50eeabb5fd9f5ba" + resolved "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.21.4.tgz" integrity sha512-RzFKaL0DIjSmkn76KZRfzfB6dD07cvID84950jlNQgdyoQFUGkqD80L6rIpVCJTY/R7LzR3aQjHnoqmq4JPo3w== dependencies: "@module-federation/runtime" "0.21.4" @@ -107,7 +67,7 @@ "@module-federation/runtime@0.21.4": version "0.21.4" - resolved "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.21.4.tgz#f07cc2dd9786b26d3af5440793693b953a818b97" + resolved "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.21.4.tgz" integrity sha512-wgvGqryurVEvkicufJmTG0ZehynCeNLklv8kIk5BLIsWYSddZAE+xe4xov1kgH5fIJQAoQNkRauFFjVNlHoAkA== dependencies: "@module-federation/error-codes" "0.21.4" @@ -116,26 +76,17 @@ "@module-federation/sdk@0.21.4": version "0.21.4" - resolved "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.21.4.tgz#4b4757b527d9a2758d01c5aadec7963348f43385" + resolved "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.21.4.tgz" integrity sha512-tzvhOh/oAfX++6zCDDxuvioHY4Jurf8vcfoCbKFxusjmyKr32GPbwFDazUP+OPhYCc3dvaa9oWU6X/qpUBLfJw== "@module-federation/webpack-bundler-runtime@0.21.4": version "0.21.4" - resolved "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.21.4.tgz#d8651c5bb8e7bc999300f32f0e11358a6acd0501" + resolved "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.21.4.tgz" integrity sha512-dusmR3uPnQh9u9ChQo3M+GLOuGFthfvnh7WitF/a1eoeTfRmXqnMFsXtZCUK+f/uXf+64874Zj/bhAgbBcVHZA== dependencies: "@module-federation/runtime" "0.21.4" "@module-federation/sdk" "0.21.4" -"@napi-rs/wasm-runtime@1.0.7", "@napi-rs/wasm-runtime@^1.0.7": - version "1.0.7" - resolved "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz#dcfea99a75f06209a235f3d941e3460a51e9b14c" - integrity sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw== - dependencies: - "@emnapi/core" "^1.5.0" - "@emnapi/runtime" "^1.5.0" - "@tybys/wasm-util" "^0.10.1" - "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" @@ -144,7 +95,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": +"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -157,106 +108,14 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@oxc-resolver/binding-android-arm-eabi@11.13.2": - version "11.13.2" - resolved "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.13.2.tgz#e309a5652787f25d65a7771c2f7608f8e4271d94" - integrity sha512-vWd1NEaclg/t2DtEmYzRRBNQOueMI8tixw/fSNZ9XETXLRJiAjQMYpYeflQdRASloGze6ZelHE/wIBNt4S+pkw== - -"@oxc-resolver/binding-android-arm64@11.13.2": - version "11.13.2" - resolved "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.13.2.tgz#bb1267f5c388462758be394d56dcc6ee62d0e39e" - integrity sha512-jxZrYcxgpI6IuQpguQVAQNrZfUyiYfMVqR4pKVU3PRLCM7AsfXNKp0TIgcvp+l6dYVdoZ1MMMMa5Ayjd09rNOw== - -"@oxc-resolver/binding-darwin-arm64@11.13.2": - version "11.13.2" - resolved "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.13.2.tgz#a2213156b6df07ace1c74faf95b7aa2cbeebe86f" - integrity sha512-RDS3HUe1FvgjNS1xfBUqiEJ8938Zb5r7iKABwxEblp3K4ufZZNAtoaHjdUH2TJ0THDmuf0OxxVUO/Y+4Ep4QfQ== - -"@oxc-resolver/binding-darwin-x64@11.13.2": - version "11.13.2" - resolved "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.13.2.tgz#bc28f62c5213633c21a513060289095bb48930da" - integrity sha512-tDcyWtkUzkt6auJLP2dOjL84BxqHkKW4mz2lNRIGPTq7b+HBraB+m8RdRH6BgqTvbnNECOxR3XAMaKBKC8J51g== - -"@oxc-resolver/binding-freebsd-x64@11.13.2": - version "11.13.2" - resolved "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.13.2.tgz#0800091260eda93e117dcde3843a480d2fa0f569" - integrity sha512-fpaeN8Q0kWvKns9uSMg6CcKo7cdgmWt6J91stPf8sdM+EKXzZ0YcRnWWyWF8SM16QcLUPCy5Iwt5Z8aYBGaZYA== - -"@oxc-resolver/binding-linux-arm-gnueabihf@11.13.2": - version "11.13.2" - resolved "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.13.2.tgz#7940c598e1ad29056d2046bd2843714d9b3e254b" - integrity sha512-idBgJU5AvSsGOeaIWiFBKbNBjpuduHsJmrG4CBbEUNW/Ykx+ISzcuj1PHayiYX6R9stVsRhj3d2PyymfC5KWRg== - -"@oxc-resolver/binding-linux-arm-musleabihf@11.13.2": - version "11.13.2" - resolved "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.13.2.tgz#05877080ab043caf49db4c3e72e4d1c4ea3e8552" - integrity sha512-BlBvQUhvvIM/7s96KlKhMk0duR2sj8T7Hyii46/5QnwfN/pHwobvOL5czZ6/SKrHNB/F/qDY4hGsBuB1y7xgTg== - -"@oxc-resolver/binding-linux-arm64-gnu@11.13.2": - version "11.13.2" - resolved "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.13.2.tgz#4af0671f403df7ec09c96989982ce6adcc111c1e" - integrity sha512-lUmDTmYOGpbIK+FBfZ0ySaQTo7g1Ia/WnDnQR2wi/0AtehZIg/ZZIgiT/fD0iRvKEKma612/0PVo8dXdAKaAGA== - -"@oxc-resolver/binding-linux-arm64-musl@11.13.2": - version "11.13.2" - resolved "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.13.2.tgz#da4888231605f3f6c587d97baef71d929356ad49" - integrity sha512-dkGzOxo+I9lA4Er6qzFgkFevl3JvwyI9i0T/PkOJHva04rb1p9dz8GPogTO9uMK4lrwLWzm/piAu+tHYC7v7+w== - -"@oxc-resolver/binding-linux-ppc64-gnu@11.13.2": - version "11.13.2" - resolved "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.13.2.tgz#357acf5020c5fe2175ab351e843c1fb0b81afb11" - integrity sha512-53kWsjLkVFnoSA7COdps38pBssN48zI8LfsOvupsmQ0/4VeMYb+0Ao9O6r52PtmFZsGB3S1Qjqbjl/Pswj1a3g== - -"@oxc-resolver/binding-linux-riscv64-gnu@11.13.2": - version "11.13.2" - resolved "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.13.2.tgz#52011bfcc931b2cf4fb744fa5253569b72141dad" - integrity sha512-MfxN6DMpvmdCbGlheJ+ihy11oTcipqDfcEIQV9ah3FGXBRCZtBOHJpQDk8qI2Y+nCXVr3Nln7OSsOzoC4+rSYQ== - -"@oxc-resolver/binding-linux-riscv64-musl@11.13.2": - version "11.13.2" - resolved "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.13.2.tgz#203eeaddb4149d36c48c132f53175963b10be151" - integrity sha512-WXrm4YiRU0ijqb72WHSjmfYaQZ7t6/kkQrFc4JtU+pUE4DZA/DEdxOuQEd4Q43VqmLvICTJWSaZMlCGQ4PSRUg== - -"@oxc-resolver/binding-linux-s390x-gnu@11.13.2": - version "11.13.2" - resolved "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.13.2.tgz#75e2473e961f17e509d975e2a4c85d9f21b64461" - integrity sha512-4pISWIlOFRUhWyvGCB3XUhtcwyvwGGhlXhHz7IXCXuGufaQtvR05trvw8U1ZnaPhsdPBkRhOMIedX11ayi5uXw== - "@oxc-resolver/binding-linux-x64-gnu@11.13.2": version "11.13.2" resolved "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.13.2.tgz" integrity sha512-DVo6jS8n73yNAmCsUOOk2vBeC60j2RauDXQM8p7RDl0afsEaA2le22vD8tky7iNoM5tsxfBmE4sOJXEKgpwWRw== -"@oxc-resolver/binding-linux-x64-musl@11.13.2": - version "11.13.2" - resolved "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.13.2.tgz#ff801da3ff949efc6e486d53225c5ca562ffc947" - integrity sha512-6WqrE+hQBFP35KdwQjWcZpldbTq6yJmuTVThISu+rY3+j6MaDp2ciLHTr1X68r2H/7ocOIl4k3NnOVIzeRJE3w== - -"@oxc-resolver/binding-wasm32-wasi@11.13.2": - version "11.13.2" - resolved "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.13.2.tgz#c5b2f0a821899a46ff1eac0b66911028070eb3c4" - integrity sha512-YpxvQmP2D+mNUkLQZbBjGz20g/pY8XoOBdPPoWMl9X68liFFjXxkPQTrZxWw4zzG/UkTM5z6dPRTyTePRsMcjw== - dependencies: - "@napi-rs/wasm-runtime" "^1.0.7" - -"@oxc-resolver/binding-win32-arm64-msvc@11.13.2": - version "11.13.2" - resolved "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.13.2.tgz#20ff151cca5b7b87f9070b6019c1ebd121672249" - integrity sha512-1SKBw6KcCmvPBdEw1/Qdpv6eSDf23lCXTWz9VxTe6QUQ/1wR+HZR2uS4q6C8W6jnIswMTQbxpTvVwdRXl+ufeA== - -"@oxc-resolver/binding-win32-ia32-msvc@11.13.2": - version "11.13.2" - resolved "https://registry.npmjs.org/@oxc-resolver/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-11.13.2.tgz#96788e52bd32f317b35f4d4a1c87d1e86e3509f7" - integrity sha512-KEVV7wggDucxRn3vvyHnmTCPXoCT7vWpH18UVLTygibHJvNRP2zl5lBaQcCIdIaYYZjKt1aGI/yZqxZvHoiCdg== - -"@oxc-resolver/binding-win32-x64-msvc@11.13.2": - version "11.13.2" - resolved "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.13.2.tgz#14fa5ce6d8065d6b5c5ab31155f9f8ebc188b0f5" - integrity sha512-6AAdN9v/wO5c3td1yidgNLKYlzuNgfOtEqBq60WE469bJWR7gHgG/S5aLR2pH6/gyPLs9UXtItxi934D+0Estg== - -"@rsbuild/core@~1.6.6": +"@rsbuild/core@~1.6.6", "@rsbuild/core@1.x": version "1.6.6" - resolved "https://registry.npmjs.org/@rsbuild/core/-/core-1.6.6.tgz#a2a277d33b648a5f055dc629afc12c0183b8e4b7" + resolved "https://registry.npmjs.org/@rsbuild/core/-/core-1.6.6.tgz" integrity sha512-QE1MvRFKDeeQUAwZrCPhEHgvy/XieYQj0aPho1SkkL/M4ruonp/p8ymhUJZE5wFQxIhBHaOvE2gwKnME0XQgKg== dependencies: "@rspack/core" "1.6.3" @@ -267,67 +126,20 @@ "@rslib/core@^0.18.0": version "0.18.0" - resolved "https://registry.npmjs.org/@rslib/core/-/core-0.18.0.tgz#dd87b9b15dce6add4323783cf8a7bf23bf33dc1a" + resolved "https://registry.npmjs.org/@rslib/core/-/core-0.18.0.tgz" integrity sha512-ZLonxKnef5Hx4zZj2ZckicVnM0KwTP9x8XcNnZBuXo82maXVmSSERtvMrB7aufxlFjqEThOHG6RNuLZToM1eFQ== dependencies: "@rsbuild/core" "~1.6.6" rsbuild-plugin-dts "0.18.0" -"@rspack/binding-darwin-arm64@1.6.3": - version "1.6.3" - resolved "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.6.3.tgz#b7b342935a87cab9ff1716da20b7766c71622472" - integrity sha512-GxjrB5RhxlEoX3uoWtzNPcINPOn6hzqhn00Y164gofwQ6KgvtEJU7DeYXgCq4TQDD1aQbF/lsV1wpzb2LMkQdg== - -"@rspack/binding-darwin-x64@1.6.3": - version "1.6.3" - resolved "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.6.3.tgz#61dc8ed8eec8e76a9c01c9d31041be19e8b89af3" - integrity sha512-X6TEPwc+FeApTgnzBefc/viuUP7LkqTY1GxltRYuabs8E7bExlmYoyB8KhIlC66NWtgjmcNWvZIkUlr9ZalBkQ== - -"@rspack/binding-linux-arm64-gnu@1.6.3": - version "1.6.3" - resolved "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.6.3.tgz#e0a5a84988c3bde8d8e57698ef0b98140f850263" - integrity sha512-uid2GjLzRnYNzNuTTS/hUZdYO6bNATWfaeuhGBU8RWrRgB+clJwhZskSwhfVrvmyTXYbHI95CJIPt4TbZ1FRTg== - -"@rspack/binding-linux-arm64-musl@1.6.3": - version "1.6.3" - resolved "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.6.3.tgz#a77095a31f3fe898b20e97b5ca8e94f2569d935b" - integrity sha512-ZJqqyEARBAnv9Gj3+0/PGIw87r8Vg0ZEKiRT9u5tPKK01dptF+xGv4xywAlahOeFUik4Dni5aHixbarStzN9Cw== - "@rspack/binding-linux-x64-gnu@1.6.3": version "1.6.3" - resolved "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.6.3.tgz#fe779d69754ec5e48c144035e03dc2cfb5524b23" + resolved "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.6.3.tgz" integrity sha512-/W8/X3CBGVY7plii5eUzyIEyCKiYx1lqrSVuD1HLlVHvzC4H2Kpk0EwvY2gUhnQRLU0Ym77Sh4PRd1ZOOzP4LQ== -"@rspack/binding-linux-x64-musl@1.6.3": - version "1.6.3" - resolved "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.6.3.tgz#6b6cc621e1b3a552afa8dfe2389aa25eac522a2d" - integrity sha512-h0Q3aM0fkRCd330DfRGZ9O3nk/rfRyXRX4dEIoLcLAq34VOmp3HZUP7rEy7feiJbuU4Atcvd0MD7U6RLwa1umQ== - -"@rspack/binding-wasm32-wasi@1.6.3": - version "1.6.3" - resolved "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.6.3.tgz#0d897dccdc54b6484c289f36ff99246742e29008" - integrity sha512-XLCDe+b52kAajlHutsyfh9o+uKQvgis+rLFb3XIJ9FfCcL8opTWVyeGLNHBUBn7cGPXGEYWd0EU9CZJrjV+iVw== - dependencies: - "@napi-rs/wasm-runtime" "1.0.7" - -"@rspack/binding-win32-arm64-msvc@1.6.3": - version "1.6.3" - resolved "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.6.3.tgz#125f0272850ce3f7af4935b58711f9a031b9658d" - integrity sha512-BU3VjyzAf8noYqb7NPuUZu9VVHRH2b+x4Q5A2oqQwEq4JzW/Mrhcd//vnRpSE9HHuezxTpQTtSSsB/YqV7BkDg== - -"@rspack/binding-win32-ia32-msvc@1.6.3": - version "1.6.3" - resolved "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.6.3.tgz#71546543f80156ddbaf6cec495489b267bbcfb98" - integrity sha512-W2yHUFra9N8QbBKQC6PcyOwOJbj8qrmechK97XVQAwo0GWGnQKMphivJrbxHOxCz89FGn9kLGRakTH04bHT4MQ== - -"@rspack/binding-win32-x64-msvc@1.6.3": - version "1.6.3" - resolved "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.6.3.tgz#101c7b1cd670f956e0faeb059b4f273c5b1d70bb" - integrity sha512-mxep+BqhySoWweQSXnUaYAHx+C8IzOTNMJYuAVchXn9bMG6SPAXvZqAF8X/Q+kNg8X7won8Sjz+O+OUw3OTyOQ== - "@rspack/binding@1.6.3": version "1.6.3" - resolved "https://registry.npmjs.org/@rspack/binding/-/binding-1.6.3.tgz#1f02b0320030041e9ea1af974a572f6770902d67" + resolved "https://registry.npmjs.org/@rspack/binding/-/binding-1.6.3.tgz" integrity sha512-liRgxMjHWDL225c41pH4ZcFtPN48LM0+St3iylwavF5JFSqBv86R/Cv5+M+WLrhcihCQsxDwBofipyosJIFmmA== optionalDependencies: "@rspack/binding-darwin-arm64" "1.6.3" @@ -343,19 +155,19 @@ "@rspack/core@1.6.3": version "1.6.3" - resolved "https://registry.npmjs.org/@rspack/core/-/core-1.6.3.tgz#9ed170e72e821afb11cf82ff41b24e43a5de1916" + resolved "https://registry.npmjs.org/@rspack/core/-/core-1.6.3.tgz" integrity sha512-03pyxRtpZ9SNwuA4XHLcFG/jmmWqSd4NaXQGrwOHU0UoPKpVPTqkxtQYZLCfeNtDfAA9v2KPqgJ3b40x8nJGeA== dependencies: "@module-federation/runtime-tools" "0.21.4" "@rspack/binding" "1.6.3" "@rspack/lite-tapable" "1.1.0" -"@rspack/lite-tapable@1.1.0", "@rspack/lite-tapable@~1.1.0": +"@rspack/lite-tapable@~1.1.0", "@rspack/lite-tapable@1.1.0": version "1.1.0" - resolved "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.1.0.tgz#3cfdafeed01078e116bd4f191b684c8b484de425" + resolved "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.1.0.tgz" integrity sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw== -"@swc/helpers@^0.5.17": +"@swc/helpers@^0.5.17", "@swc/helpers@>=0.5.1": version "0.5.17" resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz" integrity sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A== @@ -364,12 +176,12 @@ "@tybys/wasm-util@^0.10.1": version "0.10.1" - resolved "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414" + resolved "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz" integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg== dependencies: tslib "^2.4.0" -"@types/node@^22.14.1": +"@types/node@^22.14.1", "@types/node@>=18": version "22.19.1" resolved "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz" integrity sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ== @@ -390,7 +202,7 @@ braces@^3.0.3: core-js@~3.46.0: version "3.46.0" - resolved "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz#323a092b96381a9184d0cd49ee9083b2f93373bb" + resolved "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz" integrity sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA== fast-glob@^3.3.3: @@ -556,7 +368,7 @@ reusify@^1.0.4: rsbuild-plugin-dts@0.18.0: version "0.18.0" - resolved "https://registry.npmjs.org/rsbuild-plugin-dts/-/rsbuild-plugin-dts-0.18.0.tgz#213a805b7bd7d0555a086d30878535ebee4f1fad" + resolved "https://registry.npmjs.org/rsbuild-plugin-dts/-/rsbuild-plugin-dts-0.18.0.tgz" integrity sha512-GzzcnYDoILabBddQH5wMQK93peZbHXnoF9wtFz0INRotAS2xIwBb1rIeFRpfn/fgXfSlFu4C8QbqD6U8PgXzJA== dependencies: "@ast-grep/napi" "0.37.0" @@ -587,10 +399,10 @@ to-regex-range@^5.0.1: tslib@^2.4.0, tslib@^2.8.0: version "2.8.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== -typescript@^5.9.3: +typescript@^5, typescript@^5.9.3, "typescript@>=5.0.4 <7": version "5.9.3" resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz" integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==