mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Add Bun.JSONC API for parsing JSON with comments and trailing commas (#22115)
## Summary This PR implements a new `Bun.JSONC.parse()` API that allows parsing JSONC (JSON with Comments) files. It addresses the feature request from issue #16257 by providing a native API for parsing JSON with comments and trailing commas. The implementation follows the same pattern as `Bun.YAML` and `Bun.TOML`, leveraging the existing `TSConfigParser` which already handles JSONC parsing internally. ## Features - **Parse JSON with comments**: Supports both `//` single-line and `/* */` block comments - **Handle trailing commas**: Works with trailing commas in objects and arrays - **Full JavaScript object conversion**: Returns native JavaScript objects/arrays - **Error handling**: Proper error throwing for invalid JSON - **TypeScript compatibility**: Works with TypeScript config files and other JSONC formats ## Usage Example ```javascript const result = Bun.JSONC.parse(`{ // This is a comment "name": "my-app", "version": "1.0.0", // trailing comma is allowed "dependencies": { "react": "^18.0.0", }, }`); // Returns native JavaScript object ``` ## Implementation Details - Created `JSONCObject.zig` following the same pattern as `YAMLObject.zig` and `TOMLObject.zig` - Uses the existing `TSConfigParser` from `json.zig` which already handles comments and trailing commas - Added proper C++ bindings and exports following Bun's established patterns - Comprehensive test suite covering various JSONC features ## Test Plan - [x] Basic JSON parsing works - [x] Single-line comments (`//`) are handled correctly - [x] Block comments (`/* */`) are handled correctly - [x] Trailing commas in objects and arrays work - [x] Complex nested structures parse correctly - [x] Error handling for invalid JSON - [x] Empty objects and arrays work - [x] Boolean and null values work correctly 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com> Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com>
This commit is contained in:
27
packages/bun-types/bun.d.ts
vendored
27
packages/bun-types/bun.d.ts
vendored
@@ -625,6 +625,33 @@ declare module "bun" {
|
||||
export function parse(input: string): object;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSONC related APIs
|
||||
*/
|
||||
namespace JSONC {
|
||||
/**
|
||||
* Parse a JSONC (JSON with Comments) string into a JavaScript value.
|
||||
*
|
||||
* Supports both single-line (`//`) and block comments (`/* ... *\/`), as well as
|
||||
* trailing commas in objects and arrays.
|
||||
*
|
||||
* @category Utilities
|
||||
*
|
||||
* @param input The JSONC string to parse
|
||||
* @returns A JavaScript value
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* const result = Bun.JSONC.parse(`{
|
||||
* // This is a comment
|
||||
* "name": "my-app",
|
||||
* "version": "1.0.0", // trailing comma is allowed
|
||||
* }`);
|
||||
* ```
|
||||
*/
|
||||
export function parse(input: string): unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* YAML related APIs
|
||||
*/
|
||||
|
||||
@@ -26,8 +26,9 @@ pub const SocketHandlers = @import("./api/bun/socket.zig").Handlers;
|
||||
pub const Subprocess = @import("./api/bun/subprocess.zig");
|
||||
pub const Terminal = @import("./api/bun/Terminal.zig");
|
||||
pub const HashObject = @import("./api/HashObject.zig");
|
||||
pub const UnsafeObject = @import("./api/UnsafeObject.zig");
|
||||
pub const JSONCObject = @import("./api/JSONCObject.zig");
|
||||
pub const TOMLObject = @import("./api/TOMLObject.zig");
|
||||
pub const UnsafeObject = @import("./api/UnsafeObject.zig");
|
||||
pub const YAMLObject = @import("./api/YAMLObject.zig");
|
||||
pub const Timer = @import("./api/Timer.zig");
|
||||
pub const FFIObject = @import("./api/FFIObject.zig");
|
||||
|
||||
@@ -62,6 +62,7 @@ pub const BunObject = struct {
|
||||
pub const SHA384 = toJSLazyPropertyCallback(Crypto.SHA384.getter);
|
||||
pub const SHA512 = toJSLazyPropertyCallback(Crypto.SHA512.getter);
|
||||
pub const SHA512_256 = toJSLazyPropertyCallback(Crypto.SHA512_256.getter);
|
||||
pub const JSONC = toJSLazyPropertyCallback(Bun.getJSONCObject);
|
||||
pub const TOML = toJSLazyPropertyCallback(Bun.getTOMLObject);
|
||||
pub const YAML = toJSLazyPropertyCallback(Bun.getYAMLObject);
|
||||
pub const Transpiler = toJSLazyPropertyCallback(Bun.getTranspilerConstructor);
|
||||
@@ -127,7 +128,7 @@ pub const BunObject = struct {
|
||||
@export(&BunObject.SHA384, .{ .name = lazyPropertyCallbackName("SHA384") });
|
||||
@export(&BunObject.SHA512, .{ .name = lazyPropertyCallbackName("SHA512") });
|
||||
@export(&BunObject.SHA512_256, .{ .name = lazyPropertyCallbackName("SHA512_256") });
|
||||
|
||||
@export(&BunObject.JSONC, .{ .name = lazyPropertyCallbackName("JSONC") });
|
||||
@export(&BunObject.TOML, .{ .name = lazyPropertyCallbackName("TOML") });
|
||||
@export(&BunObject.YAML, .{ .name = lazyPropertyCallbackName("YAML") });
|
||||
@export(&BunObject.Glob, .{ .name = lazyPropertyCallbackName("Glob") });
|
||||
@@ -1261,6 +1262,9 @@ pub fn getHashObject(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSVa
|
||||
return HashObject.create(globalThis);
|
||||
}
|
||||
|
||||
pub fn getJSONCObject(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSValue {
|
||||
return JSONCObject.create(globalThis);
|
||||
}
|
||||
pub fn getTOMLObject(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSValue {
|
||||
return TOMLObject.create(globalThis);
|
||||
}
|
||||
@@ -2058,6 +2062,7 @@ const gen = bun.gen.BunObject;
|
||||
const api = bun.api;
|
||||
const FFIObject = bun.api.FFIObject;
|
||||
const HashObject = bun.api.HashObject;
|
||||
const JSONCObject = bun.api.JSONCObject;
|
||||
const TOMLObject = bun.api.TOMLObject;
|
||||
const UnsafeObject = bun.api.UnsafeObject;
|
||||
const YAMLObject = bun.api.YAMLObject;
|
||||
|
||||
55
src/bun.js/api/JSONCObject.zig
Normal file
55
src/bun.js/api/JSONCObject.zig
Normal file
@@ -0,0 +1,55 @@
|
||||
pub fn create(globalThis: *jsc.JSGlobalObject) jsc.JSValue {
|
||||
const object = JSValue.createEmptyObject(globalThis, 1);
|
||||
object.put(
|
||||
globalThis,
|
||||
ZigString.static("parse"),
|
||||
jsc.JSFunction.create(
|
||||
globalThis,
|
||||
"parse",
|
||||
parse,
|
||||
1,
|
||||
.{},
|
||||
),
|
||||
);
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
pub fn parse(
|
||||
globalThis: *jsc.JSGlobalObject,
|
||||
callframe: *jsc.CallFrame,
|
||||
) bun.JSError!jsc.JSValue {
|
||||
var arena = bun.ArenaAllocator.init(globalThis.allocator());
|
||||
const allocator = arena.allocator();
|
||||
defer arena.deinit();
|
||||
var log = logger.Log.init(default_allocator);
|
||||
defer log.deinit();
|
||||
const input_value = callframe.argument(0);
|
||||
if (input_value.isEmptyOrUndefinedOrNull()) {
|
||||
return globalThis.throwInvalidArguments("Expected a string to parse", .{});
|
||||
}
|
||||
|
||||
var input_slice = try input_value.toSlice(globalThis, bun.default_allocator);
|
||||
defer input_slice.deinit();
|
||||
const source = &logger.Source.initPathString("input.jsonc", input_slice.slice());
|
||||
const parse_result = json.parseTSConfig(source, &log, allocator, true) catch {
|
||||
return globalThis.throwValue(try log.toJS(globalThis, default_allocator, "Failed to parse JSONC"));
|
||||
};
|
||||
|
||||
return parse_result.toJS(allocator, globalThis) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
error.JSError => return error.JSError,
|
||||
error.JSTerminated => return error.JSTerminated,
|
||||
// JSONC parsing does not produce macros or identifiers
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
const bun = @import("bun");
|
||||
const default_allocator = bun.default_allocator;
|
||||
const logger = bun.logger;
|
||||
const json = bun.interchange.json;
|
||||
|
||||
const jsc = bun.jsc;
|
||||
const JSValue = jsc.JSValue;
|
||||
const ZigString = jsc.ZigString;
|
||||
@@ -8,6 +8,7 @@
|
||||
macro(FFI) \
|
||||
macro(FileSystemRouter) \
|
||||
macro(Glob) \
|
||||
macro(JSONC) \
|
||||
macro(MD4) \
|
||||
macro(MD5) \
|
||||
macro(S3Client) \
|
||||
|
||||
@@ -727,6 +727,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
|
||||
SHA384 BunObject_lazyPropCb_wrap_SHA384 DontDelete|PropertyCallback
|
||||
SHA512 BunObject_lazyPropCb_wrap_SHA512 DontDelete|PropertyCallback
|
||||
SHA512_256 BunObject_lazyPropCb_wrap_SHA512_256 DontDelete|PropertyCallback
|
||||
JSONC BunObject_lazyPropCb_wrap_JSONC DontDelete|PropertyCallback
|
||||
TOML BunObject_lazyPropCb_wrap_TOML DontDelete|PropertyCallback
|
||||
YAML BunObject_lazyPropCb_wrap_YAML DontDelete|PropertyCallback
|
||||
Transpiler BunObject_lazyPropCb_wrap_Transpiler DontDelete|PropertyCallback
|
||||
|
||||
125
test/js/bun/jsonc/jsonc.test.ts
Normal file
125
test/js/bun/jsonc/jsonc.test.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
test("Bun.JSONC exists", () => {
|
||||
expect(Bun.JSONC).toBeDefined();
|
||||
expect(typeof Bun.JSONC).toBe("object");
|
||||
expect(typeof Bun.JSONC.parse).toBe("function");
|
||||
});
|
||||
|
||||
test("Bun.JSONC.parse handles basic JSON", () => {
|
||||
const result = Bun.JSONC.parse('{"name": "test", "value": 42}');
|
||||
expect(result).toEqual({ name: "test", value: 42 });
|
||||
});
|
||||
|
||||
test("Bun.JSONC.parse handles comments", () => {
|
||||
const jsonc = `{
|
||||
// This is a comment
|
||||
"name": "test",
|
||||
/* This is a block comment */
|
||||
"value": 42
|
||||
}`;
|
||||
|
||||
const result = Bun.JSONC.parse(jsonc);
|
||||
expect(result).toEqual({ name: "test", value: 42 });
|
||||
});
|
||||
|
||||
test("Bun.JSONC.parse handles trailing commas", () => {
|
||||
const jsonc = `{
|
||||
"name": "test",
|
||||
"value": 42,
|
||||
}`;
|
||||
|
||||
const result = Bun.JSONC.parse(jsonc);
|
||||
expect(result).toEqual({ name: "test", value: 42 });
|
||||
});
|
||||
|
||||
test("Bun.JSONC.parse handles arrays with trailing commas", () => {
|
||||
const jsonc = `[
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
]`;
|
||||
|
||||
const result = Bun.JSONC.parse(jsonc);
|
||||
expect(result).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test("Bun.JSONC.parse handles complex JSONC", () => {
|
||||
const jsonc = `{
|
||||
// Configuration object
|
||||
"name": "my-app",
|
||||
"version": "1.0.0",
|
||||
/* Dependencies section */
|
||||
"dependencies": {
|
||||
"react": "^18.0.0",
|
||||
"typescript": "^5.0.0", // Latest TypeScript
|
||||
},
|
||||
"scripts": [
|
||||
"build",
|
||||
"test",
|
||||
"lint", // Code formatting
|
||||
],
|
||||
}`;
|
||||
|
||||
const result = Bun.JSONC.parse(jsonc);
|
||||
expect(result).toEqual({
|
||||
name: "my-app",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
react: "^18.0.0",
|
||||
typescript: "^5.0.0",
|
||||
},
|
||||
scripts: ["build", "test", "lint"],
|
||||
});
|
||||
});
|
||||
|
||||
test("Bun.JSONC.parse handles nested objects", () => {
|
||||
const jsonc = `{
|
||||
"outer": {
|
||||
// Nested comment
|
||||
"inner": {
|
||||
"value": 123,
|
||||
}
|
||||
},
|
||||
}`;
|
||||
|
||||
const result = Bun.JSONC.parse(jsonc);
|
||||
expect(result).toEqual({
|
||||
outer: {
|
||||
inner: {
|
||||
value: 123,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("Bun.JSONC.parse handles boolean and null values", () => {
|
||||
const jsonc = `{
|
||||
"enabled": true, // Boolean true
|
||||
"disabled": false, // Boolean false
|
||||
"nothing": null, // Null value
|
||||
}`;
|
||||
|
||||
const result = Bun.JSONC.parse(jsonc);
|
||||
expect(result).toEqual({
|
||||
enabled: true,
|
||||
disabled: false,
|
||||
nothing: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("Bun.JSONC.parse throws on invalid JSON", () => {
|
||||
expect(() => {
|
||||
Bun.JSONC.parse("{ invalid json }");
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
test("Bun.JSONC.parse handles empty object", () => {
|
||||
const result = Bun.JSONC.parse("{}");
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
test("Bun.JSONC.parse handles empty array", () => {
|
||||
const result = Bun.JSONC.parse("[]");
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
Reference in New Issue
Block a user