@openpkg-ts/spec 0.1.0 → 0.2.1
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 +13 -46
- package/dist/index.d.ts +117 -68
- package/dist/index.js +382 -241
- package/package.json +4 -3
- package/schemas/v0.1.0/openpkg.schema.json +68 -42
- package/schemas/v0.2.0/openpkg.schema.json +412 -0
package/dist/index.js
CHANGED
|
@@ -1,14 +1,252 @@
|
|
|
1
1
|
// src/constants.ts
|
|
2
|
-
var SCHEMA_VERSION = "0.
|
|
3
|
-
var SCHEMA_URL = "https://unpkg.com/@openpkg-ts/spec/schemas/v0.
|
|
2
|
+
var SCHEMA_VERSION = "0.2.0";
|
|
3
|
+
var SCHEMA_URL = "https://unpkg.com/@openpkg-ts/spec/schemas/v0.2.0/openpkg.schema.json";
|
|
4
4
|
var JSON_SCHEMA_DRAFT = "https://json-schema.org/draft/2020-12/schema";
|
|
5
|
+
// src/deref.ts
|
|
6
|
+
function dereference(spec) {
|
|
7
|
+
const clone = JSON.parse(JSON.stringify(spec));
|
|
8
|
+
const typeLookup = buildTypeLookup(clone.types);
|
|
9
|
+
const visit = (value, seen) => {
|
|
10
|
+
if (Array.isArray(value)) {
|
|
11
|
+
return value.map((item) => visit(item, new Set(seen)));
|
|
12
|
+
}
|
|
13
|
+
if (value && typeof value === "object") {
|
|
14
|
+
const record = value;
|
|
15
|
+
const ref = readTypeRef(record);
|
|
16
|
+
if (ref) {
|
|
17
|
+
return resolveTypeRef(ref, typeLookup, seen);
|
|
18
|
+
}
|
|
19
|
+
const next = {};
|
|
20
|
+
for (const [key, nested] of Object.entries(record)) {
|
|
21
|
+
next[key] = visit(nested, seen);
|
|
22
|
+
}
|
|
23
|
+
return next;
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
};
|
|
27
|
+
clone.exports = clone.exports.map((item) => visit(item, new Set));
|
|
28
|
+
if (clone.types) {
|
|
29
|
+
clone.types = clone.types.map((item) => visit(item, new Set));
|
|
30
|
+
}
|
|
31
|
+
return clone;
|
|
32
|
+
}
|
|
33
|
+
function buildTypeLookup(types) {
|
|
34
|
+
const map = new Map;
|
|
35
|
+
if (!Array.isArray(types)) {
|
|
36
|
+
return map;
|
|
37
|
+
}
|
|
38
|
+
for (const type of types) {
|
|
39
|
+
if (type && typeof type.id === "string") {
|
|
40
|
+
map.set(type.id, type);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return map;
|
|
44
|
+
}
|
|
45
|
+
function readTypeRef(value) {
|
|
46
|
+
const ref = value.$ref;
|
|
47
|
+
if (typeof ref !== "string") {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const prefix = "#/types/";
|
|
51
|
+
if (!ref.startsWith(prefix)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return ref.slice(prefix.length);
|
|
55
|
+
}
|
|
56
|
+
function resolveTypeRef(id, lookup, seen) {
|
|
57
|
+
if (seen.has(id)) {
|
|
58
|
+
return { $ref: `#/types/${id}` };
|
|
59
|
+
}
|
|
60
|
+
const target = lookup.get(id);
|
|
61
|
+
if (!target) {
|
|
62
|
+
return { $ref: `#/types/${id}` };
|
|
63
|
+
}
|
|
64
|
+
seen.add(id);
|
|
65
|
+
if (target.schema) {
|
|
66
|
+
return JSON.parse(JSON.stringify(target.schema));
|
|
67
|
+
}
|
|
68
|
+
return JSON.parse(JSON.stringify(target));
|
|
69
|
+
}
|
|
70
|
+
// src/diff.ts
|
|
71
|
+
function diffSpec(oldSpec, newSpec) {
|
|
72
|
+
const result = {
|
|
73
|
+
breaking: [],
|
|
74
|
+
nonBreaking: [],
|
|
75
|
+
docsOnly: [],
|
|
76
|
+
coverageDelta: 0,
|
|
77
|
+
oldCoverage: 0,
|
|
78
|
+
newCoverage: 0,
|
|
79
|
+
newUndocumented: [],
|
|
80
|
+
improvedExports: [],
|
|
81
|
+
regressedExports: [],
|
|
82
|
+
driftIntroduced: 0,
|
|
83
|
+
driftResolved: 0
|
|
84
|
+
};
|
|
85
|
+
diffCollections(result, oldSpec.exports, newSpec.exports);
|
|
86
|
+
diffCollections(result, oldSpec.types ?? [], newSpec.types ?? []);
|
|
87
|
+
result.oldCoverage = oldSpec.docs?.coverageScore ?? 0;
|
|
88
|
+
result.newCoverage = newSpec.docs?.coverageScore ?? 0;
|
|
89
|
+
result.coverageDelta = Math.round((result.newCoverage - result.oldCoverage) * 10) / 10;
|
|
90
|
+
const oldExportMap = toExportMap(oldSpec.exports);
|
|
91
|
+
const newExportMap = toExportMap(newSpec.exports);
|
|
92
|
+
for (const [id, newExport] of newExportMap.entries()) {
|
|
93
|
+
const oldExport = oldExportMap.get(id);
|
|
94
|
+
const newScore = newExport.docs?.coverageScore ?? 0;
|
|
95
|
+
const newDriftCount = newExport.docs?.drift?.length ?? 0;
|
|
96
|
+
if (!oldExport) {
|
|
97
|
+
if (newScore < 100 || (newExport.docs?.missing?.length ?? 0) > 0) {
|
|
98
|
+
result.newUndocumented.push(id);
|
|
99
|
+
}
|
|
100
|
+
result.driftIntroduced += newDriftCount;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const oldScore = oldExport.docs?.coverageScore ?? 0;
|
|
104
|
+
const oldDriftCount = oldExport.docs?.drift?.length ?? 0;
|
|
105
|
+
if (newScore > oldScore) {
|
|
106
|
+
result.improvedExports.push(id);
|
|
107
|
+
} else if (newScore < oldScore) {
|
|
108
|
+
result.regressedExports.push(id);
|
|
109
|
+
}
|
|
110
|
+
if (newDriftCount > oldDriftCount) {
|
|
111
|
+
result.driftIntroduced += newDriftCount - oldDriftCount;
|
|
112
|
+
} else if (oldDriftCount > newDriftCount) {
|
|
113
|
+
result.driftResolved += oldDriftCount - newDriftCount;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
function toExportMap(exports) {
|
|
119
|
+
const map = new Map;
|
|
120
|
+
for (const exp of exports) {
|
|
121
|
+
if (exp && typeof exp.id === "string") {
|
|
122
|
+
map.set(exp.id, exp);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return map;
|
|
126
|
+
}
|
|
127
|
+
function diffCollections(result, oldItems, newItems) {
|
|
128
|
+
const oldMap = toMap(oldItems);
|
|
129
|
+
const newMap = toMap(newItems);
|
|
130
|
+
for (const [id, oldItem] of oldMap.entries()) {
|
|
131
|
+
const newItem = newMap.get(id);
|
|
132
|
+
if (!newItem) {
|
|
133
|
+
result.breaking.push(id);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const docOnly = isDocOnlyChange(oldItem, newItem);
|
|
137
|
+
const identical = isDeepEqual(oldItem, newItem);
|
|
138
|
+
if (identical) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (docOnly) {
|
|
142
|
+
result.docsOnly.push(id);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
result.breaking.push(id);
|
|
146
|
+
}
|
|
147
|
+
for (const id of newMap.keys()) {
|
|
148
|
+
if (!oldMap.has(id)) {
|
|
149
|
+
result.nonBreaking.push(id);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function toMap(items) {
|
|
154
|
+
const map = new Map;
|
|
155
|
+
for (const item of items) {
|
|
156
|
+
if (item && typeof item.id === "string") {
|
|
157
|
+
map.set(item.id, item);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return map;
|
|
161
|
+
}
|
|
162
|
+
var DOC_KEYS = new Set(["description", "examples", "tags", "source", "rawComments"]);
|
|
163
|
+
function isDocOnlyChange(a, b) {
|
|
164
|
+
const structuralA = normalizeForComparison(removeDocFields(a));
|
|
165
|
+
const structuralB = normalizeForComparison(removeDocFields(b));
|
|
166
|
+
if (structuralA !== structuralB) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
const fullA = normalizeForComparison(a);
|
|
170
|
+
const fullB = normalizeForComparison(b);
|
|
171
|
+
return fullA !== fullB;
|
|
172
|
+
}
|
|
173
|
+
function isDeepEqual(a, b) {
|
|
174
|
+
return normalizeForComparison(a) === normalizeForComparison(b);
|
|
175
|
+
}
|
|
176
|
+
function removeDocFields(value) {
|
|
177
|
+
if (Array.isArray(value)) {
|
|
178
|
+
return value.map((item) => removeDocFields(item));
|
|
179
|
+
}
|
|
180
|
+
if (!value || typeof value !== "object") {
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
const entries = Object.entries(value).filter(([key]) => !DOC_KEYS.has(key));
|
|
184
|
+
const cleaned = {};
|
|
185
|
+
for (const [key, val] of entries) {
|
|
186
|
+
cleaned[key] = removeDocFields(val);
|
|
187
|
+
}
|
|
188
|
+
return cleaned;
|
|
189
|
+
}
|
|
190
|
+
function normalizeForComparison(value) {
|
|
191
|
+
return JSON.stringify(sortKeys(value));
|
|
192
|
+
}
|
|
193
|
+
function sortKeys(value) {
|
|
194
|
+
if (Array.isArray(value)) {
|
|
195
|
+
return value.map((item) => sortKeys(item));
|
|
196
|
+
}
|
|
197
|
+
if (!value || typeof value !== "object") {
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
|
|
201
|
+
const result = {};
|
|
202
|
+
for (const [key, val] of entries) {
|
|
203
|
+
result[key] = sortKeys(val);
|
|
204
|
+
}
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
// src/normalize.ts
|
|
208
|
+
var DEFAULT_ECOSYSTEM = "js/ts";
|
|
209
|
+
var arrayFieldsByExport = ["signatures", "members", "examples", "tags"];
|
|
210
|
+
var arrayFieldsByType = ["members", "tags"];
|
|
211
|
+
function normalize(spec) {
|
|
212
|
+
const normalized = JSON.parse(JSON.stringify(spec));
|
|
213
|
+
normalized.meta = {
|
|
214
|
+
ecosystem: normalized.meta?.ecosystem ?? DEFAULT_ECOSYSTEM,
|
|
215
|
+
...normalized.meta
|
|
216
|
+
};
|
|
217
|
+
normalized.exports = Array.isArray(normalized.exports) ? [...normalized.exports] : [];
|
|
218
|
+
normalized.exports.sort((a, b) => (a.name || "").localeCompare(b.name || ""));
|
|
219
|
+
normalized.exports = normalized.exports.map((item) => normalizeExport(item));
|
|
220
|
+
const types = Array.isArray(normalized.types) ? [...normalized.types] : [];
|
|
221
|
+
types.sort((a, b) => (a.name || "").localeCompare(b.name || ""));
|
|
222
|
+
normalized.types = types.map((item) => normalizeType(item));
|
|
223
|
+
return normalized;
|
|
224
|
+
}
|
|
225
|
+
function normalizeExport(item) {
|
|
226
|
+
const clone = JSON.parse(JSON.stringify(item));
|
|
227
|
+
for (const field of arrayFieldsByExport) {
|
|
228
|
+
if (!Array.isArray(clone[field])) {
|
|
229
|
+
clone[field] = [];
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return clone;
|
|
233
|
+
}
|
|
234
|
+
function normalizeType(item) {
|
|
235
|
+
const clone = JSON.parse(JSON.stringify(item));
|
|
236
|
+
for (const field of arrayFieldsByType) {
|
|
237
|
+
if (!Array.isArray(clone[field])) {
|
|
238
|
+
clone[field] = [];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return clone;
|
|
242
|
+
}
|
|
5
243
|
// src/validate.ts
|
|
6
244
|
import Ajv from "ajv/dist/2020.js";
|
|
7
245
|
import addFormats from "ajv-formats";
|
|
8
|
-
// schemas/v0.
|
|
246
|
+
// schemas/v0.2.0/openpkg.schema.json
|
|
9
247
|
var openpkg_schema_default = {
|
|
10
248
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
11
|
-
$id: "https://unpkg.com/@openpkg-ts/spec/schemas/v0.
|
|
249
|
+
$id: "https://unpkg.com/@openpkg-ts/spec/schemas/v0.2.0/openpkg.schema.json",
|
|
12
250
|
title: "OpenPkg Specification",
|
|
13
251
|
description: "Schema for OpenPkg specification files",
|
|
14
252
|
type: "object",
|
|
@@ -27,7 +265,7 @@ var openpkg_schema_default = {
|
|
|
27
265
|
type: "string",
|
|
28
266
|
description: "OpenPkg specification version",
|
|
29
267
|
pattern: "^[0-9]+\\.[0-9]+\\.[0-9]+$",
|
|
30
|
-
const: "0.
|
|
268
|
+
const: "0.2.0"
|
|
31
269
|
},
|
|
32
270
|
meta: {
|
|
33
271
|
type: "object",
|
|
@@ -58,14 +296,7 @@ var openpkg_schema_default = {
|
|
|
58
296
|
},
|
|
59
297
|
ecosystem: {
|
|
60
298
|
type: "string",
|
|
61
|
-
description: "Package ecosystem"
|
|
62
|
-
enum: [
|
|
63
|
-
"js/ts",
|
|
64
|
-
"python",
|
|
65
|
-
"rust",
|
|
66
|
-
"go",
|
|
67
|
-
"java"
|
|
68
|
-
]
|
|
299
|
+
description: "Package ecosystem"
|
|
69
300
|
}
|
|
70
301
|
}
|
|
71
302
|
},
|
|
@@ -82,9 +313,73 @@ var openpkg_schema_default = {
|
|
|
82
313
|
items: {
|
|
83
314
|
$ref: "#/$defs/typeDef"
|
|
84
315
|
}
|
|
316
|
+
},
|
|
317
|
+
docs: {
|
|
318
|
+
$ref: "#/$defs/docsMetadata",
|
|
319
|
+
description: "Aggregate documentation coverage metadata"
|
|
85
320
|
}
|
|
86
321
|
},
|
|
87
322
|
$defs: {
|
|
323
|
+
docSignal: {
|
|
324
|
+
type: "string",
|
|
325
|
+
enum: [
|
|
326
|
+
"description",
|
|
327
|
+
"params",
|
|
328
|
+
"returns",
|
|
329
|
+
"examples"
|
|
330
|
+
]
|
|
331
|
+
},
|
|
332
|
+
docDrift: {
|
|
333
|
+
type: "object",
|
|
334
|
+
required: ["type", "issue"],
|
|
335
|
+
properties: {
|
|
336
|
+
type: {
|
|
337
|
+
type: "string",
|
|
338
|
+
enum: ["param-mismatch", "param-type-mismatch", "return-type-mismatch", "generic-constraint-mismatch", "optionality-mismatch", "deprecated-mismatch", "visibility-mismatch", "example-drift", "broken-link"]
|
|
339
|
+
},
|
|
340
|
+
target: {
|
|
341
|
+
type: "string",
|
|
342
|
+
description: "Relevant identifier (e.g., parameter name)"
|
|
343
|
+
},
|
|
344
|
+
issue: {
|
|
345
|
+
type: "string",
|
|
346
|
+
description: "Human-friendly drift explanation"
|
|
347
|
+
},
|
|
348
|
+
suggestion: {
|
|
349
|
+
type: "string",
|
|
350
|
+
description: "Optional remediation hint"
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
additionalProperties: false
|
|
354
|
+
},
|
|
355
|
+
docsMetadata: {
|
|
356
|
+
type: "object",
|
|
357
|
+
description: "Documentation coverage metadata",
|
|
358
|
+
additionalProperties: false,
|
|
359
|
+
properties: {
|
|
360
|
+
coverageScore: {
|
|
361
|
+
type: "number",
|
|
362
|
+
minimum: 0,
|
|
363
|
+
maximum: 100,
|
|
364
|
+
description: "Documentation coverage value from 0-100."
|
|
365
|
+
},
|
|
366
|
+
missing: {
|
|
367
|
+
type: "array",
|
|
368
|
+
description: "Doc components missing for this entity",
|
|
369
|
+
items: {
|
|
370
|
+
$ref: "#/$defs/docSignal"
|
|
371
|
+
},
|
|
372
|
+
uniqueItems: true
|
|
373
|
+
},
|
|
374
|
+
drift: {
|
|
375
|
+
type: "array",
|
|
376
|
+
description: "Detected documentation drift signals",
|
|
377
|
+
items: {
|
|
378
|
+
$ref: "#/$defs/docDrift"
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
},
|
|
88
383
|
export: {
|
|
89
384
|
type: "object",
|
|
90
385
|
required: [
|
|
@@ -101,6 +396,22 @@ var openpkg_schema_default = {
|
|
|
101
396
|
type: "string",
|
|
102
397
|
description: "Export name"
|
|
103
398
|
},
|
|
399
|
+
slug: {
|
|
400
|
+
type: "string",
|
|
401
|
+
description: "Stable slug for linking"
|
|
402
|
+
},
|
|
403
|
+
displayName: {
|
|
404
|
+
type: "string",
|
|
405
|
+
description: "UI-friendly label"
|
|
406
|
+
},
|
|
407
|
+
category: {
|
|
408
|
+
type: "string",
|
|
409
|
+
description: "Grouping hint for navigation"
|
|
410
|
+
},
|
|
411
|
+
importPath: {
|
|
412
|
+
type: "string",
|
|
413
|
+
description: "Recommended import path"
|
|
414
|
+
},
|
|
104
415
|
kind: {
|
|
105
416
|
type: "string",
|
|
106
417
|
description: "Kind of export",
|
|
@@ -131,15 +442,37 @@ var openpkg_schema_default = {
|
|
|
131
442
|
$ref: "#/$defs/signature"
|
|
132
443
|
}
|
|
133
444
|
},
|
|
134
|
-
|
|
445
|
+
type: {
|
|
446
|
+
description: "Type reference or inline schema for variables",
|
|
447
|
+
oneOf: [
|
|
448
|
+
{ type: "string" },
|
|
449
|
+
{ $ref: "#/$defs/schema" }
|
|
450
|
+
]
|
|
451
|
+
},
|
|
452
|
+
members: {
|
|
135
453
|
type: "array",
|
|
136
|
-
description: "Class/interface
|
|
454
|
+
description: "Class/interface/enum members",
|
|
455
|
+
items: { type: "object" }
|
|
456
|
+
},
|
|
457
|
+
tags: {
|
|
458
|
+
type: "array",
|
|
459
|
+
description: "JSDoc/TSDoc tags",
|
|
137
460
|
items: {
|
|
138
|
-
|
|
461
|
+
type: "object",
|
|
462
|
+
required: ["name", "text"],
|
|
463
|
+
properties: {
|
|
464
|
+
name: { type: "string" },
|
|
465
|
+
text: { type: "string" }
|
|
466
|
+
},
|
|
467
|
+
additionalProperties: false
|
|
139
468
|
}
|
|
140
469
|
},
|
|
141
470
|
source: {
|
|
142
471
|
$ref: "#/$defs/sourceLocation"
|
|
472
|
+
},
|
|
473
|
+
docs: {
|
|
474
|
+
$ref: "#/$defs/docsMetadata",
|
|
475
|
+
description: "Documentation coverage metadata for this export"
|
|
143
476
|
}
|
|
144
477
|
}
|
|
145
478
|
},
|
|
@@ -159,6 +492,22 @@ var openpkg_schema_default = {
|
|
|
159
492
|
type: "string",
|
|
160
493
|
description: "Type name"
|
|
161
494
|
},
|
|
495
|
+
slug: {
|
|
496
|
+
type: "string",
|
|
497
|
+
description: "Stable slug for linking"
|
|
498
|
+
},
|
|
499
|
+
displayName: {
|
|
500
|
+
type: "string",
|
|
501
|
+
description: "UI-friendly label"
|
|
502
|
+
},
|
|
503
|
+
category: {
|
|
504
|
+
type: "string",
|
|
505
|
+
description: "Grouping hint for navigation"
|
|
506
|
+
},
|
|
507
|
+
importPath: {
|
|
508
|
+
type: "string",
|
|
509
|
+
description: "Recommended import path"
|
|
510
|
+
},
|
|
162
511
|
kind: {
|
|
163
512
|
type: "string",
|
|
164
513
|
description: "Kind of type definition",
|
|
@@ -180,11 +529,22 @@ var openpkg_schema_default = {
|
|
|
180
529
|
type: "string",
|
|
181
530
|
description: "Type expression for type aliases"
|
|
182
531
|
},
|
|
183
|
-
|
|
532
|
+
members: {
|
|
184
533
|
type: "array",
|
|
185
|
-
description: "
|
|
534
|
+
description: "Members for classes/interfaces/enums",
|
|
535
|
+
items: { type: "object" }
|
|
536
|
+
},
|
|
537
|
+
tags: {
|
|
538
|
+
type: "array",
|
|
539
|
+
description: "JSDoc/TSDoc tags",
|
|
186
540
|
items: {
|
|
187
|
-
|
|
541
|
+
type: "object",
|
|
542
|
+
required: ["name", "text"],
|
|
543
|
+
properties: {
|
|
544
|
+
name: { type: "string" },
|
|
545
|
+
text: { type: "string" }
|
|
546
|
+
},
|
|
547
|
+
additionalProperties: false
|
|
188
548
|
}
|
|
189
549
|
},
|
|
190
550
|
source: {
|
|
@@ -225,12 +585,12 @@ var openpkg_schema_default = {
|
|
|
225
585
|
type: "boolean",
|
|
226
586
|
description: "Whether the parameter is required"
|
|
227
587
|
},
|
|
588
|
+
schema: {
|
|
589
|
+
$ref: "#/$defs/schema"
|
|
590
|
+
},
|
|
228
591
|
description: {
|
|
229
592
|
type: "string",
|
|
230
593
|
description: "Parameter description"
|
|
231
|
-
},
|
|
232
|
-
schema: {
|
|
233
|
-
$ref: "#/$defs/schema"
|
|
234
594
|
}
|
|
235
595
|
}
|
|
236
596
|
},
|
|
@@ -246,30 +606,6 @@ var openpkg_schema_default = {
|
|
|
246
606
|
}
|
|
247
607
|
}
|
|
248
608
|
},
|
|
249
|
-
property: {
|
|
250
|
-
type: "object",
|
|
251
|
-
required: [
|
|
252
|
-
"name",
|
|
253
|
-
"required"
|
|
254
|
-
],
|
|
255
|
-
properties: {
|
|
256
|
-
name: {
|
|
257
|
-
type: "string",
|
|
258
|
-
description: "Property name"
|
|
259
|
-
},
|
|
260
|
-
required: {
|
|
261
|
-
type: "boolean",
|
|
262
|
-
description: "Whether the property is required"
|
|
263
|
-
},
|
|
264
|
-
description: {
|
|
265
|
-
type: "string",
|
|
266
|
-
description: "Property description"
|
|
267
|
-
},
|
|
268
|
-
schema: {
|
|
269
|
-
$ref: "#/$defs/schema"
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
},
|
|
273
609
|
schema: {
|
|
274
610
|
anyOf: [
|
|
275
611
|
{
|
|
@@ -358,204 +694,9 @@ function getValidationErrors(spec) {
|
|
|
358
694
|
const result = validateSpec(spec);
|
|
359
695
|
return result.ok ? [] : result.errors;
|
|
360
696
|
}
|
|
361
|
-
// src/normalize.ts
|
|
362
|
-
var DEFAULT_ECOSYSTEM = "js/ts";
|
|
363
|
-
var arrayFieldsByExport = ["signatures", "members", "examples", "tags"];
|
|
364
|
-
var arrayFieldsByType = ["members", "tags"];
|
|
365
|
-
function normalize(spec) {
|
|
366
|
-
const normalized = JSON.parse(JSON.stringify(spec));
|
|
367
|
-
normalized.meta = {
|
|
368
|
-
ecosystem: normalized.meta?.ecosystem ?? DEFAULT_ECOSYSTEM,
|
|
369
|
-
...normalized.meta
|
|
370
|
-
};
|
|
371
|
-
normalized.exports = Array.isArray(normalized.exports) ? [...normalized.exports] : [];
|
|
372
|
-
normalized.exports.sort((a, b) => (a.name || "").localeCompare(b.name || ""));
|
|
373
|
-
normalized.exports = normalized.exports.map((item) => normalizeExport(item));
|
|
374
|
-
const types = Array.isArray(normalized.types) ? [...normalized.types] : [];
|
|
375
|
-
types.sort((a, b) => (a.name || "").localeCompare(b.name || ""));
|
|
376
|
-
normalized.types = types.map((item) => normalizeType(item));
|
|
377
|
-
normalized.examples = normalized.examples ?? [];
|
|
378
|
-
normalized.extensions = normalized.extensions ?? {};
|
|
379
|
-
return normalized;
|
|
380
|
-
}
|
|
381
|
-
function normalizeExport(item) {
|
|
382
|
-
const clone = JSON.parse(JSON.stringify(item));
|
|
383
|
-
for (const field of arrayFieldsByExport) {
|
|
384
|
-
if (!Array.isArray(clone[field])) {
|
|
385
|
-
clone[field] = [];
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
return clone;
|
|
389
|
-
}
|
|
390
|
-
function normalizeType(item) {
|
|
391
|
-
const clone = JSON.parse(JSON.stringify(item));
|
|
392
|
-
for (const field of arrayFieldsByType) {
|
|
393
|
-
if (!Array.isArray(clone[field])) {
|
|
394
|
-
clone[field] = [];
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
return clone;
|
|
398
|
-
}
|
|
399
|
-
// src/deref.ts
|
|
400
|
-
function dereference(spec) {
|
|
401
|
-
const clone = JSON.parse(JSON.stringify(spec));
|
|
402
|
-
const typeLookup = buildTypeLookup(clone.types);
|
|
403
|
-
const visit = (value, seen) => {
|
|
404
|
-
if (Array.isArray(value)) {
|
|
405
|
-
return value.map((item) => visit(item, new Set(seen)));
|
|
406
|
-
}
|
|
407
|
-
if (value && typeof value === "object") {
|
|
408
|
-
const record = value;
|
|
409
|
-
const ref = readTypeRef(record);
|
|
410
|
-
if (ref) {
|
|
411
|
-
return resolveTypeRef(ref, typeLookup, seen);
|
|
412
|
-
}
|
|
413
|
-
const next = {};
|
|
414
|
-
for (const [key, nested] of Object.entries(record)) {
|
|
415
|
-
next[key] = visit(nested, seen);
|
|
416
|
-
}
|
|
417
|
-
return next;
|
|
418
|
-
}
|
|
419
|
-
return value;
|
|
420
|
-
};
|
|
421
|
-
clone.exports = clone.exports.map((item) => visit(item, new Set));
|
|
422
|
-
if (clone.types) {
|
|
423
|
-
clone.types = clone.types.map((item) => visit(item, new Set));
|
|
424
|
-
}
|
|
425
|
-
return clone;
|
|
426
|
-
}
|
|
427
|
-
function buildTypeLookup(types) {
|
|
428
|
-
const map = new Map;
|
|
429
|
-
if (!Array.isArray(types)) {
|
|
430
|
-
return map;
|
|
431
|
-
}
|
|
432
|
-
for (const type of types) {
|
|
433
|
-
if (type && typeof type.id === "string") {
|
|
434
|
-
map.set(type.id, type);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
return map;
|
|
438
|
-
}
|
|
439
|
-
function readTypeRef(value) {
|
|
440
|
-
const ref = value["$ref"];
|
|
441
|
-
if (typeof ref !== "string") {
|
|
442
|
-
return null;
|
|
443
|
-
}
|
|
444
|
-
const prefix = "#/types/";
|
|
445
|
-
if (!ref.startsWith(prefix)) {
|
|
446
|
-
return null;
|
|
447
|
-
}
|
|
448
|
-
return ref.slice(prefix.length);
|
|
449
|
-
}
|
|
450
|
-
function resolveTypeRef(id, lookup, seen) {
|
|
451
|
-
if (seen.has(id)) {
|
|
452
|
-
return { $ref: `#/types/${id}` };
|
|
453
|
-
}
|
|
454
|
-
const target = lookup.get(id);
|
|
455
|
-
if (!target) {
|
|
456
|
-
return { $ref: `#/types/${id}` };
|
|
457
|
-
}
|
|
458
|
-
seen.add(id);
|
|
459
|
-
if (target.schema) {
|
|
460
|
-
return JSON.parse(JSON.stringify(target.schema));
|
|
461
|
-
}
|
|
462
|
-
return JSON.parse(JSON.stringify(target));
|
|
463
|
-
}
|
|
464
|
-
// src/migrate/v0_1_0__to__0_2_0.ts
|
|
465
|
-
function migrate_0_1_0__to__0_2_0(spec) {
|
|
466
|
-
return spec;
|
|
467
|
-
}
|
|
468
|
-
// src/diff.ts
|
|
469
|
-
function diffSpec(a, b) {
|
|
470
|
-
const result = { breaking: [], nonBreaking: [], docsOnly: [] };
|
|
471
|
-
diffCollections(result, a.exports, b.exports);
|
|
472
|
-
diffCollections(result, a.types ?? [], b.types ?? []);
|
|
473
|
-
return result;
|
|
474
|
-
}
|
|
475
|
-
function diffCollections(result, oldItems, newItems) {
|
|
476
|
-
const oldMap = toMap(oldItems);
|
|
477
|
-
const newMap = toMap(newItems);
|
|
478
|
-
for (const [id, oldItem] of oldMap.entries()) {
|
|
479
|
-
const newItem = newMap.get(id);
|
|
480
|
-
if (!newItem) {
|
|
481
|
-
result.breaking.push(id);
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
const docOnly = isDocOnlyChange(oldItem, newItem);
|
|
485
|
-
const identical = isDeepEqual(oldItem, newItem);
|
|
486
|
-
if (identical) {
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
if (docOnly) {
|
|
490
|
-
result.docsOnly.push(id);
|
|
491
|
-
continue;
|
|
492
|
-
}
|
|
493
|
-
result.breaking.push(id);
|
|
494
|
-
}
|
|
495
|
-
for (const id of newMap.keys()) {
|
|
496
|
-
if (!oldMap.has(id)) {
|
|
497
|
-
result.nonBreaking.push(id);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
function toMap(items) {
|
|
502
|
-
const map = new Map;
|
|
503
|
-
for (const item of items) {
|
|
504
|
-
if (item && typeof item.id === "string") {
|
|
505
|
-
map.set(item.id, item);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
return map;
|
|
509
|
-
}
|
|
510
|
-
var DOC_KEYS = new Set(["description", "examples", "tags", "source", "rawComments"]);
|
|
511
|
-
function isDocOnlyChange(a, b) {
|
|
512
|
-
const structuralA = normalizeForComparison(removeDocFields(a));
|
|
513
|
-
const structuralB = normalizeForComparison(removeDocFields(b));
|
|
514
|
-
if (structuralA !== structuralB) {
|
|
515
|
-
return false;
|
|
516
|
-
}
|
|
517
|
-
const fullA = normalizeForComparison(a);
|
|
518
|
-
const fullB = normalizeForComparison(b);
|
|
519
|
-
return fullA !== fullB;
|
|
520
|
-
}
|
|
521
|
-
function isDeepEqual(a, b) {
|
|
522
|
-
return normalizeForComparison(a) === normalizeForComparison(b);
|
|
523
|
-
}
|
|
524
|
-
function removeDocFields(value) {
|
|
525
|
-
if (Array.isArray(value)) {
|
|
526
|
-
return value.map((item) => removeDocFields(item));
|
|
527
|
-
}
|
|
528
|
-
if (!value || typeof value !== "object") {
|
|
529
|
-
return value;
|
|
530
|
-
}
|
|
531
|
-
const entries = Object.entries(value).filter(([key]) => !DOC_KEYS.has(key));
|
|
532
|
-
const cleaned = {};
|
|
533
|
-
for (const [key, val] of entries) {
|
|
534
|
-
cleaned[key] = removeDocFields(val);
|
|
535
|
-
}
|
|
536
|
-
return cleaned;
|
|
537
|
-
}
|
|
538
|
-
function normalizeForComparison(value) {
|
|
539
|
-
return JSON.stringify(sortKeys(value));
|
|
540
|
-
}
|
|
541
|
-
function sortKeys(value) {
|
|
542
|
-
if (Array.isArray(value)) {
|
|
543
|
-
return value.map((item) => sortKeys(item));
|
|
544
|
-
}
|
|
545
|
-
if (!value || typeof value !== "object") {
|
|
546
|
-
return value;
|
|
547
|
-
}
|
|
548
|
-
const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
|
|
549
|
-
const result = {};
|
|
550
|
-
for (const [key, val] of entries) {
|
|
551
|
-
result[key] = sortKeys(val);
|
|
552
|
-
}
|
|
553
|
-
return result;
|
|
554
|
-
}
|
|
555
697
|
export {
|
|
556
698
|
validateSpec,
|
|
557
699
|
normalize,
|
|
558
|
-
migrate_0_1_0__to__0_2_0 as migrate,
|
|
559
700
|
getValidationErrors,
|
|
560
701
|
diffSpec,
|
|
561
702
|
dereference,
|