@script-it/json-views-core 0.1.0-beta.1 → 2026.10.25
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 +3 -1
- package/dist/formats/json-format.js +65 -35
- package/dist/json-source-patcher.js +115 -45
- package/dist/metadata.d.ts +4 -0
- package/dist/metadata.js +150 -86
- package/dist/source-default.js +24 -2
- package/dist/table-suitability.d.ts +1 -1
- package/dist/table-suitability.js +16 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -51,6 +51,8 @@ The compiler returns the untouched effective `metadata`, its `metadataSource` (`
|
|
|
51
51
|
|
|
52
52
|
Only integer version `1` is supported. Other positive integer versions produce `unsupported-metadata-version`; malformed annotations produce `invalid-metadata`. The raw future metadata is retained for round trips and source editing. Consumers must not overwrite it with inferred metadata or version 1 settings.
|
|
53
53
|
|
|
54
|
+
Checking every document value against the schema is most of the compile time on large documents. Callers that only need the compiled schema and views, such as a route or settings panel, can pass `{ validateValues: false }`: the schema, views, and every other diagnostic are identical, and value diagnostics are omitted.
|
|
55
|
+
|
|
54
56
|
### Paths and precedence
|
|
55
57
|
|
|
56
58
|
All paths start at `$`. Supported segments are `.property`, `['quoted property']`, `[0]`, and `[*]`. Quoted properties support single-quote, backslash, slash, control-character, and `\uXXXX` escapes. Array indices are nonnegative safe integers without leading zeros. A numeric-looking object key is a string property, such as `$['0']`; it is different from `$[0]`.
|
|
@@ -161,7 +163,7 @@ Patching helpers preserve unrelated bytes. The agent-facing mutation API accepts
|
|
|
161
163
|
|
|
162
164
|
Compiled metadata includes the root and each view's `value` snapshot for direct rendering. `applyJsonViewViewRows`, `projectJsonViewCollection`, and `resolveJsonViewRowPath` resolve against their explicit current-root argument. `getJsonViewViewRows(view, root)` does the same; `getJsonViewViewRows(view)` intentionally reads the compiled snapshot. Recompile when annotations or registry behavior change, or to refresh diagnostics and inferred options for a new document revision.
|
|
163
165
|
|
|
164
|
-
The synchronous compiler and scanner have no universal document-size guarantee. Table virtualization limits mounted rows, not parsing, validation, projection, or Kanban work.
|
|
166
|
+
The synchronous compiler and scanner have no universal document-size guarantee. Table virtualization limits mounted rows, not parsing, validation, projection, or Kanban work. `inspectJsonSource` keeps the diagnostics of the most recently inspected text, so hosts that inspect one revision several times pay for one scan; applications handling very large documents should perform compilation/inspection in a worker and choose their own measured size budgets. The test workload covers 1,000 independent edits within a 10,000-row document, and comparator/adversarial-pattern regressions are retained in the core suite.
|
|
165
167
|
|
|
166
168
|
### Host acknowledgements and reloads
|
|
167
169
|
|
|
@@ -48,62 +48,92 @@ export function jsonFormatForSource(source) {
|
|
|
48
48
|
export function jsonFormatForRoot(root) {
|
|
49
49
|
return Array.isArray(root) ? jsonArrayFormat : jsonObjectFormat;
|
|
50
50
|
}
|
|
51
|
+
// Outside strings, valid JSON only contains these four whitespace characters.
|
|
52
|
+
function isJsonWhitespace(code) {
|
|
53
|
+
return code === 0x20 || code === 0x0a || code === 0x0d || code === 0x09;
|
|
54
|
+
}
|
|
55
|
+
function isStructural(code) {
|
|
56
|
+
return code === 0x22 || code === 0x7b || code === 0x5b || code === 0x7d || code === 0x5d || code === 0x2c || code === 0x3a;
|
|
57
|
+
}
|
|
51
58
|
/** Pretty-print JSON without changing numeric spelling, duplicate keys, or string contents. */
|
|
52
59
|
export function formatJsonSource(source) {
|
|
53
60
|
assertJsonDepth(JSON.parse(source));
|
|
54
|
-
|
|
55
|
-
let depth = 0;
|
|
56
|
-
let escaped = false;
|
|
57
|
-
let inString = false;
|
|
61
|
+
const parts = [];
|
|
58
62
|
const expandedContainers = [];
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
const length = source.length;
|
|
64
|
+
let depth = 0;
|
|
65
|
+
let indentation = '';
|
|
66
|
+
const indent = (next) => {
|
|
67
|
+
depth = next;
|
|
68
|
+
indentation = ' '.repeat(depth);
|
|
69
|
+
};
|
|
70
|
+
let index = 0;
|
|
71
|
+
while (index < length) {
|
|
72
|
+
const code = source.charCodeAt(index);
|
|
73
|
+
if (code === 0x22) {
|
|
74
|
+
// Strings are copied whole, escapes included.
|
|
75
|
+
let end = index + 1;
|
|
76
|
+
while (end < length) {
|
|
77
|
+
const inner = source.charCodeAt(end);
|
|
78
|
+
end += 1;
|
|
79
|
+
if (inner === 0x5c)
|
|
80
|
+
end += 1;
|
|
81
|
+
else if (inner === 0x22)
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
parts.push(source.slice(index, end));
|
|
85
|
+
index = end;
|
|
70
86
|
continue;
|
|
71
87
|
}
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
if (character === '"') {
|
|
75
|
-
inString = true;
|
|
76
|
-
formatted += character;
|
|
88
|
+
if (isJsonWhitespace(code)) {
|
|
89
|
+
index += 1;
|
|
77
90
|
continue;
|
|
78
91
|
}
|
|
79
|
-
if (
|
|
80
|
-
const
|
|
92
|
+
if (code === 0x7b || code === 0x5b) {
|
|
93
|
+
const closing = code === 0x7b ? 0x7d : 0x5d;
|
|
81
94
|
let nextIndex = index + 1;
|
|
82
|
-
while (nextIndex <
|
|
95
|
+
while (nextIndex < length && isJsonWhitespace(source.charCodeAt(nextIndex)))
|
|
83
96
|
nextIndex += 1;
|
|
84
|
-
const expanded = source
|
|
97
|
+
const expanded = source.charCodeAt(nextIndex) !== closing;
|
|
85
98
|
expandedContainers.push(expanded);
|
|
86
|
-
|
|
99
|
+
parts.push(source[index]);
|
|
87
100
|
if (expanded) {
|
|
88
|
-
depth
|
|
89
|
-
|
|
101
|
+
indent(depth + 1);
|
|
102
|
+
parts.push(`\n${indentation}`);
|
|
90
103
|
}
|
|
104
|
+
index += 1;
|
|
91
105
|
continue;
|
|
92
106
|
}
|
|
93
|
-
if (
|
|
107
|
+
if (code === 0x7d || code === 0x5d) {
|
|
94
108
|
const expanded = expandedContainers.pop() ?? false;
|
|
95
109
|
if (expanded) {
|
|
96
|
-
depth
|
|
97
|
-
|
|
110
|
+
indent(depth - 1);
|
|
111
|
+
parts.push(`\n${indentation}`);
|
|
98
112
|
}
|
|
99
|
-
|
|
113
|
+
parts.push(source[index]);
|
|
114
|
+
index += 1;
|
|
100
115
|
continue;
|
|
101
116
|
}
|
|
102
|
-
if (
|
|
103
|
-
|
|
117
|
+
if (code === 0x2c) {
|
|
118
|
+
parts.push(`,\n${indentation}`);
|
|
119
|
+
index += 1;
|
|
104
120
|
continue;
|
|
105
121
|
}
|
|
106
|
-
|
|
122
|
+
if (code === 0x3a) {
|
|
123
|
+
parts.push(': ');
|
|
124
|
+
index += 1;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// Numbers and literals are copied as one run.
|
|
128
|
+
let end = index + 1;
|
|
129
|
+
while (end < length) {
|
|
130
|
+
const inner = source.charCodeAt(end);
|
|
131
|
+
if (isStructural(inner) || isJsonWhitespace(inner))
|
|
132
|
+
break;
|
|
133
|
+
end += 1;
|
|
134
|
+
}
|
|
135
|
+
parts.push(source.slice(index, end));
|
|
136
|
+
index = end;
|
|
107
137
|
}
|
|
108
|
-
return
|
|
138
|
+
return parts.join('');
|
|
109
139
|
}
|
|
@@ -15,6 +15,25 @@ function assertSameEditableAtomicType(current, next, path) {
|
|
|
15
15
|
throw new TypeError(`Replacement at ${describeValuePath(path)} must keep type ${currentType}`);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
// Character codes the scanner dispatches on. The text is already valid JSON.
|
|
19
|
+
const QUOTE = 0x22;
|
|
20
|
+
const COMMA = 0x2c;
|
|
21
|
+
const MINUS = 0x2d;
|
|
22
|
+
const COLON = 0x3a;
|
|
23
|
+
const OPEN_BRACKET = 0x5b;
|
|
24
|
+
const BACKSLASH = 0x5c;
|
|
25
|
+
const CLOSE_BRACKET = 0x5d;
|
|
26
|
+
const OPEN_BRACE = 0x7b;
|
|
27
|
+
const CLOSE_BRACE = 0x7d;
|
|
28
|
+
function isJsonWhitespace(code) {
|
|
29
|
+
return code === 0x20 || code === 0x0a || code === 0x0d || code === 0x09;
|
|
30
|
+
}
|
|
31
|
+
function isDigit(code) {
|
|
32
|
+
return code >= 0x30 && code <= 0x39;
|
|
33
|
+
}
|
|
34
|
+
function isNumberCharacter(code) {
|
|
35
|
+
return isDigit(code) || code === 0x2e || code === MINUS || code === 0x2b || code === 0x65 || code === 0x45;
|
|
36
|
+
}
|
|
18
37
|
class JsonSourceScanner {
|
|
19
38
|
index = 0;
|
|
20
39
|
source;
|
|
@@ -23,6 +42,7 @@ class JsonSourceScanner {
|
|
|
23
42
|
}
|
|
24
43
|
parse() {
|
|
25
44
|
// Callers validate once with JSON.parse before scanning source spans.
|
|
45
|
+
const source = this.source;
|
|
26
46
|
const root = this.parseValue();
|
|
27
47
|
const frames = [];
|
|
28
48
|
const push = (node, property) => {
|
|
@@ -37,8 +57,8 @@ class JsonSourceScanner {
|
|
|
37
57
|
const frame = frames[frames.length - 1];
|
|
38
58
|
const node = frame.node;
|
|
39
59
|
this.skipWhitespace();
|
|
40
|
-
const close = node.kind === 'array' ?
|
|
41
|
-
if (
|
|
60
|
+
const close = node.kind === 'array' ? CLOSE_BRACKET : CLOSE_BRACE;
|
|
61
|
+
if (source.charCodeAt(this.index) === close) {
|
|
42
62
|
node.end = ++this.index;
|
|
43
63
|
if (frame.property)
|
|
44
64
|
frame.property.end = node.end;
|
|
@@ -46,7 +66,7 @@ class JsonSourceScanner {
|
|
|
46
66
|
continue;
|
|
47
67
|
}
|
|
48
68
|
if (frame.afterValue) {
|
|
49
|
-
if (
|
|
69
|
+
if (source.charCodeAt(this.index++) !== COMMA)
|
|
50
70
|
throw new SyntaxError('Expected comma');
|
|
51
71
|
this.skipWhitespace();
|
|
52
72
|
}
|
|
@@ -58,10 +78,12 @@ class JsonSourceScanner {
|
|
|
58
78
|
}
|
|
59
79
|
else {
|
|
60
80
|
const start = this.index;
|
|
61
|
-
|
|
62
|
-
const key =
|
|
81
|
+
// A key without escapes is its own text; JSON.parse decodes the rest.
|
|
82
|
+
const key = this.scanString()
|
|
83
|
+
? JSON.parse(source.slice(start, this.index))
|
|
84
|
+
: source.slice(start + 1, this.index - 1);
|
|
63
85
|
this.skipWhitespace();
|
|
64
|
-
if (
|
|
86
|
+
if (source.charCodeAt(this.index++) !== COLON)
|
|
65
87
|
throw new SyntaxError('Expected colon');
|
|
66
88
|
const value = this.parseValue();
|
|
67
89
|
const property = { key, start, end: value.end, value };
|
|
@@ -71,50 +93,61 @@ class JsonSourceScanner {
|
|
|
71
93
|
}
|
|
72
94
|
}
|
|
73
95
|
this.skipWhitespace();
|
|
74
|
-
if (this.index !==
|
|
96
|
+
if (this.index !== source.length)
|
|
75
97
|
throw new SyntaxError('Unexpected content after JSON value');
|
|
76
98
|
return root;
|
|
77
99
|
}
|
|
78
100
|
skipWhitespace() {
|
|
79
|
-
|
|
80
|
-
|
|
101
|
+
const source = this.source;
|
|
102
|
+
let index = this.index;
|
|
103
|
+
while (index < source.length && isJsonWhitespace(source.charCodeAt(index)))
|
|
104
|
+
index += 1;
|
|
105
|
+
this.index = index;
|
|
81
106
|
}
|
|
82
107
|
parseValue() {
|
|
83
108
|
this.skipWhitespace();
|
|
84
109
|
const start = this.index;
|
|
85
|
-
const
|
|
86
|
-
if (
|
|
87
|
-
this.index
|
|
110
|
+
const code = this.source.charCodeAt(start);
|
|
111
|
+
if (code === OPEN_BRACE) {
|
|
112
|
+
this.index += 1;
|
|
88
113
|
return { start, end: this.index, kind: 'object', properties: [], propertyIndex: new Map() };
|
|
89
114
|
}
|
|
90
|
-
if (
|
|
91
|
-
this.index
|
|
115
|
+
if (code === OPEN_BRACKET) {
|
|
116
|
+
this.index += 1;
|
|
92
117
|
return { start, end: this.index, kind: 'array', items: [] };
|
|
93
118
|
}
|
|
94
|
-
if (
|
|
119
|
+
if (code === QUOTE) {
|
|
95
120
|
this.scanString();
|
|
96
121
|
return { start, end: this.index, kind: 'atomic' };
|
|
97
122
|
}
|
|
98
|
-
if (
|
|
123
|
+
if (code === 0x74)
|
|
99
124
|
return this.scanLiteral(start, 'true');
|
|
100
|
-
if (
|
|
125
|
+
if (code === 0x66)
|
|
101
126
|
return this.scanLiteral(start, 'false');
|
|
102
|
-
if (
|
|
127
|
+
if (code === 0x6e)
|
|
103
128
|
return this.scanLiteral(start, 'null');
|
|
104
129
|
return this.scanNumber(start);
|
|
105
130
|
}
|
|
131
|
+
/** Moves past the closing quote and reports whether the string used escapes. */
|
|
106
132
|
scanString() {
|
|
107
|
-
|
|
133
|
+
const source = this.source;
|
|
134
|
+
if (source.charCodeAt(this.index) !== QUOTE)
|
|
108
135
|
throw new SyntaxError('Expected JSON string');
|
|
109
|
-
this.index
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
136
|
+
let index = this.index + 1;
|
|
137
|
+
let escaped = false;
|
|
138
|
+
while (index < source.length) {
|
|
139
|
+
const code = source.charCodeAt(index);
|
|
140
|
+
index += 1;
|
|
141
|
+
if (code === QUOTE) {
|
|
142
|
+
this.index = index;
|
|
143
|
+
return escaped;
|
|
144
|
+
}
|
|
145
|
+
if (code === BACKSLASH) {
|
|
146
|
+
escaped = true;
|
|
147
|
+
index += 1;
|
|
148
|
+
}
|
|
117
149
|
}
|
|
150
|
+
this.index = index;
|
|
118
151
|
throw new SyntaxError('Unterminated JSON string');
|
|
119
152
|
}
|
|
120
153
|
scanLiteral(start, literal) {
|
|
@@ -122,11 +155,14 @@ class JsonSourceScanner {
|
|
|
122
155
|
return { start, end: this.index, kind: 'atomic' };
|
|
123
156
|
}
|
|
124
157
|
scanNumber(start) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
158
|
+
const source = this.source;
|
|
159
|
+
let index = start;
|
|
160
|
+
while (index < source.length && isNumberCharacter(source.charCodeAt(index)))
|
|
161
|
+
index += 1;
|
|
162
|
+
if (index === start)
|
|
128
163
|
throw new SyntaxError('Expected JSON value');
|
|
129
|
-
|
|
164
|
+
this.index = index;
|
|
165
|
+
return { start, end: index, kind: 'atomic' };
|
|
130
166
|
}
|
|
131
167
|
}
|
|
132
168
|
function sourceNodeAtPath(root, path) {
|
|
@@ -229,9 +265,26 @@ function serializeReplacement(next, path, space = 0) {
|
|
|
229
265
|
throw new TypeError(`Replacement at ${describeValuePath(path)} must be valid JSON data`, { cause: error });
|
|
230
266
|
}
|
|
231
267
|
}
|
|
232
|
-
/**
|
|
233
|
-
|
|
234
|
-
const
|
|
268
|
+
/** Paths are rebuilt from parent links only for the few nodes that report something. */
|
|
269
|
+
function inspectionPath(frame) {
|
|
270
|
+
const path = [];
|
|
271
|
+
for (let current = frame; current.parent; current = current.parent)
|
|
272
|
+
path.push(current.segment);
|
|
273
|
+
return path.reverse();
|
|
274
|
+
}
|
|
275
|
+
/** Integers of at most 15 digits round-trip exactly; -0 is the one exception. */
|
|
276
|
+
function isPlainSafeInteger(token) {
|
|
277
|
+
let index = token.charCodeAt(0) === MINUS ? 1 : 0;
|
|
278
|
+
if (token.length - index < 1 || token.length - index > 15)
|
|
279
|
+
return false;
|
|
280
|
+
if (index === 1 && token.length === 2 && token.charCodeAt(1) === 0x30)
|
|
281
|
+
return false;
|
|
282
|
+
for (; index < token.length; index += 1)
|
|
283
|
+
if (!isDigit(token.charCodeAt(index)))
|
|
284
|
+
return false;
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
function inspectSourceDiagnostics(source) {
|
|
235
288
|
const tree = new JsonSourceScanner(source).parse();
|
|
236
289
|
const diagnostics = [];
|
|
237
290
|
const decimalKey = (token) => {
|
|
@@ -245,38 +298,42 @@ export function inspectJsonSource(source) {
|
|
|
245
298
|
const significant = digits.replace(/0+$/, '');
|
|
246
299
|
return `${negative ? '-' : ''}${significant}e${Number(exponent) - fraction.length + digits.length - significant.length}`;
|
|
247
300
|
};
|
|
248
|
-
const pending = [{ node: tree,
|
|
301
|
+
const pending = [{ node: tree, parent: undefined, segment: '', shadowed: false }];
|
|
249
302
|
while (pending.length) {
|
|
250
|
-
const
|
|
303
|
+
const frame = pending.pop();
|
|
304
|
+
const { node, shadowed } = frame;
|
|
251
305
|
if (node.kind === 'object') {
|
|
252
306
|
const seen = new Set();
|
|
253
|
-
for (const property of node.properties
|
|
254
|
-
const
|
|
255
|
-
const
|
|
307
|
+
for (const property of node.properties) {
|
|
308
|
+
const childShadowed = shadowed || node.propertyIndex.get(property.key) !== property.value;
|
|
309
|
+
const child = { node: property.value, parent: frame, segment: property.key, shadowed: childShadowed };
|
|
256
310
|
if (seen.has(property.key))
|
|
257
311
|
diagnostics.push({
|
|
258
|
-
code: 'duplicate-key', sourcePath:
|
|
312
|
+
code: 'duplicate-key', sourcePath: inspectionPath(child), token: JSON.stringify(property.key),
|
|
259
313
|
...(childShadowed ? { shadowed: true } : {}),
|
|
260
314
|
start: property.start, end: property.end,
|
|
261
315
|
message: 'Duplicate object key; structured inspection shows the last occurrence. Source retains every occurrence.',
|
|
262
316
|
});
|
|
263
317
|
seen.add(property.key);
|
|
264
|
-
pending.push(
|
|
318
|
+
pending.push(child);
|
|
265
319
|
}
|
|
266
320
|
}
|
|
267
321
|
else if (node.kind === 'array') {
|
|
268
|
-
node.items
|
|
322
|
+
node.items.forEach((item, index) => pending.push({ node: item, parent: frame, segment: index, shadowed }));
|
|
269
323
|
}
|
|
270
324
|
else {
|
|
325
|
+
const first = source.charCodeAt(node.start);
|
|
326
|
+
if (first !== MINUS && !isDigit(first))
|
|
327
|
+
continue;
|
|
271
328
|
const token = source.slice(node.start, node.end);
|
|
272
|
-
if (
|
|
329
|
+
if (isPlainSafeInteger(token))
|
|
273
330
|
continue;
|
|
274
331
|
const number = Number(token);
|
|
275
332
|
if (!Number.isFinite(number) || Object.is(number, -0)
|
|
276
333
|
|| (Number.isInteger(number) && !Number.isSafeInteger(number))
|
|
277
334
|
|| decimalKey(token) !== decimalKey(String(number))) {
|
|
278
335
|
diagnostics.push({
|
|
279
|
-
code: 'unsafe-number', sourcePath:
|
|
336
|
+
code: 'unsafe-number', sourcePath: inspectionPath(frame), token, start: node.start, end: node.end,
|
|
280
337
|
...(shadowed ? { shadowed: true } : {}),
|
|
281
338
|
message: 'This numeric token cannot be safely edited through JavaScript numbers. Edit its exact value in Source.',
|
|
282
339
|
});
|
|
@@ -284,7 +341,20 @@ export function inspectJsonSource(source) {
|
|
|
284
341
|
}
|
|
285
342
|
}
|
|
286
343
|
diagnostics.sort((a, b) => a.start - b.start);
|
|
287
|
-
return
|
|
344
|
+
return diagnostics;
|
|
345
|
+
}
|
|
346
|
+
let lastInspection;
|
|
347
|
+
/** Reads a source-preserving inspection value and identifies information JSON.parse cannot retain. */
|
|
348
|
+
export function inspectJsonSource(source) {
|
|
349
|
+
const value = JSON.parse(source);
|
|
350
|
+
// Hosts inspect one text several times per change. The scan is the expensive
|
|
351
|
+
// part, so its result is kept for the most recent source; callers still get
|
|
352
|
+
// their own value, diagnostic objects, and paths.
|
|
353
|
+
const cached = lastInspection !== undefined && lastInspection.source === source ? lastInspection.diagnostics : undefined;
|
|
354
|
+
const diagnostics = cached ?? inspectSourceDiagnostics(source);
|
|
355
|
+
if (!cached)
|
|
356
|
+
lastInspection = { source, diagnostics };
|
|
357
|
+
return { value, diagnostics: diagnostics.map((item) => ({ ...item, sourcePath: [...item.sourcePath], help: sourceDiagnosticHelp(item.code) })) };
|
|
288
358
|
}
|
|
289
359
|
/** Replaces one existing JSON value while preserving every unrelated source byte. */
|
|
290
360
|
export function replaceJsonValueInSource(source, path, next) {
|
package/dist/metadata.d.ts
CHANGED
|
@@ -133,5 +133,9 @@ export interface CompileJsonViewMetadataOptions {
|
|
|
133
133
|
metadata?: unknown;
|
|
134
134
|
/** Temporarily hides required-value diagnostics below paths for unfinished record drafts. */
|
|
135
135
|
suppressRequiredPaths?: readonly ValuePath[];
|
|
136
|
+
/** `false` skips checking every document value against the schema, which is
|
|
137
|
+
* most of the compile time on large documents. The compiled schema, views, and
|
|
138
|
+
* all other diagnostics are unchanged; value diagnostics are omitted. */
|
|
139
|
+
validateValues?: boolean;
|
|
136
140
|
}
|
|
137
141
|
export declare function compileJsonViewMetadata(root: unknown, typeRegistry?: JsonViewTypeRegistry, options?: CompileJsonViewMetadataOptions): CompiledJsonViewMetadata;
|
package/dist/metadata.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MAX_HTML_VIEW_BYTES } from './html-view-limits.js';
|
|
2
2
|
import { assertJsonDepth } from './json-limits.js';
|
|
3
|
-
import { getValueAtPath, VALUE_PATH_MISSING } from './json-path.js';
|
|
3
|
+
import { getValueAtPath, VALUE_PATH_MISSING, valuePathKey } from './json-path.js';
|
|
4
4
|
import { compileSafePattern } from './safe-pattern.js';
|
|
5
5
|
import { defaultTypeRegistry, JSON_VIEW_OPTION_COLORS } from './type-registry.js';
|
|
6
6
|
import { compareDateValues, parseDateValue } from './date-type.js';
|
|
@@ -369,23 +369,34 @@ function canonicalValue(value) {
|
|
|
369
369
|
return JSON.stringify(value);
|
|
370
370
|
}
|
|
371
371
|
export function matchesJsonViewPath(path, sourcePath) {
|
|
372
|
-
|
|
372
|
+
const segments = path.segments;
|
|
373
|
+
if (path.root !== '$' || segments.length !== sourcePath.length)
|
|
373
374
|
return false;
|
|
374
|
-
|
|
375
|
-
const
|
|
375
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
376
|
+
const segment = segments[index];
|
|
376
377
|
if (segment.kind === 'wildcard')
|
|
377
|
-
|
|
378
|
-
if (segment.kind === 'property')
|
|
379
|
-
return
|
|
380
|
-
|
|
381
|
-
|
|
378
|
+
continue;
|
|
379
|
+
if ((segment.kind === 'property' ? segment.key : segment.index) !== sourcePath[index])
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
/** More concrete segments win, then longer paths, then the earlier declaration. */
|
|
385
|
+
function outranks(candidate, current) {
|
|
386
|
+
if (candidate.specificity[0] !== current.specificity[0])
|
|
387
|
+
return candidate.specificity[0] > current.specificity[0];
|
|
388
|
+
if (candidate.specificity[1] !== current.specificity[1])
|
|
389
|
+
return candidate.specificity[1] > current.specificity[1];
|
|
390
|
+
return candidate.declarationIndex < current.declarationIndex;
|
|
382
391
|
}
|
|
383
392
|
export function schemaForJsonViewPath(schema, sourcePath) {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
393
|
+
// Keeps the earliest of the best-ranked matches, exactly as a stable sort would.
|
|
394
|
+
let winner;
|
|
395
|
+
for (const entry of schema) {
|
|
396
|
+
if (matchesJsonViewPath(entry.path, sourcePath) && (!winner || outranks(entry, winner)))
|
|
397
|
+
winner = entry;
|
|
398
|
+
}
|
|
399
|
+
return winner;
|
|
389
400
|
}
|
|
390
401
|
function parseDeclaredPath(value, expectedRoot, allowWildcard, metadataPath, diagnostics, scope, code, details = {}) {
|
|
391
402
|
if (typeof value !== 'string') {
|
|
@@ -445,20 +456,20 @@ function compileSchema(value, diagnostics, typeRegistry) {
|
|
|
445
456
|
}
|
|
446
457
|
function inferSchemaOptions(root, schema) {
|
|
447
458
|
for (const entry of schema) {
|
|
448
|
-
|
|
459
|
+
const type = entry.descriptor.type;
|
|
460
|
+
if (type !== 'select' && type !== 'multi-select')
|
|
449
461
|
continue;
|
|
450
462
|
const options = new Set(entry.descriptor.options ?? []);
|
|
451
|
-
|
|
452
|
-
if (
|
|
453
|
-
options.add(
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
options.add(value);
|
|
463
|
+
visitSchemaLocations(root, entry, (value) => {
|
|
464
|
+
if (type === 'select' && typeof value === 'string' && value !== '')
|
|
465
|
+
options.add(value);
|
|
466
|
+
if (type === 'multi-select' && Array.isArray(value)) {
|
|
467
|
+
value.forEach((item) => {
|
|
468
|
+
if (typeof item === 'string' && item !== '')
|
|
469
|
+
options.add(item);
|
|
459
470
|
});
|
|
460
471
|
}
|
|
461
|
-
}
|
|
472
|
+
});
|
|
462
473
|
entry.descriptor = { ...entry.descriptor, options: Array.from(options) };
|
|
463
474
|
}
|
|
464
475
|
}
|
|
@@ -622,10 +633,15 @@ function validateViewField(root, rows, viewPath, fieldPath, metadataPath, diagno
|
|
|
622
633
|
}
|
|
623
634
|
if (rows.length === 0)
|
|
624
635
|
return;
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
636
|
+
// Only a field missing from every record is reported, so stop at the first record that has it.
|
|
637
|
+
let first;
|
|
638
|
+
for (const row of rows) {
|
|
639
|
+
const resolved = resolveJsonViewRowPath(root, row, fieldPath);
|
|
640
|
+
if (resolved.value !== JSON_VIEW_PATH_MISSING)
|
|
641
|
+
return;
|
|
642
|
+
first ??= resolved;
|
|
628
643
|
}
|
|
644
|
+
diagnostic(diagnostics, 'view', code, `Path ${JSON.stringify(fieldPath.source)} does not resolve on any record selected by ${JSON.stringify(viewPath.source)}`, metadataPath, { viewId, sourcePath: first?.sourcePath, help: fieldHelp() });
|
|
629
645
|
}
|
|
630
646
|
function reserveViewId(raw, declarationIndex, usedIds) {
|
|
631
647
|
const requested = typeof raw.id === 'string' && raw.id.trim() ? raw.id.trim() : `view-${declarationIndex + 1}`;
|
|
@@ -798,93 +814,140 @@ function compileViews(root, value, diagnostics, schema, typeRegistry) {
|
|
|
798
814
|
}];
|
|
799
815
|
});
|
|
800
816
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
817
|
+
/** Visits every location a schema path selects, in document order, without
|
|
818
|
+
* allocating a path per location. Missing data is visited once at its deepest
|
|
819
|
+
* concrete prefix.
|
|
820
|
+
*/
|
|
821
|
+
function visitSchemaLocations(root, entry, visit) {
|
|
822
|
+
const segments = entry.path.segments;
|
|
823
|
+
const stack = [];
|
|
824
|
+
let snapshot;
|
|
825
|
+
const path = () => snapshot ??= stack.slice();
|
|
826
|
+
const report = (value, complete) => {
|
|
827
|
+
snapshot = undefined;
|
|
828
|
+
visit(value, path, complete);
|
|
829
|
+
};
|
|
830
|
+
const missing = (segmentIndex) => {
|
|
831
|
+
let pushed = 0;
|
|
832
|
+
for (let index = segmentIndex; index < segments.length; index += 1) {
|
|
833
|
+
const part = segments[index];
|
|
834
|
+
if (part.kind === 'wildcard')
|
|
835
|
+
break;
|
|
836
|
+
stack.push(part.kind === 'property' ? part.key : part.index);
|
|
837
|
+
pushed += 1;
|
|
838
|
+
}
|
|
839
|
+
report(JSON_VIEW_PATH_MISSING, stack.length === segments.length);
|
|
840
|
+
stack.length -= pushed;
|
|
841
|
+
};
|
|
842
|
+
const descend = (value, segmentIndex) => {
|
|
843
|
+
if (segmentIndex === segments.length) {
|
|
844
|
+
report(value, true);
|
|
806
845
|
return;
|
|
807
846
|
}
|
|
808
|
-
const segment =
|
|
809
|
-
const missing = () => {
|
|
810
|
-
const remainder = entry.path.segments.slice(segmentIndex);
|
|
811
|
-
const concrete = [...sourcePath];
|
|
812
|
-
for (const part of remainder) {
|
|
813
|
-
if (part.kind === 'wildcard')
|
|
814
|
-
break;
|
|
815
|
-
concrete.push(part.kind === 'property' ? part.key : part.index);
|
|
816
|
-
}
|
|
817
|
-
results.push({ value: JSON_VIEW_PATH_MISSING, sourcePath: concrete });
|
|
818
|
-
};
|
|
847
|
+
const segment = segments[segmentIndex];
|
|
819
848
|
if (segment.kind === 'property') {
|
|
820
|
-
if (!isRecord(value)) {
|
|
821
|
-
missing();
|
|
849
|
+
if (!isRecord(value) || !own(value, segment.key)) {
|
|
850
|
+
missing(segmentIndex);
|
|
822
851
|
return;
|
|
823
852
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
missing();
|
|
853
|
+
stack.push(segment.key);
|
|
854
|
+
descend(value[segment.key], segmentIndex + 1);
|
|
855
|
+
stack.pop();
|
|
828
856
|
return;
|
|
829
857
|
}
|
|
830
858
|
if (segment.kind === 'index') {
|
|
831
|
-
if (!Array.isArray(value)) {
|
|
832
|
-
missing();
|
|
859
|
+
if (!Array.isArray(value) || !own(value, String(segment.index))) {
|
|
860
|
+
missing(segmentIndex);
|
|
833
861
|
return;
|
|
834
862
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
missing();
|
|
863
|
+
stack.push(segment.index);
|
|
864
|
+
descend(value[segment.index], segmentIndex + 1);
|
|
865
|
+
stack.pop();
|
|
839
866
|
return;
|
|
840
867
|
}
|
|
841
868
|
if (Array.isArray(value)) {
|
|
842
|
-
value.forEach((child, index) =>
|
|
869
|
+
value.forEach((child, index) => {
|
|
870
|
+
stack.push(index);
|
|
871
|
+
descend(child, segmentIndex + 1);
|
|
872
|
+
stack.pop();
|
|
873
|
+
});
|
|
843
874
|
return;
|
|
844
875
|
}
|
|
845
|
-
if (isRecord(value)) {
|
|
846
|
-
|
|
876
|
+
if (!isRecord(value)) {
|
|
877
|
+
missing(segmentIndex);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
for (const key of Object.keys(value)) {
|
|
881
|
+
stack.push(key);
|
|
882
|
+
descend(value[key], segmentIndex + 1);
|
|
883
|
+
stack.pop();
|
|
847
884
|
}
|
|
848
|
-
else
|
|
849
|
-
missing();
|
|
850
885
|
};
|
|
851
|
-
|
|
852
|
-
|
|
886
|
+
descend(root, 0);
|
|
887
|
+
}
|
|
888
|
+
function underAnyPath(paths, sourcePath) {
|
|
889
|
+
return paths.some((path) => path.length <= sourcePath.length && path.every((part, index) => part === sourcePath[index]));
|
|
853
890
|
}
|
|
854
|
-
function validateResolvedSchemaValue(value, descriptor, entry,
|
|
855
|
-
|
|
891
|
+
function validateResolvedSchemaValue(value, descriptor, entry, path, diagnostics, typeRegistry, suppressRequiredPaths) {
|
|
892
|
+
// Most values are valid, so the path is only materialized for the ones that report.
|
|
893
|
+
const suppressed = (sourcePath) => suppressRequiredPaths.length > 0 && underAnyPath(suppressRequiredPaths, sourcePath);
|
|
856
894
|
if (value === JSON_VIEW_PATH_MISSING) {
|
|
857
|
-
if (descriptor.required
|
|
858
|
-
|
|
895
|
+
if (!descriptor.required)
|
|
896
|
+
return;
|
|
897
|
+
const sourcePath = path();
|
|
898
|
+
if (!suppressed(sourcePath)) {
|
|
899
|
+
diagnostic(diagnostics, 'value', 'required-value-missing', 'Required value is missing', entry.metadataPath, { declaration: entry.declaration, sourcePath });
|
|
859
900
|
}
|
|
860
901
|
return;
|
|
861
902
|
}
|
|
862
903
|
const issue = typeRegistry.validate(value, descriptor);
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
904
|
+
if (issue) {
|
|
905
|
+
const sourcePath = path();
|
|
906
|
+
if (!(issue === 'A value is required' && suppressed(sourcePath))) {
|
|
907
|
+
diagnostic(diagnostics, 'value', 'invalid-typed-value', issue, entry.metadataPath, { declaration: entry.declaration, sourcePath });
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
867
910
|
}
|
|
868
|
-
|
|
869
|
-
|
|
911
|
+
const warnings = typeRegistry.warnings(value, descriptor);
|
|
912
|
+
if (warnings.length === 0)
|
|
913
|
+
return;
|
|
914
|
+
const sourcePath = path();
|
|
915
|
+
for (const warning of warnings) {
|
|
916
|
+
diagnostic(diagnostics, 'value', 'typed-value-warning', warning, entry.metadataPath, { declaration: entry.declaration, sourcePath, severity: 'warning' });
|
|
870
917
|
}
|
|
871
918
|
}
|
|
872
919
|
function validateSchemaValues(root, schema, diagnostics, typeRegistry, suppressRequiredPaths) {
|
|
920
|
+
// A complete location has one winner among the entries of its length, so it
|
|
921
|
+
// can only be claimed away from this entry by an overlapping higher-ranked
|
|
922
|
+
// entry. Cut-off locations are the only ones several entries can share.
|
|
873
923
|
const visited = new Set();
|
|
874
924
|
for (const entry of schema) {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
925
|
+
const rivals = schema.filter((other) => other !== entry
|
|
926
|
+
&& other.path.segments.length === entry.path.segments.length
|
|
927
|
+
&& outranks(other, entry) && pathsOverlap(other.path, entry.path));
|
|
928
|
+
visitSchemaLocations(root, entry, (value, path, complete) => {
|
|
929
|
+
if (complete) {
|
|
930
|
+
if (rivals.length > 0) {
|
|
931
|
+
const sourcePath = path();
|
|
932
|
+
if (rivals.some((rival) => matchesJsonViewPath(rival.path, sourcePath)))
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
else {
|
|
937
|
+
const sourcePath = path();
|
|
938
|
+
const winner = schemaForJsonViewPath(schema, sourcePath);
|
|
939
|
+
if (winner && winner !== entry)
|
|
940
|
+
return;
|
|
941
|
+
const visitKey = valuePathKey(sourcePath);
|
|
942
|
+
if (visited.has(visitKey))
|
|
943
|
+
return;
|
|
944
|
+
visited.add(visitKey);
|
|
945
|
+
}
|
|
946
|
+
validateResolvedSchemaValue(value, entry.descriptor, entry, path, diagnostics, typeRegistry, suppressRequiredPaths);
|
|
947
|
+
});
|
|
885
948
|
}
|
|
886
949
|
}
|
|
887
|
-
function compileMetadataValue(root, metadata, typeRegistry, recognized, inference, metadataSource = 'embedded', suppressRequiredPaths = []) {
|
|
950
|
+
function compileMetadataValue(root, metadata, typeRegistry, recognized, inference, metadataSource = 'embedded', suppressRequiredPaths = [], validateValues = true) {
|
|
888
951
|
const diagnostics = [];
|
|
889
952
|
diagnoseUnknownProperties(metadata, ANNOTATION_PROPERTIES, ['$jsonviews'], diagnostics, 'metadata', 'unknown-annotation-property');
|
|
890
953
|
const schema = compileSchema(metadata.schema, diagnostics, typeRegistry);
|
|
@@ -899,7 +962,7 @@ function compileMetadataValue(root, metadata, typeRegistry, recognized, inferenc
|
|
|
899
962
|
// Inference is a presentation hint, not a user-authored validation contract.
|
|
900
963
|
// A sampled collection can contain later values that do not match the guessed
|
|
901
964
|
// type, and those values must not become upload-time errors.
|
|
902
|
-
if (metadataSource !== 'inferred') {
|
|
965
|
+
if (metadataSource !== 'inferred' && validateValues) {
|
|
903
966
|
validateSchemaValues(root, schema, diagnostics, typeRegistry, suppressRequiredPaths);
|
|
904
967
|
}
|
|
905
968
|
return {
|
|
@@ -918,12 +981,13 @@ function compileMetadataValue(root, metadata, typeRegistry, recognized, inferenc
|
|
|
918
981
|
export function compileJsonViewMetadata(root, typeRegistry = defaultTypeRegistry, options = {}) {
|
|
919
982
|
assertJsonDepth(root);
|
|
920
983
|
assertJsonDepth(options.metadata);
|
|
984
|
+
const validateValues = options.validateValues !== false;
|
|
921
985
|
const inactive = () => ({
|
|
922
986
|
root, status: 'none', recognized: false, active: false, schema: [], views: [], diagnostics: [],
|
|
923
987
|
});
|
|
924
988
|
const supplied = (metadata, metadataSource) => {
|
|
925
989
|
if (isRecord(metadata) && metadata.version === 1) {
|
|
926
|
-
return compileMetadataValue(root, metadata, typeRegistry, true, undefined, metadataSource, options.suppressRequiredPaths);
|
|
990
|
+
return compileMetadataValue(root, metadata, typeRegistry, true, undefined, metadataSource, options.suppressRequiredPaths, validateValues);
|
|
927
991
|
}
|
|
928
992
|
const unsupported = isRecord(metadata) && Number.isInteger(metadata.version) && Number(metadata.version) > 0;
|
|
929
993
|
return {
|
|
@@ -948,5 +1012,5 @@ export function compileJsonViewMetadata(root, typeRegistry = defaultTypeRegistry
|
|
|
948
1012
|
const inference = inferJsonViewMetadata(root);
|
|
949
1013
|
if (Object.keys(inference.schema).length === 0 && inference.views.length === 0)
|
|
950
1014
|
return inactive();
|
|
951
|
-
return compileMetadataValue(root, inference, typeRegistry, false, inference, 'inferred', options.suppressRequiredPaths);
|
|
1015
|
+
return compileMetadataValue(root, inference, typeRegistry, false, inference, 'inferred', options.suppressRequiredPaths, validateValues);
|
|
952
1016
|
}
|
package/dist/source-default.js
CHANGED
|
@@ -1,17 +1,39 @@
|
|
|
1
1
|
import { compileJsonViewMetadata } from './metadata.js';
|
|
2
2
|
import { isJsonViewTableCandidate } from './table-suitability.js';
|
|
3
|
+
/** Counts scalar values, stopping once the count passes the limit. */
|
|
4
|
+
function countLeaves(root, limit) {
|
|
5
|
+
let leaves = 0;
|
|
6
|
+
const pending = [root];
|
|
7
|
+
while (pending.length) {
|
|
8
|
+
const value = pending.pop();
|
|
9
|
+
if (value !== null && typeof value === 'object')
|
|
10
|
+
pending.push(...Object.values(value));
|
|
11
|
+
else if ((leaves += 1) > limit)
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
return leaves;
|
|
15
|
+
}
|
|
3
16
|
/** Initial presentation only. Never changes a user's selection during edits. */
|
|
4
17
|
export function prefersSource(content, metadata) {
|
|
5
18
|
try {
|
|
6
19
|
const root = JSON.parse(content);
|
|
7
|
-
|
|
8
|
-
|
|
20
|
+
// Every scalar takes a pretty-printed line of its own, so a document with
|
|
21
|
+
// more than 100 of them fails the size check below without a compile.
|
|
22
|
+
if (countLeaves(root, 100) > 100)
|
|
9
23
|
return false;
|
|
10
24
|
const formatted = JSON.stringify(root, null, 2);
|
|
11
25
|
const lines = formatted.split('\n').length;
|
|
12
26
|
if (formatted.length > 8000 || lines > 100)
|
|
13
27
|
return false;
|
|
28
|
+
const compiled = compileJsonViewMetadata(root, undefined, metadata === undefined ? {} : { metadata });
|
|
29
|
+
if (compiled.metadataSource !== 'inferred' || compiled.views.length || isJsonViewTableCandidate(root))
|
|
30
|
+
return false;
|
|
14
31
|
const entries = root !== null && typeof root === 'object' ? Object.values(root) : [];
|
|
32
|
+
// Dictionaries now fall back to properties when their fields differ. Keep
|
|
33
|
+
// that structured presentation instead of switching them to source.
|
|
34
|
+
if (!Array.isArray(root) && entries.length >= 2
|
|
35
|
+
&& entries.every((value) => value !== null && typeof value === 'object' && !Array.isArray(value)))
|
|
36
|
+
return false;
|
|
15
37
|
// A compact root with little visible information is easier to read whole
|
|
16
38
|
// than by opening each nested value, even when nesting is only one level.
|
|
17
39
|
if (entries.length <= 5 && formatted.length <= 3000 && lines <= 40
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Conservative eligibility shared by predicted views and automatic JSON tables. */
|
|
2
|
-
/**
|
|
2
|
+
/** Arrays imply rows; dictionaries need a substantial shared field structure. */
|
|
3
3
|
export declare function isJsonViewTableCandidate(value: unknown): boolean;
|
|
4
4
|
/** Shared scalar paths only; never explodes arrays or rewrites source records. */
|
|
5
5
|
export declare function inferFlatColumnPaths(value: unknown): string[][] | undefined;
|
|
@@ -9,11 +9,24 @@ function isReadable(value) {
|
|
|
9
9
|
|| (typeof value === 'number' && Number.isFinite(value))
|
|
10
10
|
|| (typeof value === 'string' && value.trim().length > 0);
|
|
11
11
|
}
|
|
12
|
-
/**
|
|
12
|
+
/** Arrays imply rows; dictionaries need a substantial shared field structure. */
|
|
13
13
|
export function isJsonViewTableCandidate(value) {
|
|
14
14
|
const rows = Array.isArray(value) ? value : isRecord(value) ? Object.values(value) : [];
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
if (rows.length < MIN_RECORDS || !rows.every(isRecord))
|
|
16
|
+
return false;
|
|
17
|
+
const fields = rows.map((row) => Object.keys(row).filter((key) => key !== '$jsonviews'));
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
return fields.some((keys) => keys.length > 0);
|
|
20
|
+
// Inspect the whole dictionary so late, unrelated sections cannot be missed.
|
|
21
|
+
// Nested values remain intact: only the fields used as columns matter here.
|
|
22
|
+
const counts = new Map();
|
|
23
|
+
for (const keys of fields) {
|
|
24
|
+
for (const key of keys)
|
|
25
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
26
|
+
}
|
|
27
|
+
const shared = new Set([...counts].filter(([, count]) => count / rows.length >= 0.8).map(([key]) => key));
|
|
28
|
+
return shared.size > 0 && shared.size / counts.size >= 0.5
|
|
29
|
+
&& fields.every((keys) => keys.some((key) => shared.has(key)));
|
|
17
30
|
}
|
|
18
31
|
/** Shared scalar paths only; never explodes arrays or rewrites source records. */
|
|
19
32
|
export function inferFlatColumnPaths(value) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@script-it/json-views-core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2026.10.25",
|
|
4
4
|
"description": "JSON Views metadata, path, validation, and source-preserving editing core",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"ajv": "^8.20.0",
|
|
31
31
|
"typescript": "^5.9.3",
|
|
32
|
-
"vitest": "
|
|
32
|
+
"vitest": "4.1.11"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"re2js": "^2.8.6"
|