wormajs 0.3.1 → 1.0.0-beta.0
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/dist/bin/actions.js +162 -5
- package/dist/bin/cli.js +7 -1
- package/dist/bin/renderer.js +2 -8
- package/dist/checkUpdates.js +98 -0
- package/dist/config.js +7 -0
- package/dist/constant.js +1 -2
- package/dist/core/WorkerPool.js +14 -0
- package/dist/core/loader/callingCodeLoader/helper.js +2 -3
- package/dist/core/loader/callingCodeLoader/index.js +1 -1
- package/dist/core/parser/openApiParser/helper.js +32 -19
- package/dist/core/parser/templateParser/index.js +25 -12
- package/dist/core/workerPool/index.js +2 -1
- package/dist/functions/changeReport.js +230 -0
- package/dist/functions/diffApis.js +82 -0
- package/dist/functions/diffDocument.js +542 -0
- package/dist/functions/sourceSnapshot.js +107 -0
- package/dist/functions/wormaJson.js +306 -66
- package/dist/generate.js +24 -2
- package/dist/helper/config/ConfigHelper.js +1 -2
- package/dist/helper/config/ConfigManager.js +7 -0
- package/dist/helper/config/GeneratorHelper.js +74 -21
- package/dist/helper/config/zType.js +24 -1
- package/dist/helper/template/index.js +60 -4
- package/dist/index.js +17 -1
- package/dist/plugins/index.js +2 -2
- package/dist/plugins/presets/aiDoc.js +4 -0
- package/dist/plugins/presets/payloadModifier/dsl.js +147 -0
- package/dist/plugins/presets/payloadModifier/index.js +122 -135
- package/dist/plugins/presets/payloadModifier/patch.js +171 -0
- package/dist/plugins/presets/payloadModifier/scope.js +109 -0
- package/dist/plugins/presets/platform/index.js +1 -3
- package/dist/plugins/presets/postman.js +105 -0
- package/dist/template/presets/ai-doc/SKILL.md.handlebars +1 -1
- package/dist/template/presets/alova/common/services/{tag}.d.cts.handlebars +1 -1
- package/dist/template/presets/alova/module/services/{tag}.d.ts.handlebars +1 -1
- package/dist/template/presets/alova/partials/dts-fn-declare.handlebars +1 -1
- package/dist/template/presets/alova/partials/dts-types.handlebars +12 -0
- package/dist/template/presets/alova/typescript/services/{tag}.ts.handlebars +11 -6
- package/dist/template/presets/axios/partials/dts-types.handlebars +9 -5
- package/dist/template/presets/axios/typescript/services/{tag}.ts.handlebars +9 -5
- package/dist/template/presets/fetch/partials/dts-types.handlebars +8 -5
- package/dist/template/presets/fetch/typescript/services/{tag}.ts.handlebars +9 -5
- package/dist/template/presets/ky/partials/dts-types.handlebars +8 -5
- package/dist/template/presets/ky/typescript/services/{tag}.ts.handlebars +9 -5
- package/dist/utils/format.js +62 -15
- package/dist/utils/template.js +1 -1
- package/package.json +3 -2
- package/typings/index.d.ts +266 -13
- package/typings/plugins.d.ts +171 -80
- package/dist/plugins/presets/payloadModifier/hepler.js +0 -289
- package/dist/plugins/presets/platform/fastapi.js +0 -22
- package/dist/template/presets/alova/partials/dts-extra-config.handlebars +0 -8
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Structural diff of the **source** OpenAPI document.
|
|
4
|
+
*
|
|
5
|
+
* The baseline is the document parsed from the `beforeSpecParse` output, i.e.
|
|
6
|
+
* taken *before* the `specParsed` hooks run: it is the source file as authored,
|
|
7
|
+
* not the plugin-normalised document that generation consumes. Every difference
|
|
8
|
+
* is reported as a flat {@link SourceChange} row so a caller (the CLI table, the
|
|
9
|
+
* editor webview, a CI script) can render it without any further shaping.
|
|
10
|
+
*
|
|
11
|
+
* Design notes:
|
|
12
|
+
* - `$ref`s are deliberately **not** inlined: the record is a source view, so a
|
|
13
|
+
* component change is reported once under `#/components/...`, with the
|
|
14
|
+
* affected operations attached as `affects` (resolved through the reverse
|
|
15
|
+
* `$ref` index, including transitive references) so the impact stays visible
|
|
16
|
+
* without duplicating the row.
|
|
17
|
+
* - Description-ish keys are still recorded (they are source changes) but get
|
|
18
|
+
* the `doc` level so callers can de-emphasise them.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.diffSourceDocument = diffSourceDocument;
|
|
22
|
+
/** HTTP methods recognised as operations inside a path item. */
|
|
23
|
+
const HTTP_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
|
|
24
|
+
/** Component sections compared by key. */
|
|
25
|
+
const COMPONENT_SECTIONS = [
|
|
26
|
+
'schemas',
|
|
27
|
+
'parameters',
|
|
28
|
+
'requestBodies',
|
|
29
|
+
'responses',
|
|
30
|
+
'headers',
|
|
31
|
+
'securitySchemes',
|
|
32
|
+
'links',
|
|
33
|
+
'callbacks',
|
|
34
|
+
'examples',
|
|
35
|
+
];
|
|
36
|
+
/** Keys whose change is documentation-only. */
|
|
37
|
+
const DOC_KEYS = new Set(['description', 'title', 'summary', 'example', 'examples', 'externalDocs', 'deprecated']);
|
|
38
|
+
/** Recursion guard for deeply nested schemas. */
|
|
39
|
+
const MAX_DEPTH = 8;
|
|
40
|
+
/** Row cap: a pathological spec must not produce an unbounded record. */
|
|
41
|
+
const MAX_ROWS = 1000;
|
|
42
|
+
// ──────────────────────────────────────────────────────────────
|
|
43
|
+
// Change collection
|
|
44
|
+
// ──────────────────────────────────────────────────────────────
|
|
45
|
+
class ChangeCollector {
|
|
46
|
+
changes = [];
|
|
47
|
+
truncated = false;
|
|
48
|
+
push(target, kind, change) {
|
|
49
|
+
if (this.changes.length >= MAX_ROWS) {
|
|
50
|
+
this.truncated = true;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.changes.push({ ...change, target, kind });
|
|
54
|
+
}
|
|
55
|
+
/** Finalise the row list, appending a marker when the cap was hit. */
|
|
56
|
+
finish() {
|
|
57
|
+
if (this.truncated) {
|
|
58
|
+
this.changes.push({
|
|
59
|
+
op: '~',
|
|
60
|
+
kind: 'meta',
|
|
61
|
+
target: '(truncated)',
|
|
62
|
+
item: '',
|
|
63
|
+
detail: `more than ${MAX_ROWS} changes`,
|
|
64
|
+
level: 'doc',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return this.changes;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// ──────────────────────────────────────────────────────────────
|
|
71
|
+
// Value helpers
|
|
72
|
+
// ──────────────────────────────────────────────────────────────
|
|
73
|
+
function isObject(value) {
|
|
74
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
75
|
+
}
|
|
76
|
+
function isRefObject(value) {
|
|
77
|
+
return isObject(value) && typeof value.$ref === 'string';
|
|
78
|
+
}
|
|
79
|
+
function isPrimitive(value) {
|
|
80
|
+
return value === null || ['string', 'number', 'boolean'].includes(typeof value);
|
|
81
|
+
}
|
|
82
|
+
function asArray(value) {
|
|
83
|
+
return Array.isArray(value) ? value : [];
|
|
84
|
+
}
|
|
85
|
+
function asObject(value) {
|
|
86
|
+
return isObject(value) ? value : {};
|
|
87
|
+
}
|
|
88
|
+
/** Sorted union of two objects' keys — keeps the record deterministic. */
|
|
89
|
+
function unionKeys(before, after) {
|
|
90
|
+
return [...new Set([...Object.keys(before), ...Object.keys(after)])].sort();
|
|
91
|
+
}
|
|
92
|
+
/** Sorted union of two maps' keys. */
|
|
93
|
+
function unionMapKeys(a, b) {
|
|
94
|
+
return [...new Set([...a.keys(), ...b.keys()])].sort();
|
|
95
|
+
}
|
|
96
|
+
function joinPath(prefix, key) {
|
|
97
|
+
return prefix ? `${prefix}.${key}` : key;
|
|
98
|
+
}
|
|
99
|
+
/** Shallow copy without the given keys. */
|
|
100
|
+
function omit(source, keys) {
|
|
101
|
+
const result = {};
|
|
102
|
+
for (const key of Object.keys(source)) {
|
|
103
|
+
if (!keys.includes(key))
|
|
104
|
+
result[key] = source[key];
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
function stableKey(value) {
|
|
109
|
+
return JSON.stringify(value) ?? 'null';
|
|
110
|
+
}
|
|
111
|
+
/** Short, single-line preview of a value for the `detail` column. */
|
|
112
|
+
function summarize(value) {
|
|
113
|
+
if (value === undefined)
|
|
114
|
+
return '-';
|
|
115
|
+
if (value === null)
|
|
116
|
+
return 'null';
|
|
117
|
+
if (Array.isArray(value)) {
|
|
118
|
+
if (value.length <= 4 && value.every(isPrimitive))
|
|
119
|
+
return JSON.stringify(value);
|
|
120
|
+
return `array(${value.length})`;
|
|
121
|
+
}
|
|
122
|
+
if (isObject(value)) {
|
|
123
|
+
const keys = Object.keys(value);
|
|
124
|
+
return `{${keys.slice(0, 4).join(', ')}${keys.length > 4 ? ', ...' : ''}}`;
|
|
125
|
+
}
|
|
126
|
+
return JSON.stringify(value) ?? String(value);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Severity of a leaf change.
|
|
130
|
+
*
|
|
131
|
+
* `required`/`enum` are treated per direction because their set semantics are
|
|
132
|
+
* asymmetric: a newly required field breaks callers, a relaxed one does not; a
|
|
133
|
+
* new enum value is compatible, a removed one is not.
|
|
134
|
+
*/
|
|
135
|
+
function levelFor(key, op) {
|
|
136
|
+
if (DOC_KEYS.has(key) || key.startsWith('x-'))
|
|
137
|
+
return 'doc';
|
|
138
|
+
if (key === 'required')
|
|
139
|
+
return op === '+' ? 'breaking' : 'additive';
|
|
140
|
+
return op === '+' ? 'additive' : 'breaking';
|
|
141
|
+
}
|
|
142
|
+
// ──────────────────────────────────────────────────────────────
|
|
143
|
+
// Generic recursive differ
|
|
144
|
+
// ──────────────────────────────────────────────────────────────
|
|
145
|
+
/**
|
|
146
|
+
* Recursively diff two JSON-ish values, emitting one row per changed leaf.
|
|
147
|
+
*
|
|
148
|
+
* - objects: key union (covers `properties`, where keys are property names)
|
|
149
|
+
* - primitive arrays: set semantics (covers `enum`, `required`, `type`)
|
|
150
|
+
* - object arrays: index semantics (covers `allOf`/`oneOf`/`security`/`servers`)
|
|
151
|
+
*/
|
|
152
|
+
function diffValue(ctx, before, after, pointer, key, depth = 0) {
|
|
153
|
+
if (ctx.collector.truncated || stableKey(before) === stableKey(after))
|
|
154
|
+
return;
|
|
155
|
+
if (depth > MAX_DEPTH) {
|
|
156
|
+
ctx.collector.push(ctx.target, ctx.kind, {
|
|
157
|
+
op: '~',
|
|
158
|
+
item: pointer,
|
|
159
|
+
detail: `${summarize(before)} -> ${summarize(after)}`,
|
|
160
|
+
level: levelFor(key, '~'),
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (isObject(before) && isObject(after)) {
|
|
165
|
+
for (const childKey of unionKeys(before, after)) {
|
|
166
|
+
const hasBefore = childKey in before;
|
|
167
|
+
const hasAfter = childKey in after;
|
|
168
|
+
const childPointer = joinPath(pointer, childKey);
|
|
169
|
+
if (!hasBefore) {
|
|
170
|
+
ctx.collector.push(ctx.target, ctx.kind, {
|
|
171
|
+
op: '+',
|
|
172
|
+
item: childPointer,
|
|
173
|
+
detail: summarize(after[childKey]),
|
|
174
|
+
level: levelFor(childKey, '+'),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
else if (!hasAfter) {
|
|
178
|
+
ctx.collector.push(ctx.target, ctx.kind, {
|
|
179
|
+
op: '-',
|
|
180
|
+
item: childPointer,
|
|
181
|
+
detail: summarize(before[childKey]),
|
|
182
|
+
level: levelFor(childKey, '-'),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
diffValue(ctx, before[childKey], after[childKey], childPointer, childKey, depth + 1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
192
|
+
if (before.every(isPrimitive) && after.every(isPrimitive)) {
|
|
193
|
+
const beforeSet = new Set(before.map(stableKey));
|
|
194
|
+
const afterSet = new Set(after.map(stableKey));
|
|
195
|
+
for (const item of after) {
|
|
196
|
+
if (!beforeSet.has(stableKey(item))) {
|
|
197
|
+
ctx.collector.push(ctx.target, ctx.kind, { op: '+', item: pointer, detail: `+${summarize(item)}`, level: levelFor(key, '+') });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
for (const item of before) {
|
|
201
|
+
if (!afterSet.has(stableKey(item))) {
|
|
202
|
+
ctx.collector.push(ctx.target, ctx.kind, { op: '-', item: pointer, detail: `-${summarize(item)}`, level: levelFor(key, '-') });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const length = Math.max(before.length, after.length);
|
|
208
|
+
for (let i = 0; i < length; i++) {
|
|
209
|
+
const childPointer = `${pointer}[${i}]`;
|
|
210
|
+
if (i >= before.length) {
|
|
211
|
+
ctx.collector.push(ctx.target, ctx.kind, { op: '+', item: childPointer, detail: summarize(after[i]), level: levelFor(key, '+') });
|
|
212
|
+
}
|
|
213
|
+
else if (i >= after.length) {
|
|
214
|
+
ctx.collector.push(ctx.target, ctx.kind, { op: '-', item: childPointer, detail: summarize(before[i]), level: levelFor(key, '-') });
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
diffValue(ctx, before[i], after[i], childPointer, key, depth + 1);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
ctx.collector.push(ctx.target, ctx.kind, {
|
|
223
|
+
op: '~',
|
|
224
|
+
item: pointer,
|
|
225
|
+
detail: `${summarize(before)} -> ${summarize(after)}`,
|
|
226
|
+
level: levelFor(key, '~'),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
function collectOperations(doc) {
|
|
230
|
+
const result = new Map();
|
|
231
|
+
const paths = asObject(doc.paths);
|
|
232
|
+
for (const path of Object.keys(paths).sort()) {
|
|
233
|
+
const pathItem = asObject(paths[path]);
|
|
234
|
+
for (const method of HTTP_METHODS) {
|
|
235
|
+
const operation = pathItem[method];
|
|
236
|
+
if (!isObject(operation))
|
|
237
|
+
continue;
|
|
238
|
+
const key = `${method.toUpperCase()} ${path}`;
|
|
239
|
+
result.set(key, { key, operation, pathItem });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
function operationName(entry) {
|
|
245
|
+
return typeof entry.operation.operationId === 'string' ? entry.operation.operationId : undefined;
|
|
246
|
+
}
|
|
247
|
+
function parameterKey(parameter) {
|
|
248
|
+
if (isRefObject(parameter))
|
|
249
|
+
return `ref:${String(asObject(parameter).$ref)}`;
|
|
250
|
+
const object = asObject(parameter);
|
|
251
|
+
return `${String(object.in ?? '')}:${String(object.name ?? '')}`;
|
|
252
|
+
}
|
|
253
|
+
function parameterLabel(parameter) {
|
|
254
|
+
if (isRefObject(parameter)) {
|
|
255
|
+
const ref = String(asObject(parameter).$ref);
|
|
256
|
+
return ref.replace('#/components/', '');
|
|
257
|
+
}
|
|
258
|
+
const object = asObject(parameter);
|
|
259
|
+
return `${String(object.in ?? '')}.${String(object.name ?? '')}`;
|
|
260
|
+
}
|
|
261
|
+
function parameterSummary(parameter) {
|
|
262
|
+
const object = asObject(parameter);
|
|
263
|
+
if (isRefObject(parameter))
|
|
264
|
+
return String(object.$ref);
|
|
265
|
+
const schema = asObject(object.schema);
|
|
266
|
+
const type = schema.type ?? object.type;
|
|
267
|
+
const text = Array.isArray(type) ? type.join(' | ') : (typeof type === 'string' ? type : undefined);
|
|
268
|
+
if (!text)
|
|
269
|
+
return undefined;
|
|
270
|
+
return object.required ? text : `${text}?`;
|
|
271
|
+
}
|
|
272
|
+
/** Effective parameters of an operation: path-item level first, operation level wins. */
|
|
273
|
+
function parametersByKey(entry) {
|
|
274
|
+
const map = new Map();
|
|
275
|
+
for (const parameter of [...asArray(entry.pathItem.parameters), ...asArray(entry.operation.parameters)])
|
|
276
|
+
map.set(parameterKey(parameter), parameter);
|
|
277
|
+
return map;
|
|
278
|
+
}
|
|
279
|
+
function diffPaths(collector, before, after) {
|
|
280
|
+
const beforeOps = collectOperations(before);
|
|
281
|
+
const afterOps = collectOperations(after);
|
|
282
|
+
for (const key of unionMapKeys(beforeOps, afterOps)) {
|
|
283
|
+
const beforeOp = beforeOps.get(key);
|
|
284
|
+
const afterOp = afterOps.get(key);
|
|
285
|
+
if (!beforeOp) {
|
|
286
|
+
collector.push(key, 'api', { op: '+', detail: operationName(afterOp), level: 'additive' });
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (!afterOp) {
|
|
290
|
+
collector.push(key, 'api', { op: '-', detail: operationName(beforeOp), level: 'breaking' });
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
diffOperation(collector, beforeOp, afterOp);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function diffOperation(collector, before, after) {
|
|
297
|
+
const target = after.key;
|
|
298
|
+
const ctx = { collector, target, kind: 'api' };
|
|
299
|
+
// `parameters` / `requestBody` / `responses` have dedicated walkers below.
|
|
300
|
+
diffValue(ctx, omit(before.operation, ['parameters', 'requestBody', 'responses']), omit(after.operation, ['parameters', 'requestBody', 'responses']), '', 'operation');
|
|
301
|
+
const paramCtx = { collector, target, kind: 'param' };
|
|
302
|
+
const beforeParams = parametersByKey(before);
|
|
303
|
+
const afterParams = parametersByKey(after);
|
|
304
|
+
for (const key of unionMapKeys(beforeParams, afterParams)) {
|
|
305
|
+
const beforeParam = beforeParams.get(key);
|
|
306
|
+
const afterParam = afterParams.get(key);
|
|
307
|
+
if (beforeParam === undefined) {
|
|
308
|
+
collector.push(target, 'param', {
|
|
309
|
+
op: '+',
|
|
310
|
+
item: parameterLabel(afterParam),
|
|
311
|
+
detail: parameterSummary(afterParam),
|
|
312
|
+
level: asObject(afterParam).required ? 'breaking' : 'additive',
|
|
313
|
+
});
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (afterParam === undefined) {
|
|
317
|
+
collector.push(target, 'param', { op: '-', item: parameterLabel(beforeParam), level: 'breaking' });
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
diffValue(paramCtx, beforeParam, afterParam, parameterLabel(afterParam), 'parameter');
|
|
321
|
+
}
|
|
322
|
+
diffRequestBody(collector, target, before.operation.requestBody, after.operation.requestBody);
|
|
323
|
+
diffResponses(collector, target, before.operation.responses, after.operation.responses);
|
|
324
|
+
}
|
|
325
|
+
function diffRequestBody(collector, target, before, after) {
|
|
326
|
+
if (before === undefined && after === undefined)
|
|
327
|
+
return;
|
|
328
|
+
const ctx = { collector, target, kind: 'body' };
|
|
329
|
+
if (!isObject(before)) {
|
|
330
|
+
collector.push(target, 'body', { op: '+', item: 'requestBody', detail: summarize(after), level: 'additive' });
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (!isObject(after)) {
|
|
334
|
+
collector.push(target, 'body', { op: '-', item: 'requestBody', detail: summarize(before), level: 'breaking' });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (isRefObject(before) || isRefObject(after)) {
|
|
338
|
+
diffValue(ctx, before, after, 'requestBody', 'requestBody');
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
diffValue(ctx, omit(before, ['content']), omit(after, ['content']), 'requestBody', 'requestBody');
|
|
342
|
+
diffContent(ctx, before.content, after.content, 'requestBody');
|
|
343
|
+
}
|
|
344
|
+
function diffResponses(collector, target, before, after) {
|
|
345
|
+
const beforeResponses = asObject(before);
|
|
346
|
+
const afterResponses = asObject(after);
|
|
347
|
+
const ctx = { collector, target, kind: 'resp' };
|
|
348
|
+
for (const code of unionKeys(beforeResponses, afterResponses)) {
|
|
349
|
+
const pointer = `responses.${code}`;
|
|
350
|
+
const beforeResponse = beforeResponses[code];
|
|
351
|
+
const afterResponse = afterResponses[code];
|
|
352
|
+
if (beforeResponse === undefined) {
|
|
353
|
+
collector.push(target, 'resp', { op: '+', item: pointer, detail: summarize(afterResponse), level: 'additive' });
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (afterResponse === undefined) {
|
|
357
|
+
collector.push(target, 'resp', { op: '-', item: pointer, detail: summarize(beforeResponse), level: 'breaking' });
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (isRefObject(beforeResponse) || isRefObject(afterResponse)) {
|
|
361
|
+
diffValue(ctx, beforeResponse, afterResponse, pointer, 'responses');
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
diffValue(ctx, omit(asObject(beforeResponse), ['content']), omit(asObject(afterResponse), ['content']), pointer, 'response');
|
|
365
|
+
diffContent(ctx, asObject(beforeResponse).content, asObject(afterResponse).content, pointer);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Diff a `content` map media type by media type.
|
|
370
|
+
*
|
|
371
|
+
* `schema` is walked without its own path segment so the rows read
|
|
372
|
+
* `requestBody.application/json.properties.name.type` instead of nesting the
|
|
373
|
+
* `schema` keyword into every pointer.
|
|
374
|
+
*/
|
|
375
|
+
function diffContent(ctx, before, after, prefix) {
|
|
376
|
+
const beforeContent = asObject(before);
|
|
377
|
+
const afterContent = asObject(after);
|
|
378
|
+
for (const mediaType of unionKeys(beforeContent, afterContent)) {
|
|
379
|
+
const pointer = `${prefix}.${mediaType}`;
|
|
380
|
+
const beforeMedia = beforeContent[mediaType];
|
|
381
|
+
const afterMedia = afterContent[mediaType];
|
|
382
|
+
if (beforeMedia === undefined) {
|
|
383
|
+
ctx.collector.push(ctx.target, ctx.kind, { op: '+', item: pointer, detail: summarize(afterMedia), level: 'additive' });
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (afterMedia === undefined) {
|
|
387
|
+
ctx.collector.push(ctx.target, ctx.kind, { op: '-', item: pointer, detail: summarize(beforeMedia), level: 'breaking' });
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
diffValue(ctx, omit(asObject(beforeMedia), ['schema']), omit(asObject(afterMedia), ['schema']), pointer, 'content');
|
|
391
|
+
diffValue(ctx, asObject(beforeMedia).schema, asObject(afterMedia).schema, pointer, 'schema');
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// ──────────────────────────────────────────────────────────────
|
|
395
|
+
// Components (with reverse-`$ref` impact resolution)
|
|
396
|
+
// ──────────────────────────────────────────────────────────────
|
|
397
|
+
class RefIndex {
|
|
398
|
+
opsByRef = new Map();
|
|
399
|
+
parents = new Map();
|
|
400
|
+
static build(doc) {
|
|
401
|
+
const index = new RefIndex();
|
|
402
|
+
const paths = asObject(doc.paths);
|
|
403
|
+
for (const path of Object.keys(paths)) {
|
|
404
|
+
const pathItem = asObject(paths[path]);
|
|
405
|
+
for (const method of HTTP_METHODS) {
|
|
406
|
+
if (!isObject(pathItem[method]))
|
|
407
|
+
continue;
|
|
408
|
+
const operationKey = `${method.toUpperCase()} ${path}`;
|
|
409
|
+
for (const ref of collectRefs([pathItem[method], pathItem.parameters]))
|
|
410
|
+
index.addTo(index.opsByRef, ref, operationKey);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const components = asObject(doc.components);
|
|
414
|
+
for (const section of COMPONENT_SECTIONS) {
|
|
415
|
+
const group = asObject(components[section]);
|
|
416
|
+
for (const name of Object.keys(group)) {
|
|
417
|
+
const ref = `#/components/${section}/${name}`;
|
|
418
|
+
for (const inner of collectRefs(group[name]))
|
|
419
|
+
index.addTo(index.parents, inner, ref);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return index;
|
|
423
|
+
}
|
|
424
|
+
addTo(map, key, value) {
|
|
425
|
+
const set = map.get(key) ?? new Set();
|
|
426
|
+
set.add(value);
|
|
427
|
+
map.set(key, set);
|
|
428
|
+
}
|
|
429
|
+
/** Operations affected by a component, following transitive references. */
|
|
430
|
+
affectedBy(ref) {
|
|
431
|
+
const operations = new Set();
|
|
432
|
+
const visited = new Set();
|
|
433
|
+
const queue = [ref];
|
|
434
|
+
while (queue.length) {
|
|
435
|
+
const current = queue.shift();
|
|
436
|
+
if (visited.has(current))
|
|
437
|
+
continue;
|
|
438
|
+
visited.add(current);
|
|
439
|
+
for (const operation of this.opsByRef.get(current) ?? [])
|
|
440
|
+
operations.add(operation);
|
|
441
|
+
for (const parent of this.parents.get(current) ?? [])
|
|
442
|
+
queue.push(parent);
|
|
443
|
+
}
|
|
444
|
+
return [...operations].sort();
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/** Collect every `$ref` string inside a value. */
|
|
448
|
+
function collectRefs(value, out = []) {
|
|
449
|
+
if (Array.isArray(value)) {
|
|
450
|
+
for (const item of value)
|
|
451
|
+
collectRefs(item, out);
|
|
452
|
+
return out;
|
|
453
|
+
}
|
|
454
|
+
if (isObject(value)) {
|
|
455
|
+
if (typeof value.$ref === 'string')
|
|
456
|
+
out.push(value.$ref);
|
|
457
|
+
for (const key of Object.keys(value))
|
|
458
|
+
collectRefs(value[key], out);
|
|
459
|
+
}
|
|
460
|
+
return out;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Emit one component change.
|
|
464
|
+
*
|
|
465
|
+
* The component ref is the row target, the changed field its `item`, and the
|
|
466
|
+
* affected operations travel in `affects` so a renderer can list them next to
|
|
467
|
+
* the change (the CLI prints one per line) instead of duplicating the row.
|
|
468
|
+
*/
|
|
469
|
+
function emitComponentChange(collector, ref, affected, change) {
|
|
470
|
+
collector.push(ref, 'comp', { ...change, affects: affected.length > 0 ? affected : undefined });
|
|
471
|
+
}
|
|
472
|
+
function diffComponents(collector, before, after) {
|
|
473
|
+
const beforeComponents = asObject(before.components);
|
|
474
|
+
const afterComponents = asObject(after.components);
|
|
475
|
+
const beforeRefs = RefIndex.build(before);
|
|
476
|
+
const afterRefs = RefIndex.build(after);
|
|
477
|
+
for (const section of COMPONENT_SECTIONS) {
|
|
478
|
+
const beforeGroup = asObject(beforeComponents[section]);
|
|
479
|
+
const afterGroup = asObject(afterComponents[section]);
|
|
480
|
+
for (const name of unionKeys(beforeGroup, afterGroup)) {
|
|
481
|
+
const ref = `#/components/${section}/${name}`;
|
|
482
|
+
const beforeValue = beforeGroup[name];
|
|
483
|
+
const afterValue = afterGroup[name];
|
|
484
|
+
if (beforeValue === undefined) {
|
|
485
|
+
emitComponentChange(collector, ref, afterRefs.affectedBy(ref), { op: '+', detail: summarize(afterValue), level: 'additive' });
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (afterValue === undefined) {
|
|
489
|
+
emitComponentChange(collector, ref, beforeRefs.affectedBy(ref), { op: '-', detail: summarize(beforeValue), level: 'breaking' });
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
// Diff the component in place: the pointer starts empty so items read
|
|
493
|
+
// `properties.status.enum` rather than repeating the component ref.
|
|
494
|
+
const local = new ChangeCollector();
|
|
495
|
+
diffValue({ collector: local, target: ref, kind: 'comp' }, beforeValue, afterValue, '', name);
|
|
496
|
+
const affected = afterRefs.affectedBy(ref);
|
|
497
|
+
for (const change of local.changes) {
|
|
498
|
+
const { target: _target, kind: _kind, ...rest } = change;
|
|
499
|
+
emitComponentChange(collector, ref, affected, rest);
|
|
500
|
+
}
|
|
501
|
+
if (local.truncated)
|
|
502
|
+
collector.truncated = true;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// ──────────────────────────────────────────────────────────────
|
|
507
|
+
// Document-global keys
|
|
508
|
+
// ──────────────────────────────────────────────────────────────
|
|
509
|
+
function diffRoot(collector, before, after) {
|
|
510
|
+
for (const key of unionKeys(before, after)) {
|
|
511
|
+
if (key === 'paths' || key === 'components')
|
|
512
|
+
continue;
|
|
513
|
+
const target = `#/${key}`;
|
|
514
|
+
if (!(key in after)) {
|
|
515
|
+
collector.push(target, 'meta', { op: '-', item: key, detail: summarize(before[key]), level: 'breaking' });
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (!(key in before)) {
|
|
519
|
+
collector.push(target, 'meta', { op: '+', item: key, detail: summarize(after[key]), level: 'additive' });
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
diffValue({ collector, target, kind: 'meta' }, before[key], after[key], key, key);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// ──────────────────────────────────────────────────────────────
|
|
526
|
+
// Entry point
|
|
527
|
+
// ──────────────────────────────────────────────────────────────
|
|
528
|
+
/**
|
|
529
|
+
* Diff two source documents and return the flattened change rows.
|
|
530
|
+
*
|
|
531
|
+
* Returns an empty array when the documents are structurally identical, so the
|
|
532
|
+
* caller can decide not to write a change record at all.
|
|
533
|
+
*/
|
|
534
|
+
function diffSourceDocument(before, after) {
|
|
535
|
+
const collector = new ChangeCollector();
|
|
536
|
+
const beforeDoc = asObject(before);
|
|
537
|
+
const afterDoc = asObject(after);
|
|
538
|
+
diffPaths(collector, beforeDoc, afterDoc);
|
|
539
|
+
diffComponents(collector, beforeDoc, afterDoc);
|
|
540
|
+
diffRoot(collector, beforeDoc, afterDoc);
|
|
541
|
+
return collector.finish();
|
|
542
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.snapshotsDirPath = snapshotsDirPath;
|
|
7
|
+
exports.snapshotFilePath = snapshotFilePath;
|
|
8
|
+
exports.sourceDocumentHash = sourceDocumentHash;
|
|
9
|
+
exports.readSourceSnapshot = readSourceSnapshot;
|
|
10
|
+
exports.writeSourceSnapshot = writeSourceSnapshot;
|
|
11
|
+
exports.collectSourceChanges = collectSourceChanges;
|
|
12
|
+
const node_crypto_1 = require("node:crypto");
|
|
13
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
14
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
15
|
+
const diffDocument_1 = require("../functions/diffDocument");
|
|
16
|
+
const wormaJson_1 = require("../functions/wormaJson");
|
|
17
|
+
const logger_1 = require("../helper/logger");
|
|
18
|
+
/** Payload version of one snapshot file. */
|
|
19
|
+
const SNAPSHOT_VERSION = 1;
|
|
20
|
+
/** `<cacheRoot>/.worma-cache/snapshots/` — one snapshot per generator output. */
|
|
21
|
+
function snapshotsDirPath(projectRoot) {
|
|
22
|
+
return node_path_1.default.join((0, wormaJson_1.cacheDirPath)(projectRoot), 'snapshots');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Snapshot file name for one generator output.
|
|
26
|
+
*
|
|
27
|
+
* Keyed by the generator's `output` (resolved relative to the cache root in
|
|
28
|
+
* monorepo mode, same key as the api cache) with separators turned into `_`.
|
|
29
|
+
* Sharing a snapshot between generators is deliberately avoided even when they
|
|
30
|
+
* read the same source: they are not necessarily generated at the same time, so
|
|
31
|
+
* a shared file would let one generator's run overwrite the other's baseline.
|
|
32
|
+
*/
|
|
33
|
+
function snapshotFilePath(projectRoot, outputPath) {
|
|
34
|
+
const relative = (0, wormaJson_1.toCacheRelativePath)(projectRoot, outputPath);
|
|
35
|
+
return node_path_1.default.join(snapshotsDirPath(projectRoot), `${relative.replace(/[/\\]/g, '_')}.json`);
|
|
36
|
+
}
|
|
37
|
+
/** Stable hash of a stable-stringified source document. */
|
|
38
|
+
function sourceDocumentHash(documentText) {
|
|
39
|
+
return (0, node_crypto_1.createHash)('sha256').update(documentText).digest('hex').slice(0, 16);
|
|
40
|
+
}
|
|
41
|
+
/** Read one generator's last source snapshot. */
|
|
42
|
+
async function readSourceSnapshot(projectRoot, outputPath) {
|
|
43
|
+
try {
|
|
44
|
+
const content = JSON.parse(await promises_1.default.readFile(snapshotFilePath(projectRoot, outputPath), 'utf-8'));
|
|
45
|
+
if (!content || typeof content !== 'object' || typeof content.hash !== 'string')
|
|
46
|
+
return null;
|
|
47
|
+
return content;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Persist one generator's source snapshot. */
|
|
54
|
+
async function writeSourceSnapshot(projectRoot, outputPath, snapshot) {
|
|
55
|
+
const file = snapshotFilePath(projectRoot, outputPath);
|
|
56
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(file), { recursive: true });
|
|
57
|
+
await promises_1.default.writeFile(file, JSON.stringify(snapshot));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Compare the current source document with the previous snapshot and return the
|
|
61
|
+
* change rows.
|
|
62
|
+
*
|
|
63
|
+
* Runs after a successful generation (a failed run must not pay for the diff nor
|
|
64
|
+
* advance the baseline). The baseline is advanced on every successful run:
|
|
65
|
+
*
|
|
66
|
+
* - no previous snapshot → the first run only establishes the baseline and stays
|
|
67
|
+
* silent, otherwise every existing project would report "everything changed";
|
|
68
|
+
* - identical hash → nothing to do, not even a write;
|
|
69
|
+
* - otherwise → the structured diff is returned and the snapshot is replaced.
|
|
70
|
+
*
|
|
71
|
+
* @returns the change rows, or `undefined` when there is nothing to record.
|
|
72
|
+
*/
|
|
73
|
+
async function collectSourceChanges(options) {
|
|
74
|
+
const { projectRoot, outputPath, documentText, resolvedInput } = options;
|
|
75
|
+
const hash = sourceDocumentHash(documentText);
|
|
76
|
+
const previous = await readSourceSnapshot(projectRoot, outputPath);
|
|
77
|
+
if (previous && previous.hash === hash)
|
|
78
|
+
return undefined;
|
|
79
|
+
let document;
|
|
80
|
+
try {
|
|
81
|
+
document = JSON.parse(documentText);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
// The document is not JSON-serialisable (plugins may have attached exotic
|
|
85
|
+
// values). Change history is best-effort: skip it instead of failing.
|
|
86
|
+
logger_1.logger.debug('Skipping source change detection: document is not serialisable', { error: error?.message });
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
let changes;
|
|
90
|
+
if (previous) {
|
|
91
|
+
try {
|
|
92
|
+
changes = (0, diffDocument_1.diffSourceDocument)(previous.doc, document);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
logger_1.logger.debug('Failed to diff source documents', { error: error?.message });
|
|
96
|
+
changes = undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
await writeSourceSnapshot(projectRoot, outputPath, {
|
|
100
|
+
version: SNAPSHOT_VERSION,
|
|
101
|
+
resolvedInput,
|
|
102
|
+
hash,
|
|
103
|
+
updatedAt: Date.now(),
|
|
104
|
+
doc: document,
|
|
105
|
+
});
|
|
106
|
+
return changes?.length ? changes : undefined;
|
|
107
|
+
}
|