@humanspeak/svelte-json-view-lite 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,6 +7,7 @@ runtime dependencies.
7
7
 
8
8
  [![NPM version](https://img.shields.io/npm/v/@humanspeak/svelte-json-view-lite.svg)](https://www.npmjs.com/package/@humanspeak/svelte-json-view-lite)
9
9
  [![Build Status](https://github.com/humanspeak/svelte-json-view-lite/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/humanspeak/svelte-json-view-lite/actions/workflows/npm-publish.yml)
10
+ [![AI tokens used building this repo — TokenMaxing](https://tokenmaxing.app/badge/humanspeak/svelte-json-view-lite)](https://tokenmaxing.app/card/humanspeak/svelte-json-view-lite)
10
11
  [![Coverage Status](https://coveralls.io/repos/github/humanspeak/svelte-json-view-lite/badge.svg?branch=main)](https://coveralls.io/github/humanspeak/svelte-json-view-lite?branch=main)
11
12
  [![License](https://img.shields.io/npm/l/@humanspeak/svelte-json-view-lite.svg)](https://github.com/humanspeak/svelte-json-view-lite/blob/main/LICENSE)
12
13
  [![Downloads](https://img.shields.io/npm/dm/@humanspeak/svelte-json-view-lite.svg)](https://www.npmjs.com/package/@humanspeak/svelte-json-view-lite)
@@ -252,11 +253,31 @@ pnpm test # vitest + coverage
252
253
  pnpm build # vite build + svelte-package + publint
253
254
  ```
254
255
 
256
+ <!-- docs-kit:ecosystem start -->
257
+
258
+ ## Svelte 5 ecosystem
259
+
260
+ Part of the [Humanspeak](https://humanspeak.com) family of runes-native Svelte 5 packages:
261
+
262
+ | Package | Description |
263
+ | --- | --- |
264
+ | [@humanspeak/svelte-markdown](https://markdown.svelte.page) | Runtime markdown renderer for Svelte |
265
+ | [@humanspeak/svelte-virtual-list](https://virtuallist.svelte.page) | Virtual scrolling for Svelte |
266
+ | [@humanspeak/svelte-motion](https://motion.svelte.page) | Framer Motion for Svelte 5 |
267
+ | [@humanspeak/svelte-headless-table](https://table.svelte.page) | Headless data tables for Svelte |
268
+ | [@humanspeak/svelte-diff-match-patch](https://diff.svelte.page) | Diff comparison for Svelte |
269
+ | [@humanspeak/svelte-purify](https://purify.svelte.page) | HTML sanitisation for Svelte |
270
+ | [@humanspeak/svelte-virtual-chat](https://virtualchat.svelte.page) | Virtual chat viewport for Svelte 5 |
271
+ | [@humanspeak/memory-cache](https://memory.svelte.page) | In-memory cache for TypeScript |
272
+ | **[@humanspeak/svelte-json-view-lite](https://jsonview.svelte.page)** — _this package_ | JSON tree viewer for Svelte 5 |
273
+ | [@humanspeak/svelte-scoped-props](https://scoped.svelte.page) | Scoped class props for Svelte |
274
+
255
275
  ## License
256
276
 
257
- MIT © [Humanspeak, Inc.](LICENSE) Original `react-json-view-lite`
258
- © 2024 AnyRoad.
277
+ MIT © [Humanspeak, Inc.](LICENSE)
259
278
 
260
279
  ## Credits
261
280
 
262
281
  Made with ❤️ by [Humanspeak](https://humanspeak.com)
282
+
283
+ <!-- docs-kit:ecosystem end -->
@@ -16,25 +16,18 @@
16
16
  const isArr = $derived(isArray(value))
17
17
  const isObj = $derived(isObject(value) && !isDate(value) && !isFunction(value))
18
18
 
19
- // Derive the children tuple array from `value` so ExpandableObject
20
- // receives a stable reference while `value` is unchanged — otherwise
21
- // a fresh array identity on every parent tick invalidates the
22
- // {#each} key and cascades re-renders down the tree.
23
- const children = $derived.by<Array<[string | undefined, unknown]>>(() => {
24
- if (isArr) return (value as unknown[]).map((el) => [undefined, el])
25
- if (isObj) {
26
- const obj = value as Record<string, unknown>
27
- return Object.keys(obj).map((k) => [k, obj[k]])
28
- }
29
- return []
30
- })
19
+ // The children tuple array is NOT built here: a collapsed node only needs
20
+ // its child *count*, and materializing `Array<[key, value]>` for every
21
+ // node (a 100k-element array = 100k throwaway tuples at mount under
22
+ // collapseAllNested) is pure waste. ExpandableObject derives the tuples
23
+ // lazily, gated on its own `expanded` state. See issue #21.
31
24
  </script>
32
25
 
33
26
  {#if isArr}
34
27
  <ExpandableObject
35
28
  {...props}
36
29
  value={value as unknown[]}
37
- data={children}
30
+ isArray={true}
38
31
  openBracket="["
39
32
  closeBracket="]"
40
33
  />
@@ -42,7 +35,7 @@
42
35
  <ExpandableObject
43
36
  {...props}
44
37
  value={value as object}
45
- data={children}
38
+ isArray={false}
46
39
  openBracket={OBJECT_OPEN}
47
40
  closeBracket={OBJECT_CLOSE}
48
41
  />
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { untrack } from 'svelte'
2
3
  import DataRender from './DataRender.svelte'
3
4
  import EmptyObject from './EmptyObject.svelte'
4
5
  import type { AriaLabels, ExpandableRenderProps } from './types.js'
@@ -7,7 +8,7 @@
7
8
  const {
8
9
  field,
9
10
  value,
10
- data,
11
+ isArray,
11
12
  lastElement,
12
13
  openBracket,
13
14
  closeBracket,
@@ -26,15 +27,32 @@
26
27
  // svelte-ignore state_referenced_locally
27
28
  let expanded = $state(shouldExpandNode(level, value, field))
28
29
 
30
+ // Once a node has been opened we keep its children materialized, even after
31
+ // it collapses again: re-deriving the tuple array (and re-reading N child
32
+ // values) on every re-expand would be its own waste. Latches true, stays true.
33
+ // svelte-ignore state_referenced_locally
34
+ let hasMaterialized = $state(expanded)
35
+
29
36
  let shouldExpandNodeCalled = false
30
37
 
38
+ // Single chokepoint for flipping expansion — every path (mount effect,
39
+ // click, keyboard) routes through here, so the materialization high-water
40
+ // mark can't be forgotten when a new expansion path is added.
41
+ function applyExpanded(next: boolean) {
42
+ expanded = next
43
+ if (next) hasMaterialized = true
44
+ }
45
+
31
46
  $effect(() => {
32
47
  const fn = shouldExpandNode
33
48
  if (!shouldExpandNodeCalled) {
34
49
  shouldExpandNodeCalled = true
35
50
  return
36
51
  }
37
- expanded = fn(level, value, field)
52
+ // Track only the callback identity: untrack level/value/field so an
53
+ // ancestor re-render that mutates them can't rerun this effect and
54
+ // overwrite the user's expansion state.
55
+ applyExpanded(untrack(() => fn(level, value, field)))
38
56
  })
39
57
 
40
58
  // SSR-stable id for aria-controls linkage.
@@ -54,16 +72,35 @@
54
72
  expanded ? activeAriaLabels.collapseJson : activeAriaLabels.expandJson
55
73
  )
56
74
  const childLevel = $derived(level + 1)
57
- const lastIndex = $derived(data.length - 1)
58
75
  const hasField = $derived(field !== undefined)
59
76
  const labelText = $derived(quoteString(field ?? '', style.quotesForFieldNames))
60
77
 
78
+ // Object keys resolved once and shared by the count and the tuple builder,
79
+ // so an expanded object node enumerates its keys a single time. Arrays skip
80
+ // this entirely — their length is free.
81
+ const objectKeys = $derived(isArray ? null : Object.keys(value as Record<string, unknown>))
82
+
83
+ // Child *count* is cheap — array length, or the key count for objects.
84
+ // Neither touches child *values*, so a collapsed node stays allocation-free.
85
+ const count = $derived(objectKeys ? objectKeys.length : (value as unknown[]).length)
86
+ const lastIndex = $derived(count - 1)
87
+
88
+ // The tuple array is materialized lazily on first open, then kept: a node
89
+ // that is never expanded never allocates its N tuples or reads its N child
90
+ // values (#21); one that has been opened doesn't re-read them on re-expand.
91
+ const entries = $derived.by<Array<[string | undefined, unknown]>>(() => {
92
+ if (!hasMaterialized) return []
93
+ if (isArray) return (value as unknown[]).map((el) => [undefined, el])
94
+ const obj = value as Record<string, unknown>
95
+ return (objectKeys as string[]).map((k) => [k, obj[k]])
96
+ })
97
+
61
98
  function setExpandWithCallback(newExpandValue: boolean) {
62
99
  if (expanded === newExpandValue) return
63
100
  if (beforeExpandChange && !beforeExpandChange({ level, value, field, newExpandValue })) {
64
101
  return
65
102
  }
66
- expanded = newExpandValue
103
+ applyExpanded(newExpandValue)
67
104
  }
68
105
 
69
106
  function onKeyDown(e: KeyboardEvent) {
@@ -102,7 +139,7 @@
102
139
  }
103
140
  </script>
104
141
 
105
- {#if data.length === 0}
142
+ {#if count === 0}
106
143
  <EmptyObject {field} {openBracket} {closeBracket} {lastElement} {style} />
107
144
  {:else}
108
145
  <div
@@ -131,7 +168,7 @@
131
168
  >{:else}<span class={style.label}>{labelText}:</span>{/if}{/if}<span
132
169
  class={style.punctuation}>{openBracket}</span
133
170
  >{#if expanded}<ul id={contentsId} role="group" class={style.childFieldsContainer}>
134
- {#each data as [childField, childValue], index (childField ?? index)}<DataRender
171
+ {#each entries as [childField, childValue], index (childField ?? index)}<DataRender
135
172
  field={childField}
136
173
  value={childValue}
137
174
  {style}
package/dist/types.d.ts CHANGED
@@ -132,7 +132,9 @@ export interface JsonRenderProps<T> extends CommonRenderProps {
132
132
  export interface ExpandableRenderProps extends CommonRenderProps {
133
133
  field?: string;
134
134
  value: object | unknown[];
135
- data: Array<[string | undefined, unknown]>;
135
+ /** Whether `value` is an array. Resolved once by DataRender (the type
136
+ * dispatcher) so ExpandableObject never re-tests the value's shape. */
137
+ isArray: boolean;
136
138
  openBracket: string;
137
139
  closeBracket: string;
138
140
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-json-view-lite",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Fast, tiny JSON tree viewer for Svelte 5 — port of react-json-view-lite with runes, SSR, snippet overrides, and zero runtime dependencies",
5
5
  "keywords": [
6
6
  "svelte",
@@ -57,37 +57,37 @@
57
57
  "devDependencies": {
58
58
  "@eslint/compat": "^2.1.0",
59
59
  "@eslint/js": "^10.0.1",
60
- "@playwright/test": "^1.60.0",
60
+ "@playwright/test": "^1.61.1",
61
61
  "@sveltejs/adapter-auto": "^7.0.1",
62
- "@sveltejs/kit": "^2.61.1",
63
- "@sveltejs/package": "^2.5.7",
62
+ "@sveltejs/kit": "^2.69.1",
63
+ "@sveltejs/package": "^2.5.8",
64
64
  "@sveltejs/vite-plugin-svelte": "^7.1.2",
65
65
  "@testing-library/jest-dom": "^6.9.1",
66
- "@testing-library/svelte": "^5.3.1",
66
+ "@testing-library/svelte": "^5.4.2",
67
67
  "@testing-library/user-event": "^14.6.1",
68
- "@types/node": "^25.9.1",
69
- "@typescript-eslint/eslint-plugin": "^8.60.0",
70
- "@typescript-eslint/parser": "^8.60.0",
71
- "@vitest/coverage-v8": "^4.1.7",
72
- "eslint": "^10.4.0",
68
+ "@types/node": "^26.1.0",
69
+ "@typescript-eslint/eslint-plugin": "^8.62.1",
70
+ "@typescript-eslint/parser": "^8.62.1",
71
+ "@vitest/coverage-v8": "^4.1.9",
72
+ "eslint": "^10.6.0",
73
73
  "eslint-config-prettier": "^10.1.8",
74
74
  "eslint-plugin-import": "^2.32.0",
75
- "eslint-plugin-svelte": "^3.18.0",
75
+ "eslint-plugin-svelte": "^3.20.0",
76
76
  "eslint-plugin-unused-imports": "^4.4.1",
77
- "globals": "^17.6.0",
77
+ "globals": "^17.7.0",
78
78
  "husky": "^9.1.7",
79
79
  "jsdom": "^29.1.1",
80
- "mprocs": "^0.9.3",
81
- "prettier": "^3.8.3",
80
+ "mprocs": "^0.9.6",
81
+ "prettier": "^3.9.4",
82
82
  "prettier-plugin-organize-imports": "^4.3.0",
83
- "prettier-plugin-svelte": "^4.0.1",
83
+ "prettier-plugin-svelte": "^4.1.1",
84
84
  "publint": "^0.3.21",
85
- "svelte": "^5.55.9",
86
- "svelte-check": "^4.4.8",
85
+ "svelte": "^5.56.4",
86
+ "svelte-check": "^4.7.1",
87
87
  "typescript": "^6.0.3",
88
- "typescript-eslint": "^8.60.0",
89
- "vite": "^8.0.14",
90
- "vitest": "^4.1.7"
88
+ "typescript-eslint": "^8.62.1",
89
+ "vite": "^8.1.3",
90
+ "vitest": "^4.1.9"
91
91
  },
92
92
  "peerDependencies": {
93
93
  "svelte": "^5.0.0"