From 9b899adbc35cc000c2aa5686f7a05d930d482f59 Mon Sep 17 00:00:00 2001 From: eden <198768181+lcweden@users.noreply.github.com> Date: Sat, 23 May 2026 07:39:11 +0000 Subject: [PATCH 1/5] fix: drop allowImportingTsExtensions and .ts from #src/ imports --- deno.json | 11 ++++++-- src/api/decoder.ts | 12 ++++----- src/api/encoder.ts | 10 +++---- src/index.ts | 34 ++++++++++++------------ src/libs/stream-decoder.ts | 8 +++--- src/libs/stream-encoder.ts | 8 +++--- src/libs/stream-line.ts | 8 +++--- src/libs/stream-selector.ts | 14 +++++----- src/modules/automaton.ts | 6 ++--- src/modules/decoder.ts | 24 ++++++++--------- src/modules/encoder.ts | 20 +++++++------- src/modules/entry.ts | 4 +-- src/modules/path.ts | 8 +++--- src/modules/state.ts | 10 +++---- src/modules/token.ts | 8 +++--- src/modules/value.ts | 14 +++++----- src/types/kind.ts | 2 +- src/types/path.ts | 2 +- src/utils/kind.ts | 4 +-- src/utils/wire.ts | 4 +-- tests/e2e/correctness.test.ts | 4 +-- tests/e2e/query.test.ts | 4 +-- tests/e2e/round-trip.test.ts | 4 +-- tests/e2e/streaming.test.ts | 2 +- tests/integration/decoder.test.ts | 8 +++--- tests/integration/encoder.test.ts | 10 +++---- tests/integration/stream-decoder.test.ts | 6 ++--- tests/integration/stream-encoder.test.ts | 6 ++--- tests/integration/stream-line.test.ts | 4 +-- tests/path.test.ts | 4 +-- tests/state.test.ts | 4 +-- tests/token.test.ts | 6 ++--- tests/value.test.ts | 6 ++--- tests/wire.test.ts | 4 +-- tsconfig.json | 1 - vite.config.ts | 6 ++--- 36 files changed, 147 insertions(+), 143 deletions(-) diff --git a/deno.json b/deno.json index c9b63a2..2d14b30 100644 --- a/deno.json +++ b/deno.json @@ -15,8 +15,15 @@ }, "exports": { ".": "./src/index.ts" }, "fmt": { "lineWidth": 100 }, - "lint": { "rules": { "tags": ["recommended"], "include": ["no-slow-types"] } }, - "exclude": ["node_modules/", "public/", "dist/", "vite.config.ts"], + "lint": { + "rules": { + "tags": ["recommended"], + "include": ["no-slow-types"], + "exclude": ["no-sloppy-imports"] + } + }, + "unstable": ["sloppy-imports"], + "exclude": ["node_modules/", "public/", "dist/"], "publish": { "include": ["src/", "README.md", "LICENSE", "deno.json"], "exclude": ["tests/", ".github/", ".vscode/", "dist/", "public/"] diff --git a/src/api/decoder.ts b/src/api/decoder.ts index c9445d3..89f2fda 100644 --- a/src/api/decoder.ts +++ b/src/api/decoder.ts @@ -1,9 +1,9 @@ -import { DEFAULT_DECODER_OPTIONS } from "#src/common/constants.ts"; -import Decoder from "#src/modules/decoder.ts"; -import type Token from "#src/modules/token.ts"; -import type Value from "#src/modules/value.ts"; -import type { Kind } from "#src/types/kind.ts"; -import type { DecoderOptions } from "#src/types/options.ts"; +import { DEFAULT_DECODER_OPTIONS } from "#src/common/constants"; +import Decoder from "#src/modules/decoder"; +import type Token from "#src/modules/token"; +import type Value from "#src/modules/value"; +import type { Kind } from "#src/types/kind"; +import type { DecoderOptions } from "#src/types/options"; type JSONTextDecoderOptions = DecoderOptions; diff --git a/src/api/encoder.ts b/src/api/encoder.ts index d20c210..609938f 100644 --- a/src/api/encoder.ts +++ b/src/api/encoder.ts @@ -1,8 +1,8 @@ -import { DEFAULT_ENCODER_OPTIONS } from "#src/common/constants.ts"; -import Encoder from "#src/modules/encoder.ts"; -import type Token from "#src/modules/token.ts"; -import type Value from "#src/modules/value.ts"; -import type { EncoderOptions } from "#src/types/options.ts"; +import { DEFAULT_ENCODER_OPTIONS } from "#src/common/constants"; +import Encoder from "#src/modules/encoder"; +import type Token from "#src/modules/token"; +import type Value from "#src/modules/value"; +import type { EncoderOptions } from "#src/types/options"; type JSONTextEncoderOptions = EncoderOptions; diff --git a/src/index.ts b/src/index.ts index 04fcc5f..842ff66 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,21 @@ -export type { JSONTextDecoderOptions } from "#src/api/decoder.ts"; -export type { JSONTextEncoderOptions } from "#src/api/encoder.ts"; -export type { JSONTextDecoderStreamOptions } from "#src/libs/stream-decoder.ts"; -export type { JSONTextEncoderStreamOptions } from "#src/libs/stream-encoder.ts"; -export type { JSONTextLineStreamOptions } from "#src/libs/stream-line.ts"; -export type { JSONTextSelectorStreamOptions } from "#src/libs/stream-selector.ts"; +export type { JSONTextDecoderOptions } from "#src/api/decoder"; +export type { JSONTextEncoderOptions } from "#src/api/encoder"; +export type { JSONTextDecoderStreamOptions } from "#src/libs/stream-decoder"; +export type { JSONTextEncoderStreamOptions } from "#src/libs/stream-encoder"; +export type { JSONTextLineStreamOptions } from "#src/libs/stream-line"; +export type { JSONTextSelectorStreamOptions } from "#src/libs/stream-selector"; -export type { Kind } from "#src/types/kind.ts"; +export type { Kind } from "#src/types/kind"; -export { default as JSONTextDecoder } from "#src/api/decoder.ts"; -export { default as JSONTextEncoder } from "#src/api/encoder.ts"; -export { default as Token } from "#src/modules/token.ts"; -export { default as Value } from "#src/modules/value.ts"; +export { default as JSONTextDecoder } from "#src/api/decoder"; +export { default as JSONTextEncoder } from "#src/api/encoder"; +export { default as Token } from "#src/modules/token"; +export { default as Value } from "#src/modules/value"; -export { default as JSONTextDecoderStream } from "#src/libs/stream-decoder.ts"; -export { default as JSONTextEncoderStream } from "#src/libs/stream-encoder.ts"; -export { default as JSONTextLineStream } from "#src/libs/stream-line.ts"; -export { default as JSONTextSelectorStream } from "#src/libs/stream-selector.ts"; +export { default as JSONTextDecoderStream } from "#src/libs/stream-decoder"; +export { default as JSONTextEncoderStream } from "#src/libs/stream-encoder"; +export { default as JSONTextLineStream } from "#src/libs/stream-line"; +export { default as JSONTextSelectorStream } from "#src/libs/stream-selector"; -export { KIND } from "#src/common/constants.ts"; -export { SyntacticError } from "#src/common/errors.ts"; +export { KIND } from "#src/common/constants"; +export { SyntacticError } from "#src/common/errors"; diff --git a/src/libs/stream-decoder.ts b/src/libs/stream-decoder.ts index 5f4e589..da47942 100644 --- a/src/libs/stream-decoder.ts +++ b/src/libs/stream-decoder.ts @@ -1,7 +1,7 @@ -import { DEFAULT_DECODER_OPTIONS } from "#src/common/constants.ts"; -import Decoder from "#src/modules/decoder.ts"; -import type Token from "#src/modules/token.ts"; -import type { DecoderOptions } from "#src/types/options.ts"; +import { DEFAULT_DECODER_OPTIONS } from "#src/common/constants"; +import Decoder from "#src/modules/decoder"; +import type Token from "#src/modules/token"; +import type { DecoderOptions } from "#src/types/options"; type JSONTextDecoderStreamOptions = DecoderOptions & { writableStrategy?: QueuingStrategy; diff --git a/src/libs/stream-encoder.ts b/src/libs/stream-encoder.ts index 2da09b3..7950854 100644 --- a/src/libs/stream-encoder.ts +++ b/src/libs/stream-encoder.ts @@ -1,7 +1,7 @@ -import { DEFAULT_ENCODER_OPTIONS } from "#src/common/constants.ts"; -import Encoder from "#src/modules/encoder.ts"; -import type Token from "#src/modules/token.ts"; -import type { EncoderOptions } from "#src/types/options.ts"; +import { DEFAULT_ENCODER_OPTIONS } from "#src/common/constants"; +import Encoder from "#src/modules/encoder"; +import type Token from "#src/modules/token"; +import type { EncoderOptions } from "#src/types/options"; type JSONTextEncoderStreamOptions = EncoderOptions & { writableStrategy?: QueuingStrategy; diff --git a/src/libs/stream-line.ts b/src/libs/stream-line.ts index 801a5b4..e0c834a 100644 --- a/src/libs/stream-line.ts +++ b/src/libs/stream-line.ts @@ -1,7 +1,7 @@ -import { DEFAULT_DECODER_OPTIONS } from "#src/common/constants.ts"; -import Decoder from "#src/modules/decoder.ts"; -import type Value from "#src/modules/value.ts"; -import type { DecoderOptions } from "#src/types/options.ts"; +import { DEFAULT_DECODER_OPTIONS } from "#src/common/constants"; +import Decoder from "#src/modules/decoder"; +import type Value from "#src/modules/value"; +import type { DecoderOptions } from "#src/types/options"; type JSONTextLineStreamOptions = DecoderOptions & { writableStrategy?: QueuingStrategy; diff --git a/src/libs/stream-selector.ts b/src/libs/stream-selector.ts index 6d68e2a..c6967f4 100644 --- a/src/libs/stream-selector.ts +++ b/src/libs/stream-selector.ts @@ -1,10 +1,10 @@ -import { DEFAULT_DECODER_OPTIONS, KIND, MAX_NESTING_DEPTH } from "#src/common/constants.ts"; -import Decoder from "#src/modules/decoder.ts"; -import type { Matcher } from "#src/modules/path.ts"; -import Path from "#src/modules/path.ts"; -import type Value from "#src/modules/value.ts"; -import type { DecoderOptions } from "#src/types/options.ts"; -import { encodeText } from "#src/utils/text.ts"; +import { DEFAULT_DECODER_OPTIONS, KIND, MAX_NESTING_DEPTH } from "#src/common/constants"; +import Decoder from "#src/modules/decoder"; +import type { Matcher } from "#src/modules/path"; +import Path from "#src/modules/path"; +import type Value from "#src/modules/value"; +import type { DecoderOptions } from "#src/types/options"; +import { encodeText } from "#src/utils/text"; type JSONTextSelectorStreamOptions = DecoderOptions & { writableStrategy?: QueuingStrategy; diff --git a/src/modules/automaton.ts b/src/modules/automaton.ts index e972f00..cb2f291 100644 --- a/src/modules/automaton.ts +++ b/src/modules/automaton.ts @@ -1,6 +1,6 @@ -import { MAX_NESTING_DEPTH } from "#src/common/constants.ts"; -import Entry from "#src/modules/entry.ts"; -import type { Kind } from "#src/types/kind.ts"; +import { MAX_NESTING_DEPTH } from "#src/common/constants"; +import Entry from "#src/modules/entry"; +import type { Kind } from "#src/types/kind"; class Automaton { #last: Entry; diff --git a/src/modules/decoder.ts b/src/modules/decoder.ts index 3759df0..037cd07 100644 --- a/src/modules/decoder.ts +++ b/src/modules/decoder.ts @@ -1,14 +1,14 @@ -import { ASCII, KIND } from "#src/common/constants.ts"; -import { SyntacticError } from "#src/common/errors.ts"; -import Cursor from "#src/modules/cursor.ts"; -import type Pointer from "#src/modules/pointer.ts"; -import State from "#src/modules/state.ts"; -import Token from "#src/modules/token.ts"; -import Value from "#src/modules/value.ts"; -import type { Kind } from "#src/types/kind.ts"; -import type { DecoderOptions } from "#src/types/options.ts"; -import { normalize } from "#src/utils/kind.ts"; -import { decodeText } from "#src/utils/text.ts"; +import { ASCII, KIND } from "#src/common/constants"; +import { SyntacticError } from "#src/common/errors"; +import Cursor from "#src/modules/cursor"; +import type Pointer from "#src/modules/pointer"; +import State from "#src/modules/state"; +import Token from "#src/modules/token"; +import Value from "#src/modules/value"; +import type { Kind } from "#src/types/kind"; +import type { DecoderOptions } from "#src/types/options"; +import { normalize } from "#src/utils/kind"; +import { decodeText } from "#src/utils/text"; import { consumeFalse, consumeNull, @@ -18,7 +18,7 @@ import { consumeString, consumeTrue, consumeWhitespace, -} from "#src/utils/wire.ts"; +} from "#src/utils/wire"; class Decoder { #cursor: Cursor; diff --git a/src/modules/encoder.ts b/src/modules/encoder.ts index 389c617..2431386 100644 --- a/src/modules/encoder.ts +++ b/src/modules/encoder.ts @@ -1,13 +1,13 @@ -import { ASCII, KIND, UNICODE } from "#src/common/constants.ts"; -import { SyntacticError } from "#src/common/errors.ts"; -import type Pointer from "#src/modules/pointer.ts"; -import State from "#src/modules/state.ts"; -import Tape from "#src/modules/tape.ts"; -import Token from "#src/modules/token.ts"; -import Value from "#src/modules/value.ts"; -import type { Kind } from "#src/types/kind.ts"; -import type { EncoderOptions } from "#src/types/options.ts"; -import { decodeText, encodeText } from "#src/utils/text.ts"; +import { ASCII, KIND, UNICODE } from "#src/common/constants"; +import { SyntacticError } from "#src/common/errors"; +import type Pointer from "#src/modules/pointer"; +import State from "#src/modules/state"; +import Tape from "#src/modules/tape"; +import Token from "#src/modules/token"; +import Value from "#src/modules/value"; +import type { Kind } from "#src/types/kind"; +import type { EncoderOptions } from "#src/types/options"; +import { decodeText, encodeText } from "#src/utils/text"; class Encoder { #tape: Tape; diff --git a/src/modules/entry.ts b/src/modules/entry.ts index 7452533..8d12bf2 100644 --- a/src/modules/entry.ts +++ b/src/modules/entry.ts @@ -1,5 +1,5 @@ -import { KIND } from "#src/common/constants.ts"; -import type { Kind } from "#src/types/kind.ts"; +import { KIND } from "#src/common/constants"; +import type { Kind } from "#src/types/kind"; class Entry { #type: "object" | "array"; diff --git a/src/modules/path.ts b/src/modules/path.ts index 602581f..d7a3ad3 100644 --- a/src/modules/path.ts +++ b/src/modules/path.ts @@ -1,4 +1,4 @@ -import { ASCII, SEGMENT, SELECTOR } from "#src/common/constants.ts"; +import { ASCII, SEGMENT, SELECTOR } from "#src/common/constants"; import type { ArraySliceSelector, IndexSelector, @@ -6,9 +6,9 @@ import type { Segment, Selector, WildcardSelector, -} from "#src/types/path.ts"; -import { decodeText } from "#src/utils/text.ts"; -import { consumeNumber, consumeWhitespace } from "#src/utils/wire.ts"; +} from "#src/types/path"; +import { decodeText } from "#src/utils/text"; +import { consumeNumber, consumeWhitespace } from "#src/utils/wire"; /** * Supports a subset of JSON Path syntax and provides an NFA-based matcher. diff --git a/src/modules/state.ts b/src/modules/state.ts index e5ab82a..cac49e0 100644 --- a/src/modules/state.ts +++ b/src/modules/state.ts @@ -1,8 +1,8 @@ -import Automaton from "#src/modules/automaton.ts"; -import Pointer from "#src/modules/pointer.ts"; -import { ObjectNamespaceStack, ObjectNameStack } from "#src/modules/stack.ts"; -import type { Kind } from "#src/types/kind.ts"; -import type { BaseOptions } from "#src/types/options.ts"; +import Automaton from "#src/modules/automaton"; +import Pointer from "#src/modules/pointer"; +import { ObjectNamespaceStack, ObjectNameStack } from "#src/modules/stack"; +import type { Kind } from "#src/types/kind"; +import type { BaseOptions } from "#src/types/options"; class State { #automaton: Automaton; diff --git a/src/modules/token.ts b/src/modules/token.ts index f6bb345..3c8d3ed 100644 --- a/src/modules/token.ts +++ b/src/modules/token.ts @@ -1,7 +1,7 @@ -import { KIND } from "#src/common/constants.ts"; -import type { Kind } from "#src/types/kind.ts"; -import { normalize } from "#src/utils/kind.ts"; -import { decodeText, encodeText } from "#src/utils/text.ts"; +import { KIND } from "#src/common/constants"; +import type { Kind } from "#src/types/kind"; +import { normalize } from "#src/utils/kind"; +import { decodeText, encodeText } from "#src/utils/text"; /** * Represents a single JSON token. diff --git a/src/modules/value.ts b/src/modules/value.ts index c6772dd..8b5ceaf 100644 --- a/src/modules/value.ts +++ b/src/modules/value.ts @@ -1,10 +1,10 @@ -import { ASCII, DEFAULT_DECODER_OPTIONS, KIND } from "#src/common/constants.ts"; -import Decoder from "#src/modules/decoder.ts"; -import type Token from "#src/modules/token.ts"; -import type { Kind } from "#src/types/kind.ts"; -import { normalize } from "#src/utils/kind.ts"; -import { decodeText, encodeText } from "#src/utils/text.ts"; -import { compareUTF16, consumeWhitespace } from "#src/utils/wire.ts"; +import { ASCII, DEFAULT_DECODER_OPTIONS, KIND } from "#src/common/constants"; +import Decoder from "#src/modules/decoder"; +import type Token from "#src/modules/token"; +import type { Kind } from "#src/types/kind"; +import { normalize } from "#src/utils/kind"; +import { decodeText, encodeText } from "#src/utils/text"; +import { compareUTF16, consumeWhitespace } from "#src/utils/wire"; /** * Represents a complete JSON value. diff --git a/src/types/kind.ts b/src/types/kind.ts index 9055ba8..ea61ada 100644 --- a/src/types/kind.ts +++ b/src/types/kind.ts @@ -1,3 +1,3 @@ -import type { KIND } from "#src/common/constants.ts"; +import type { KIND } from "#src/common/constants"; export type Kind = typeof KIND[keyof typeof KIND]; diff --git a/src/types/path.ts b/src/types/path.ts index 0017e51..80fc017 100644 --- a/src/types/path.ts +++ b/src/types/path.ts @@ -1,4 +1,4 @@ -import { IDENTIFIER, SEGMENT, SELECTOR } from "#src/common/constants.ts"; +import { IDENTIFIER, SEGMENT, SELECTOR } from "#src/common/constants"; export type NameSelector = { type: typeof SELECTOR.NAME; name: string }; diff --git a/src/utils/kind.ts b/src/utils/kind.ts index 5a55c42..7f0fedf 100644 --- a/src/utils/kind.ts +++ b/src/utils/kind.ts @@ -1,5 +1,5 @@ -import { ASCII, KIND } from "#src/common/constants.ts"; -import type { Kind } from "#src/types/kind.ts"; +import { ASCII, KIND } from "#src/common/constants"; +import type { Kind } from "#src/types/kind"; const NORM_KIND: Record = { [ASCII.LOWER_CASE_N]: KIND.NULL, diff --git a/src/utils/wire.ts b/src/utils/wire.ts index 6739bc3..1269522 100644 --- a/src/utils/wire.ts +++ b/src/utils/wire.ts @@ -1,5 +1,5 @@ -import { ASCII } from "#src/common/constants.ts"; -import { decodeText } from "#src/utils/text.ts"; +import { ASCII } from "#src/common/constants"; +import { decodeText } from "#src/utils/text"; /** * Compares two strings by UTF-16 code unit order, as required by RFC 8785 §3.2.3. diff --git a/tests/e2e/correctness.test.ts b/tests/e2e/correctness.test.ts index e5ba1bf..1da1ec9 100644 --- a/tests/e2e/correctness.test.ts +++ b/tests/e2e/correctness.test.ts @@ -1,5 +1,5 @@ -import { KIND } from "#src/common/constants.ts"; -import { JSONTextDecoderStream, JSONTextLineStream, Token } from "#src/index.ts"; +import { KIND } from "#src/common/constants"; +import { JSONTextDecoderStream, JSONTextLineStream, Token } from "#src/index"; import { assert, assertEquals } from "#std/assert"; const GITHUB_TOKEN = Deno.env.get("GITHUB_TOKEN"); diff --git a/tests/e2e/query.test.ts b/tests/e2e/query.test.ts index 60a32ec..91d6be1 100644 --- a/tests/e2e/query.test.ts +++ b/tests/e2e/query.test.ts @@ -1,5 +1,5 @@ -import { KIND } from "#src/common/constants.ts"; -import { JSONTextSelectorStream } from "#src/index.ts"; +import { KIND } from "#src/common/constants"; +import { JSONTextSelectorStream } from "#src/index"; import { assert, assertEquals } from "#std/assert"; const GITHUB_TOKEN = Deno.env.get("GITHUB_TOKEN"); diff --git a/tests/e2e/round-trip.test.ts b/tests/e2e/round-trip.test.ts index 57a6122..925a389 100644 --- a/tests/e2e/round-trip.test.ts +++ b/tests/e2e/round-trip.test.ts @@ -1,5 +1,5 @@ -import { KIND } from "#src/common/constants.ts"; -import { JSONTextSelectorStream } from "#src/index.ts"; +import { KIND } from "#src/common/constants"; +import { JSONTextSelectorStream } from "#src/index"; import { assert, assertEquals } from "#std/assert"; const GITHUB_TOKEN = Deno.env.get("GITHUB_TOKEN"); diff --git a/tests/e2e/streaming.test.ts b/tests/e2e/streaming.test.ts index 18317b0..5c31d91 100644 --- a/tests/e2e/streaming.test.ts +++ b/tests/e2e/streaming.test.ts @@ -1,4 +1,4 @@ -import { JSONTextDecoderStream } from "#src/index.ts"; +import { JSONTextDecoderStream } from "#src/index"; import { assert } from "#std/assert"; const GITHUB_TOKEN = Deno.env.get("GITHUB_TOKEN"); diff --git a/tests/integration/decoder.test.ts b/tests/integration/decoder.test.ts index afd71e0..f8342e5 100644 --- a/tests/integration/decoder.test.ts +++ b/tests/integration/decoder.test.ts @@ -1,7 +1,7 @@ -import { KIND } from "#src/common/constants.ts"; -import { SyntacticError } from "#src/common/errors.ts"; -import { JSONTextDecoder } from "#src/index.ts"; -import { encodeText } from "#src/utils/text.ts"; +import { KIND } from "#src/common/constants"; +import { SyntacticError } from "#src/common/errors"; +import { JSONTextDecoder } from "#src/index"; +import { encodeText } from "#src/utils/text"; import { assertEquals, assertThrows } from "#std/assert"; Deno.test("[integration] JSONTextDecoder", async (test) => { diff --git a/tests/integration/encoder.test.ts b/tests/integration/encoder.test.ts index 5dad492..554f7e3 100644 --- a/tests/integration/encoder.test.ts +++ b/tests/integration/encoder.test.ts @@ -1,8 +1,8 @@ -import { SyntacticError } from "#src/common/errors.ts"; -import { JSONTextEncoder } from "#src/index.ts"; -import Token from "#src/modules/token.ts"; -import Value from "#src/modules/value.ts"; -import { decodeText, encodeText } from "#src/utils/text.ts"; +import { SyntacticError } from "#src/common/errors"; +import { JSONTextEncoder } from "#src/index"; +import Token from "#src/modules/token"; +import Value from "#src/modules/value"; +import { decodeText, encodeText } from "#src/utils/text"; import { assertEquals, assertThrows } from "#std/assert"; Deno.test("[integration] JSONTextEncoder", async (test) => { diff --git a/tests/integration/stream-decoder.test.ts b/tests/integration/stream-decoder.test.ts index 2d02068..e5df894 100644 --- a/tests/integration/stream-decoder.test.ts +++ b/tests/integration/stream-decoder.test.ts @@ -1,6 +1,6 @@ -import { JSONTextDecoderStream } from "#src/index.ts"; -import type Token from "#src/modules/token.ts"; -import { encodeText } from "#src/utils/text.ts"; +import { JSONTextDecoderStream } from "#src/index"; +import type Token from "#src/modules/token"; +import { encodeText } from "#src/utils/text"; import { assertEquals, assertRejects } from "#std/assert"; Deno.test("[integration] JSONTextDecoderStream", async (test) => { diff --git a/tests/integration/stream-encoder.test.ts b/tests/integration/stream-encoder.test.ts index 684e617..4595670 100644 --- a/tests/integration/stream-encoder.test.ts +++ b/tests/integration/stream-encoder.test.ts @@ -1,6 +1,6 @@ -import { JSONTextEncoderStream } from "#src/index.ts"; -import Token from "#src/modules/token.ts"; -import { decodeText } from "#src/utils/text.ts"; +import { JSONTextEncoderStream } from "#src/index"; +import Token from "#src/modules/token"; +import { decodeText } from "#src/utils/text"; import { assertEquals, assertRejects } from "#std/assert"; Deno.test("[integration] JSONTextEncoderStream", async (test) => { diff --git a/tests/integration/stream-line.test.ts b/tests/integration/stream-line.test.ts index b5955b4..0c92d25 100644 --- a/tests/integration/stream-line.test.ts +++ b/tests/integration/stream-line.test.ts @@ -1,5 +1,5 @@ -import { JSONTextLineStream } from "#src/index.ts"; -import { encodeText } from "#src/utils/text.ts"; +import { JSONTextLineStream } from "#src/index"; +import { encodeText } from "#src/utils/text"; import { assertEquals } from "#std/assert"; Deno.test("[integration] JSONTextLineStream", async (test) => { diff --git a/tests/path.test.ts b/tests/path.test.ts index 9b55ade..c1964c4 100644 --- a/tests/path.test.ts +++ b/tests/path.test.ts @@ -1,5 +1,5 @@ -import Path from "#src/modules/path.ts"; -import { encodeText } from "#src/utils/text.ts"; +import Path from "#src/modules/path"; +import { encodeText } from "#src/utils/text"; import { assert, assertThrows } from "#std/assert"; Deno.test("[module] path", async (test) => { diff --git a/tests/state.test.ts b/tests/state.test.ts index faa0c5e..a6848bd 100644 --- a/tests/state.test.ts +++ b/tests/state.test.ts @@ -1,5 +1,5 @@ -import { KIND, MAX_NESTING_DEPTH } from "#src/common/constants.ts"; -import State from "#src/modules/state.ts"; +import { KIND, MAX_NESTING_DEPTH } from "#src/common/constants"; +import State from "#src/modules/state"; import { assertEquals, assertExists, assertThrows } from "#std/assert"; Deno.test("[module] state", async (test) => { diff --git a/tests/token.test.ts b/tests/token.test.ts index 5278b84..517b2f8 100644 --- a/tests/token.test.ts +++ b/tests/token.test.ts @@ -1,6 +1,6 @@ -import { KIND } from "#src/common/constants.ts"; -import Token from "#src/modules/token.ts"; -import { encodeText as e } from "#src/utils/text.ts"; +import { KIND } from "#src/common/constants"; +import Token from "#src/modules/token"; +import { encodeText as e } from "#src/utils/text"; import { assertEquals, assertThrows } from "#std/assert"; Deno.test("[module] token", async (test) => { diff --git a/tests/value.test.ts b/tests/value.test.ts index 498459c..4023538 100644 --- a/tests/value.test.ts +++ b/tests/value.test.ts @@ -1,6 +1,6 @@ -import { KIND } from "#src/common/constants.ts"; -import Value from "#src/modules/value.ts"; -import { encodeText as e } from "#src/utils/text.ts"; +import { KIND } from "#src/common/constants"; +import Value from "#src/modules/value"; +import { encodeText as e } from "#src/utils/text"; import { assertEquals, assertFalse, assertThrows } from "#std/assert"; Deno.test("[module] value", async (test) => { diff --git a/tests/wire.test.ts b/tests/wire.test.ts index e11f474..a529c74 100644 --- a/tests/wire.test.ts +++ b/tests/wire.test.ts @@ -1,4 +1,4 @@ -import { encodeText as e } from "#src/utils/text.ts"; +import { encodeText as e } from "#src/utils/text"; import { compareUTF16, consumeFalse, @@ -9,7 +9,7 @@ import { consumeString, consumeTrue, consumeWhitespace, -} from "#src/utils/wire.ts"; +} from "#src/utils/wire"; import { assertEquals } from "#std/assert"; Deno.test("[utils] wire", async (test) => { diff --git a/tsconfig.json b/tsconfig.json index 37c2a57..623f472 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,6 @@ "lib": ["ESNext", "DOM"], "paths": { "#src/*": ["./src/*"] }, "moduleResolution": "bundler", - "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "moduleDetection": "force", "skipLibCheck": true, diff --git a/vite.config.ts b/vite.config.ts index 4556148..489fd95 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -19,8 +19,6 @@ export default defineConfig(({ mode }) => { } return { - server: { - host: true - } - } + server: { host: true }, + }; }); From affb32cd436c7161c91fa692b96bfa3f56c2f4ca Mon Sep 17 00:00:00 2001 From: eden <198768181+lcweden@users.noreply.github.com> Date: Sat, 23 May 2026 20:02:25 +0000 Subject: [PATCH 2/5] refactor: replace bytes() method with takeBytes() for improved buffer management --- src/api/encoder.ts | 20 ++++++++--------- tests/integration/encoder.test.ts | 36 +++++++++++++++---------------- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/api/encoder.ts b/src/api/encoder.ts index 609938f..05e6fde 100644 --- a/src/api/encoder.ts +++ b/src/api/encoder.ts @@ -23,17 +23,6 @@ class JSONTextEncoder { this.#encoder = new Encoder({ ...DEFAULT_ENCODER_OPTIONS, ...options }); } - /** - * Returns the cumulative encoded bytes produced so far. - * - * The buffer is never cleared between calls; use {@link reset} to start fresh. - * - * @returns The cumulative encoded bytes produced so far. - */ - bytes(): Uint8Array { - return this.#encoder.bytes(); - } - /** * The current nesting depth — `0` at top level, incremented inside each * object or array. @@ -79,6 +68,15 @@ class JSONTextEncoder { return this.#encoder.stackPointer(where).toString(); } + /** + * Drains and returns the bytes accumulated in the output buffer since the + * last call. The internal buffer is cleared; structural state (nesting, + * delimiters) is preserved so subsequent writes continue the same document. + */ + takeBytes(): Uint8Array { + return this.#encoder.takeBytes(); + } + /** * Encodes a single {@link Token} and appends its bytes to the output buffer. * diff --git a/tests/integration/encoder.test.ts b/tests/integration/encoder.test.ts index 554f7e3..cf02275 100644 --- a/tests/integration/encoder.test.ts +++ b/tests/integration/encoder.test.ts @@ -12,7 +12,7 @@ Deno.test("[integration] JSONTextEncoder", async (test) => { encoder.writeToken(Token.fromText("null")); - assertEquals(decodeText(encoder.bytes()), "null"); + assertEquals(decodeText(encoder.takeBytes()), "null"); }); await test.step("should write a boolean token", () => { @@ -20,7 +20,7 @@ Deno.test("[integration] JSONTextEncoder", async (test) => { encoder.writeToken(Token.fromBoolean(true)); - assertEquals(decodeText(encoder.bytes()), "true"); + assertEquals(decodeText(encoder.takeBytes()), "true"); }); await test.step("should write a number token", () => { @@ -28,7 +28,7 @@ Deno.test("[integration] JSONTextEncoder", async (test) => { encoder.writeToken(Token.fromNumber(42)); - assertEquals(decodeText(encoder.bytes()), "42"); + assertEquals(decodeText(encoder.takeBytes()), "42"); }); await test.step("should write a string token", () => { @@ -36,7 +36,7 @@ Deno.test("[integration] JSONTextEncoder", async (test) => { encoder.writeToken(Token.fromString("hello")); - assertEquals(decodeText(encoder.bytes()), '"hello"'); + assertEquals(decodeText(encoder.takeBytes()), '"hello"'); }); await test.step("should write array tokens producing compact JSON", () => { @@ -47,7 +47,7 @@ Deno.test("[integration] JSONTextEncoder", async (test) => { encoder.writeToken(Token.fromNumber(2)); encoder.writeToken(Token.fromText("]")); - assertEquals(decodeText(encoder.bytes()), "[1,2]"); + assertEquals(decodeText(encoder.takeBytes()), "[1,2]"); }); await test.step("should write object tokens with space after colon", () => { @@ -58,7 +58,7 @@ Deno.test("[integration] JSONTextEncoder", async (test) => { encoder.writeToken(Token.fromNumber(1)); encoder.writeToken(Token.fromText("}")); - assertEquals(decodeText(encoder.bytes()), '{"a": 1}'); + assertEquals(decodeText(encoder.takeBytes()), '{"a": 1}'); }); await test.step("should throw SyntacticError when a number is written as an object key", () => { @@ -82,7 +82,7 @@ Deno.test("[integration] JSONTextEncoder", async (test) => { encoder.writeValue(new Value(encodeText("42"))); - assertEquals(decodeText(encoder.bytes()), "42"); + assertEquals(decodeText(encoder.takeBytes()), "42"); }); await test.step("should write a nested array as a single call", () => { @@ -90,7 +90,7 @@ Deno.test("[integration] JSONTextEncoder", async (test) => { encoder.writeValue(new Value(encodeText("[1,2]"))); - assertEquals(decodeText(encoder.bytes()), "[1,2]"); + assertEquals(decodeText(encoder.takeBytes()), "[1,2]"); }); await test.step("should write a nested object as a single call", () => { @@ -98,15 +98,15 @@ Deno.test("[integration] JSONTextEncoder", async (test) => { encoder.writeValue(new Value(encodeText('{"a":1}'))); - assertEquals(decodeText(encoder.bytes()), '{"a":1}'); + assertEquals(decodeText(encoder.takeBytes()), '{"a":1}'); }); }); - await test.step("[scenario] bytes", async (test) => { + await test.step("[scenario] takeBytes", async (test) => { await test.step("should return an empty buffer before any writes", () => { const encoder = new JSONTextEncoder(); - assertEquals(encoder.bytes(), new Uint8Array(0)); + assertEquals(encoder.takeBytes(), new Uint8Array(0)); }); await test.step("should return the written bytes after writing", () => { @@ -114,7 +114,7 @@ Deno.test("[integration] JSONTextEncoder", async (test) => { encoder.writeToken(Token.fromNumber(42)); - assertEquals(encoder.bytes(), encodeText("42")); + assertEquals(encoder.takeBytes(), encodeText("42")); }); }); @@ -162,7 +162,7 @@ Deno.test("[integration] JSONTextEncoder", async (test) => { encoder.writeToken(Token.fromNumber(1)); encoder.writeToken(Token.fromText("]")); - assertEquals(decodeText(encoder.bytes()), "[\n\t1\n]"); + assertEquals(decodeText(encoder.takeBytes()), "[\n\t1\n]"); }); await test.step("should produce compact output when multiline is false", () => { @@ -172,7 +172,7 @@ Deno.test("[integration] JSONTextEncoder", async (test) => { encoder.writeToken(Token.fromNumber(1)); encoder.writeToken(Token.fromText("]")); - assertEquals(decodeText(encoder.bytes()), "[1]"); + assertEquals(decodeText(encoder.takeBytes()), "[1]"); }); await test.step("should not add space after colon when spaceAfterColon is false", () => { @@ -183,7 +183,7 @@ Deno.test("[integration] JSONTextEncoder", async (test) => { encoder.writeToken(Token.fromNumber(1)); encoder.writeToken(Token.fromText("}")); - assertEquals(decodeText(encoder.bytes()), '{"a":1}'); + assertEquals(decodeText(encoder.takeBytes()), '{"a":1}'); }); await test.step("should canonicalize raw numbers when option is enabled", () => { @@ -191,7 +191,7 @@ Deno.test("[integration] JSONTextEncoder", async (test) => { encoder.writeToken(Token.fromText("1.0e1")); - assertEquals(decodeText(encoder.bytes()), "10"); + assertEquals(decodeText(encoder.takeBytes()), "10"); }); await test.step("should escape HTML characters when escapeForHTML is enabled", () => { @@ -199,7 +199,7 @@ Deno.test("[integration] JSONTextEncoder", async (test) => { encoder.writeToken(Token.fromString("&")); - assertEquals(decodeText(encoder.bytes()), '"\\u003cb\\u003e\\u0026\\u003c/b\\u003e"'); + assertEquals(decodeText(encoder.takeBytes()), '"\\u003cb\\u003e\\u0026\\u003c/b\\u003e"'); }); await test.step("should escape JS line terminators when escapeForJS is enabled", () => { @@ -207,7 +207,7 @@ Deno.test("[integration] JSONTextEncoder", async (test) => { encoder.writeToken(Token.fromString("\u2028\u2029")); - assertEquals(decodeText(encoder.bytes()), '"\\u2028\\u2029"'); + assertEquals(decodeText(encoder.takeBytes()), '"\\u2028\\u2029"'); }); }); }); From 6c480029a8de600ed243d5cb0b473ac2ce3e7b91 Mon Sep 17 00:00:00 2001 From: eden <198768181+lcweden@users.noreply.github.com> Date: Sat, 23 May 2026 20:02:31 +0000 Subject: [PATCH 3/5] chore: add engines field to specify Node.js version requirement --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 4e67c04..d3685b7 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "test": "deno task test" }, "sideEffects": false, + "engines": { "node": ">=18" }, "devDependencies": { "@types/node": "^25.9.1", "typescript": "~6.0.2", From 53f56eeb9fc1d0b85e4c248eda93b5dd4c559cb1 Mon Sep 17 00:00:00 2001 From: eden <198768181+lcweden@users.noreply.github.com> Date: Sat, 23 May 2026 20:08:20 +0000 Subject: [PATCH 4/5] docs: update README to enhance clarity and examples for JSONText usage --- README.md | 389 ++++++++++++++++++++++++++---------------------------- 1 file changed, 184 insertions(+), 205 deletions(-) diff --git a/README.md b/README.md index dd5eec1..81b1ecc 100644 --- a/README.md +++ b/README.md @@ -1,205 +1,267 @@ # JSONText -A streaming JSON parser and serializer for JavaScript, operating at the syntactic layer — processing -JSON tokens and values without any semantic marshaling. +A state machine for incremental JSON processing. ## Quick Start -Parse a JSON stream and extract specific values with a JSONPath query: +The following example demonstrates how to use `JSONTextSelectorStream` to extract all `title` values +from a JSON stream fetched from [DummyJSON](https://dummyjson.com/). -```ts +```javascript import { JSONTextSelectorStream } from "jsontext"; -const response = await fetch("https://example.com/data.json"); -const stream = response.body.pipeThrough(new JSONTextSelectorStream("$.items[*]")); +const response = await fetch("https://dummyjson.com/users"); +const addresses = response.body.pipeThrough(new JSONTextSelectorStream("$.users[*].address")); -for await (const value of stream) { +for await (const value of addresses) { console.log(value.json()); } ``` ## Installation +`jsontext` is an ESM-only package available on both `NPM` and `JSR`. The core decoder and encoder +run in any modern JavaScript environment; the optional `*Stream` classes additionally require +`WHATWG` Streams support: + ### NPM +Install via [npm](https://www.npmjs.com/package/jsontext): + ```bash npm install jsontext ``` -## APIs - -### JSONTextDecoderStream - -Transforms a `ReadableStream` into a `ReadableStream`, emitting one `Token` per -JSON token in document order. +> [!NOTE] +> It may require Node.js 18 or later. -```ts -import { JSONTextDecoderStream } from "jsontext"; +### Deno -const response = await fetch("https://example.com/data.json"); +Install via [JSR](https://jsr.io/@lcweden/jsontext): -for await (const token of response.body.pipeThrough(new JSONTextDecoderStream())) { - console.log(token.kind, token.bytes); -} +```bash +deno add jsr:@lcweden/jsontext ``` -### JSONTextEncoderStream - -Transforms a `ReadableStream` into a `ReadableStream`. +## APIs -```ts -import { JSONTextEncoderStream, Token } from "jsontext"; +See full reference on [JSR](https://jsr.io/@lcweden/jsontext/doc/). -const { readable, writable } = new JSONTextEncoderStream(); -const writer = writable.getWriter(); +| Category | Exports | +| :-------- | :------------------------------------------------------------------------------------------------------- | +| Core | [`JSONTextDecoder`], [`JSONTextEncoder`] | +| Stream | [`JSONTextDecoderStream`], [`JSONTextEncoderStream`], [`JSONTextSelectorStream`], [`JSONTextLineStream`] | +| Component | [`Token`], [`Value`], [`KIND`] | +| Error | [`SyntacticError`] | -writer.write(Token.ARRAY_BEGIN); -writer.write(Token.fromNumber(1)); -writer.write(Token.fromNumber(2)); -writer.write(Token.fromNumber(3)); -writer.write(Token.ARRAY_END); -writer.close(); +[`JSONTextDecoder`]: https://jsr.io/@lcweden/jsontext/doc/~/JSONTextDecoder +[`JSONTextEncoder`]: https://jsr.io/@lcweden/jsontext/doc/~/JSONTextEncoder +[`JSONTextDecoderStream`]: https://jsr.io/@lcweden/jsontext/doc/~/JSONTextDecoderStream +[`JSONTextEncoderStream`]: https://jsr.io/@lcweden/jsontext/doc/~/JSONTextEncoderStream +[`JSONTextSelectorStream`]: https://jsr.io/@lcweden/jsontext/doc/~/JSONTextSelectorStream +[`JSONTextLineStream`]: https://jsr.io/@lcweden/jsontext/doc/~/JSONTextLineStream +[`Token`]: https://jsr.io/@lcweden/jsontext/doc/~/Token +[`Value`]: https://jsr.io/@lcweden/jsontext/doc/~/Value +[`KIND`]: https://jsr.io/@lcweden/jsontext/doc/~/KIND +[`SyntacticError`]: https://jsr.io/@lcweden/jsontext/doc/~/SyntacticError -const bytes = await new Response(readable).arrayBuffer(); -``` +## Concepts -### JSONTextSelectorStream +`JSON.parse` is a native, single-pass parser. It will always be faster than `jsontext` on JSON that +fits comfortably in memory. -Transforms a byte stream into a `ReadableStream`, emitting only values matched by a -[JSON Path](https://www.rfc-editor.org/rfc/rfc9535) query. +`jsontext` makes a deliberate tradeoff: it gives up raw throughput to gain bounded memory, lower +time to first result, incremental processing, and the ability to filter data without ever +materializing it. This matters when the input is too large to hold, arrives in chunks, or you only +care about a small slice of it. -```ts -import { JSONTextSelectorStream } from "jsontext"; +```javascript +// JSON.parse — needs the whole string, builds the whole tree +const data = await response.json(); // JSON.parse(await response.text()); +const titles = data.map((item) => item.title); -const response = await fetch("https://example.com/data.json"); -const stream = response.body.pipeThrough(new JSONTextSelectorStream("$.features[*].geometry")); +// jsontext — reads bytes as they arrive, emits values one by one +const titles = response.body.pipeThrough(new JSONTextSelectorStream("$..title")); -for await (const value of stream) { +for await (const value of titles) { console.log(value.json()); } ``` -Supports the following [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535) selectors: Child Segment, -Descendant Segment, Name Selector, Wildcard Selector, Index Selector (positive only), and Array -Slice Selector (positive only). +### Tokens and Values -### JSONTextLineStream +Represents JSON at two granularities: -Transforms a byte stream into a `ReadableStream`, emitting one complete `Value` per top-level -JSON value. Well-suited for JSONL / JSON Lines and other concatenated-JSON streams. +- **Tokens**: The smallest lexical unit (a scalar like `"Alice"`, `true`, `123`, or a delimiter like + `{`, `}`, `[`, `]`). -```ts -import { JSONTextLineStream } from "jsontext"; +- **Values**: A complete unit — a scalar, or an entire `object` or `array` including everything + nested inside. -const response = await fetch("https://example.com/data.jsonl"); -const stream = response.body.pipeThrough(new JSONTextLineStream()); +A token can never represent a whole object or array; a value always can. -for await (const value of stream) { - console.log(value.json()); -} +```javascript +import { JSONTextDecoder, KIND } from "jsontext"; + +const json = `{"name": "Alice", "tags": ["admin", "user"]}`; +const decoder = new JSONTextDecoder(new TextEncoder().encode(json)); +decoder.end(); // signal no more bytes will be pushed + +// readToken — one lexical step at a time +decoder.readToken().kind; // KIND.OBJECT_BEGIN ('{') +decoder.readToken().asString(); // "name" +decoder.readToken().asString(); // "Alice" +decoder.readToken().asString(); // "tags" + +// readValue — collapses an entire subtree into one Value +const tags = decoder.readValue(); +tags.json(); // ["admin", "user"] + +decoder.readToken().kind; // KIND.OBJECT_END ('}') ``` -## Types +Use `Value` when you need a specific subtree. Call `value.json()` to materialize it, or +`decoder.skipValue()` to cheaply discard massive branches you don't need without parsing them. + +> [!IMPORTANT] +> Tokens and values returned from a decoder are views into its internal buffer. The buffer is +> overwritten the next time you `.push()` more bytes or read another token/value, so anything you +> keep around must be copied first with `.clone()`. +> +> ```javascript +> const collected = []; +> let token; +> +> while ((token = decoder.readToken()) !== undefined) { +> collected.push(token); // unsafe: all entries may end up pointing at the same bytes +> collected.push(token.clone()); // safe: independent copy +> } +> ``` + +### Push and pull + +Decoding is split into two halves. You **push** bytes in whenever you have them — from a single +buffer, a stream chunk, a socket, anything — and you **pull** tokens or values out at your own pace. +The decoder buffers what it needs and waits for more bytes when a token straddles a chunk boundary. + +This decoupling is what makes _time to first result_ low. With `JSON.parse` you must wait for the +entire payload before you can touch any data; if the server takes 5 seconds to stream a 50 MB +response, you wait 5 seconds. With `jsontext` the first token is available as soon as the first few +bytes arrive — typically tens of milliseconds. -### Kind +```javascript +import { JSONTextDecoder } from "jsontext"; -`Kind` is a string literal type representing the class of a JSON token. All values are available on -the `KIND` constant: +const decoder = new JSONTextDecoder(); -```ts -import { KIND } from "jsontext"; +for await (const chunk of response.body) { + decoder.push(chunk); // push: feed bytes as they arrive -KIND.NULL; // "null" -KIND.TRUE; // "true" -KIND.FALSE; // "false" -KIND.STRING; // "string" -KIND.NUMBER; // "number" -KIND.OBJECT_BEGIN; // "{" -KIND.OBJECT_END; // "}" -KIND.ARRAY_BEGIN; // "[" -KIND.ARRAY_END; // "]" + let token; + while ((token = decoder.readToken()) !== undefined) { + // pull: drain decodable tokens, then wait for more bytes + handle(token); + } +} + +decoder.end(); +decoder.checkEOF(); ``` -### Token +`readToken` returns `undefined` when the buffer is exhausted mid-token — that's the signal to go +fetch more bytes, not an error. `end()` tells the decoder no more input is coming; `checkEOF()` then +asserts that what arrived was a complete, well-formed document. -A `Token` represents a single lexical element — a scalar value (`null`, `true`, `false`, a number, -or a string) or a structural delimiter (`{`, `}`, `[`, `]`). +### Composing with streams -**Pre-built tokens:** +The core `JSONTextDecoder` and `JSONTextEncoder` are manual state machines. For common use cases, +use `TransformStream` wrappers that natively compose with fetch, files, and Web Streams. -```ts -Token.NULL; // null -Token.TRUE; // true -Token.FALSE; // false -Token.OBJECT_BEGIN; // { -Token.OBJECT_END; // } -Token.ARRAY_BEGIN; // [ -Token.ARRAY_END; // ] -``` +```javascript +import { JSONTextLineStream, JSONTextSelectorStream } from "jsontext"; -**Factory methods:** +// Filter a JSON Lines feed: keep only active users, write them back out as JSONL. +// JSONTextLineStream emits one Value per line, preserving the original bytes. +const encoder = new TextEncoder(); -```ts -Token.fromBoolean(true); // true -Token.fromNumber(3.14); // 3.14 -Token.fromString("hello"); // "hello" -Token.fromText('"raw"'); // from a raw JSON text string +await response.body + .pipeThrough(new JSONTextLineStream()) + .pipeThrough( + new TransformStream({ + transform(value, controller) { + const user = value.json(); + if (user.active) controller.enqueue(encoder.encode(value.text() + "\n")); + }, + }), + ) + .pipeTo(destination); + +// Or extract a single slice from a large document with a JSONPath selector +response.body.pipeThrough(new JSONTextSelectorStream("$.users[*].email")); ``` -### Value +Each stream is a thin adapter over the core API, so you can mix hand-driven decoding and stream +piping in the same program without giving up either one's guarantees. -A `Value` represents a complete JSON value — a scalar or an entire object/array including all nested -content. +### Locating syntax errors -**Factory methods:** +When input violates RFC 8259, jsontext throws a `SyntacticError` carrying both the byte `offset` and +the JSON `pointer` to help pinpoint the exact failure. -```ts -Value.from({ name: "Alice", scores: [1, 2, 3] }); -// {"name":"Alice","scores":[1,2,3]} +```javascript +import { JSONTextDecoder, SyntacticError } from "jsontext"; + +try { + const decoder = new JSONTextDecoder(new TextEncoder().encode(`{"a": 1, "b": }`)); + decoder.end(); + while (decoder.readToken() !== undefined) { /* ... */ } +} catch (error) { + if (error instanceof SyntacticError) { + console.error(error.message); + } +} ``` -## Examples +## Example Pipelines -The following examples demonstrate how to use the streaming API to perform common transformations on -JSON data. +### Replace `null` with an empty string -### Redacting values +Swap every `null` token for an empty string as the JSON flows through — no parsing the whole +document, no intermediate object. ```javascript -import { JSONTextDecoderStream, JSONTextEncoderStream } from "jsontext"; - -const target = "password"; -const response = await fetch("https://example.com/user.json"); +import { JSONTextDecoderStream, JSONTextEncoderStream, KIND, Token } from "jsontext"; -response.body - .pipeThrough(new JSONTextDecoderStream()) +stream + .pipeThrough(new JSONTextDecoderStream()) // decode bytes into tokens .pipeThrough( new TransformStream({ transform(token, controller) { - if (token.kind === KIND.STRING && token.asString() === target) { - controller.enqueue(Token.fromString("********")); + if (token.kind === KIND.NULL) { // Detect a `null` token + controller.enqueue(Token.fromString("")); // Emit an empty string token instead } else { controller.enqueue(token); } }, }), ) - .pipeThrough(new JSONTextEncoderStream()); + .pipeThrough(new JSONTextEncoderStream()); // encode tokens back into bytes ``` -### Extracting into a new array +### Extract and Restructure Data -```javascript -import { JSONTextEncoderStream, JSONTextSelectorStream } from "jsontext"; +Extract specific nested elements using JSONPath, and wrap them into a brand new JSON array structure +directly in the stream pipeline. -const response = await fetch("https://example.com/data.json"); +```javascript +import { JSONTextEncoderStream, JSONTextSelectorStream, Token } from "jsontext"; -response.body - .pipeThrough(new JSONTextSelectorStream("$.items[*]")) +stream + .pipeThrough(new JSONTextSelectorStream("$.todos[*].todo")) // extract all `todo` values from the `todos` array .pipeThrough( new TransformStream({ start(controller) { - controller.enqueue(Token.ARRAY_BEGIN); + controller.enqueue(Token.ARRAY_BEGIN); // emit a `[` to start the output array }, transform(value, controller) { for (const token of value.tokens()) { @@ -207,101 +269,18 @@ response.body } }, flush(controller) { - controller.enqueue(Token.ARRAY_END); + controller.enqueue(Token.ARRAY_END); // emit a `]` to end the output array }, }), ) - .pipeThrough(new JSONTextEncoderStream()); + .pipeThrough(new JSONTextEncoderStream()); // encode back to bytes for output ``` -## Core - -For cases where the stream API is not flexible enough, `JSONTextDecoder` and `JSONTextEncoder` -provide a lower-level interface for building custom decoders and encoders. - -### JSONTextDecoder - -```ts -import { JSONTextDecoder } from "jsontext"; - -const decoder = new JSONTextDecoder(); -decoder.push(new TextEncoder().encode('{"name":"Alice"}')); -decoder.end(); - -let token; -while ((token = decoder.readToken()) !== undefined) { - console.log(token.kind); -} - -decoder.checkEOF(); // throws if there are unconsumed bytes or an incomplete value -``` - -| Method | Description | -| ---------------------- | ------------------------------------------------------------------------------------ | -| `push(bytes)` | Append a chunk of raw JSON bytes | -| `end()` | Signal end-of-input; validates the stream is not mid-value | -| `readToken()` | Return the next `Token`, or `undefined` if more input is needed | -| `readValue()` | Return the next complete `Value`, or `undefined` if more input is needed | -| `skipValue()` | Discard the next complete value; returns `true` if a value was skipped | -| `peekKind()` | Inspect the next token kind without consuming it | -| `stackPointer(where?)` | JSON Pointer to the next (`1`), current container (`0`), or previous (`-1`) position | -| `inputOffset()` | Byte offset of the next unread byte | -| `depth()` | Current nesting depth | -| `unreadBytes()` | View of buffered but unconsumed bytes | -| `checkEOF()` | Assert that all input has been consumed | -| `reset()` | Clear all buffered input and internal state | - -### JSONTextEncoder - -```ts -import { JSONTextEncoder, Token } from "jsontext"; - -const encoder = new JSONTextEncoder(); -encoder.writeToken(Token.OBJECT_BEGIN); -encoder.writeToken(Token.fromString("name")); -encoder.writeToken(Token.fromString("Alice")); -encoder.writeToken(Token.OBJECT_END); - -console.log(new TextDecoder().decode(encoder.bytes())); -``` - -| Method | Description | -| ---------------------- | ------------------------------------------------------------------------------------ | -| `writeToken(token)` | Encode a `Token` and append its bytes to the output buffer | -| `writeValue(value)` | Encode a `Value` and append its bytes to the output buffer | -| `bytes()` | All bytes produced so far | -| `outputOffset()` | Byte offset after the last written token | -| `stackPointer(where?)` | JSON Pointer to the next (`1`), current container (`0`), or previous (`-1`) position | -| `depth()` | Current nesting depth | -| `reset()` | Clear the output buffer and all internal state | - -## Options - -### Decoder - -| Option | Default | Description | -| --------------------- | ------- | ------------------------------------------------------------- | -| `allowDuplicateNames` | `false` | Allow duplicate object member names | -| `allowInvalidUTF8` | `false` | Replace invalid UTF-8 bytes with `U+FFFD` instead of erroring | - -### Encoder - -All decoder options, plus: +## License -| Option | Default | Description | -| ------------------------ | ------- | ------------------------------------------------------------ | -| `escapeForHTML` | `false` | Escape `<`, `>`, `&` as `\uXXXX` for safe HTML embedding | -| `escapeForJS` | `false` | Escape `U+2028` and `U+2029` for safe JavaScript embedding | -| `canonicalizeRawNumbers` | `false` | Normalize numbers per RFC 8785 §3.2.2.3 | -| `spaceAfterColon` | `true` | Emit a space after `:` in objects | -| `spaceAfterComma` | `false` | Emit a space after `,` | -| `multiline` | `true` | Expand output across multiple indented lines | -| `indent` | `"\t"` | Indentation string (implies `multiline`) | -| `indentPrefix` | `""` | Prefix prepended to each indented line (implies `multiline`) | +This project is licensed under the [MIT](LICENSE) License. ## Acknowledgements -This project is heavily inspired by Go's -[`encoding/json/jsontext`](https://pkg.go.dev/encoding/json/jsontext) standard library. The API and -internal design are closely modeled after it, with adjustments made to fit JavaScript's language -features and ecosystem. +This project is inspired by Go's +[`encoding/json/jsontext`](https://pkg.go.dev/encoding/json/jsontext) standard library. From f5f1df86f2afa6438e38ba70397afb4c79228604 Mon Sep 17 00:00:00 2001 From: eden <198768181+lcweden@users.noreply.github.com> Date: Sat, 23 May 2026 20:10:43 +0000 Subject: [PATCH 5/5] chore: update version to 0.2.0 in deno.json and package.json --- deno.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deno.json b/deno.json index 2d14b30..c1edf81 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@lcweden/jsontext", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "tasks": { "bench": "deno bench --allow-all", diff --git a/package.json b/package.json index d3685b7..fd6871f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsontext", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "description": "State machine for incremental JSON processing.", "keywords": ["json", "stream"],