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,230 @@
|
|
|
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.LATEST_CHANGE_ID = void 0;
|
|
7
|
+
exports.countChanges = countChanges;
|
|
8
|
+
exports.changesDirPath = changesDirPath;
|
|
9
|
+
exports.captureChange = captureChange;
|
|
10
|
+
exports.listChanges = listChanges;
|
|
11
|
+
exports.getChange = getChange;
|
|
12
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
13
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
const config_1 = require("../config");
|
|
15
|
+
const wormaJson_1 = require("../functions/wormaJson");
|
|
16
|
+
/** Alias accepted by {@link getChange} — resolves to the newest record. */
|
|
17
|
+
exports.LATEST_CHANGE_ID = 'latest';
|
|
18
|
+
/** Aggregate the change rows of every generator into flat counts. */
|
|
19
|
+
function countChanges(generators) {
|
|
20
|
+
let added = 0;
|
|
21
|
+
let removed = 0;
|
|
22
|
+
let modified = 0;
|
|
23
|
+
for (const generator of generators) {
|
|
24
|
+
for (const change of generator.changes) {
|
|
25
|
+
if (change.op === '+')
|
|
26
|
+
added++;
|
|
27
|
+
else if (change.op === '-')
|
|
28
|
+
removed++;
|
|
29
|
+
else
|
|
30
|
+
modified++;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { added, removed, modified };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read one generator entry, upgrading legacy api-level entries on the fly.
|
|
37
|
+
*
|
|
38
|
+
* Legacy records stored `added` / `removed` / `modified` api lists with
|
|
39
|
+
* `changedFields` and carried no `schemaVersion`; normalising both shapes into
|
|
40
|
+
* flat rows means every consumer only ever handles the source-document model.
|
|
41
|
+
*/
|
|
42
|
+
function normalizeItem(raw) {
|
|
43
|
+
const output = String(raw?.output ?? '');
|
|
44
|
+
const serverName = raw?.serverName || undefined;
|
|
45
|
+
if (Array.isArray(raw?.changes))
|
|
46
|
+
return { output, serverName, resolvedInput: raw?.resolvedInput || undefined, changes: raw.changes };
|
|
47
|
+
const targetOf = (entry) => `${String(entry?.method ?? '').toUpperCase()} ${entry?.path ?? ''}`.trim();
|
|
48
|
+
const changes = [];
|
|
49
|
+
for (const entry of raw?.added ?? [])
|
|
50
|
+
changes.push({ op: '+', kind: 'api', target: targetOf(entry), detail: entry?.name || undefined, level: 'additive' });
|
|
51
|
+
for (const entry of raw?.removed ?? [])
|
|
52
|
+
changes.push({ op: '-', kind: 'api', target: targetOf(entry), detail: entry?.name || undefined, level: 'breaking' });
|
|
53
|
+
for (const entry of raw?.modified ?? []) {
|
|
54
|
+
changes.push({
|
|
55
|
+
op: '~',
|
|
56
|
+
kind: 'api',
|
|
57
|
+
target: targetOf(entry),
|
|
58
|
+
detail: (entry?.changedFields ?? []).join(', ') || undefined,
|
|
59
|
+
level: 'breaking',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return { output, serverName, changes };
|
|
63
|
+
}
|
|
64
|
+
/** `<cacheRoot>/changes/` — change records live next to the cache index. */
|
|
65
|
+
function changesDirPath(projectRoot) {
|
|
66
|
+
return node_path_1.default.join((0, wormaJson_1.cacheDirPath)(projectRoot), 'changes');
|
|
67
|
+
}
|
|
68
|
+
function recordFile(projectRoot, id) {
|
|
69
|
+
return node_path_1.default.join(changesDirPath(projectRoot), `${id}.json`);
|
|
70
|
+
}
|
|
71
|
+
function padId(seq) {
|
|
72
|
+
return String(seq).padStart(4, '0');
|
|
73
|
+
}
|
|
74
|
+
function toSummary(change) {
|
|
75
|
+
return {
|
|
76
|
+
id: change.id,
|
|
77
|
+
createdAt: change.createdAt,
|
|
78
|
+
summary: {
|
|
79
|
+
generators: change.generators.length,
|
|
80
|
+
...countChanges(change.generators),
|
|
81
|
+
},
|
|
82
|
+
outputs: change.generators.map(g => g.output),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Highest id already present in `<cacheRoot>/changes/`.
|
|
87
|
+
*
|
|
88
|
+
* The record files — not `index.json#changeSeq` — are the authoritative source:
|
|
89
|
+
* the counter lives in a file that is rewritten by several concurrent writers
|
|
90
|
+
* and can be reset (corrupt / unreadable / hand-edited `index.json`). Allocating
|
|
91
|
+
* from `max(counter, existing ids) + 1` keeps ids monotonic and, more
|
|
92
|
+
* importantly, never reuses an id — a reused id would silently overwrite an
|
|
93
|
+
* older record.
|
|
94
|
+
*/
|
|
95
|
+
async function maxChangeSeq(projectRoot) {
|
|
96
|
+
let files = [];
|
|
97
|
+
try {
|
|
98
|
+
files = await promises_1.default.readdir(changesDirPath(projectRoot));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
let max = 0;
|
|
104
|
+
for (const name of files) {
|
|
105
|
+
const seq = Number.parseInt(name.replace(/\.json$/, ''), 10);
|
|
106
|
+
if (Number.isFinite(seq) && seq > max)
|
|
107
|
+
max = seq;
|
|
108
|
+
}
|
|
109
|
+
return max;
|
|
110
|
+
}
|
|
111
|
+
async function readRecord(projectRoot, id) {
|
|
112
|
+
try {
|
|
113
|
+
const content = JSON.parse(await promises_1.default.readFile(recordFile(projectRoot, id), 'utf-8'));
|
|
114
|
+
if (!content || typeof content !== 'object')
|
|
115
|
+
return null;
|
|
116
|
+
const record = content;
|
|
117
|
+
return { ...record, generators: (record.generators ?? []).map(normalizeItem) };
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Persist one change record.
|
|
125
|
+
*
|
|
126
|
+
* Called by `generate()` **only when something actually changed**. Allocates the
|
|
127
|
+
* next `changes/<NNNN>.json` id from `index.json#changeSeq` and prunes records
|
|
128
|
+
* that fall outside `changeHistoryLimit`.
|
|
129
|
+
*
|
|
130
|
+
* @returns the allocated id (e.g. `"0007"`)
|
|
131
|
+
*/
|
|
132
|
+
async function captureChange(projectPath, change) {
|
|
133
|
+
// Never trust the counter alone: if it was reset (see {@link maxChangeSeq})
|
|
134
|
+
// `counter + 1` would collide with an existing record and overwrite it.
|
|
135
|
+
const [counter, existingMax] = await Promise.all([
|
|
136
|
+
(0, wormaJson_1.readChangeSeq)(projectPath),
|
|
137
|
+
maxChangeSeq(projectPath),
|
|
138
|
+
]);
|
|
139
|
+
const seq = Math.max(counter, existingMax) + 1;
|
|
140
|
+
const id = padId(seq);
|
|
141
|
+
const record = {
|
|
142
|
+
...change,
|
|
143
|
+
id,
|
|
144
|
+
createdAt: change.createdAt ?? Date.now(),
|
|
145
|
+
};
|
|
146
|
+
const dir = changesDirPath(projectPath);
|
|
147
|
+
await promises_1.default.mkdir(dir, { recursive: true });
|
|
148
|
+
await promises_1.default.writeFile(recordFile(projectPath, id), JSON.stringify(record));
|
|
149
|
+
// Persist the bumped counter; `writeCacheIndex` preserves existing entries.
|
|
150
|
+
await (0, wormaJson_1.writeCacheIndex)(projectPath, [], { changeSeq: seq });
|
|
151
|
+
await pruneChanges(projectPath);
|
|
152
|
+
return id;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Remove records outside the configured history window, keeping the newest
|
|
156
|
+
* `changeHistoryLimit` records. `changeHistoryLimit <= 0` means "keep everything".
|
|
157
|
+
*
|
|
158
|
+
* Ordering is by `createdAt` (not by id): ids can be out of chronological order
|
|
159
|
+
* when `index.json#changeSeq` was reset at some point, and pruning by id
|
|
160
|
+
* arithmetic would then delete the newest records instead of the oldest ones.
|
|
161
|
+
*/
|
|
162
|
+
async function pruneChanges(projectPath) {
|
|
163
|
+
const limit = (0, config_1.getGlobalConfig)().changeHistoryLimit;
|
|
164
|
+
if (typeof limit !== 'number' || limit <= 0)
|
|
165
|
+
return;
|
|
166
|
+
// `listChanges` returns the records newest first.
|
|
167
|
+
const summaries = await listChanges(projectPath);
|
|
168
|
+
const stale = summaries.slice(limit);
|
|
169
|
+
await Promise.all(stale.map(s => promises_1.default.unlink(recordFile(projectPath, s.id)).catch(() => { })));
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* List recorded changes, newest first.
|
|
173
|
+
*
|
|
174
|
+
* Sorted by `createdAt` (id as tie-breaker) rather than by file name: an id is
|
|
175
|
+
* only chronological as long as `index.json#changeSeq` never resets, and a
|
|
176
|
+
* reset would otherwise make a brand-new record show up last.
|
|
177
|
+
*/
|
|
178
|
+
async function listChanges(projectPath) {
|
|
179
|
+
const dir = changesDirPath(projectPath);
|
|
180
|
+
let files = [];
|
|
181
|
+
try {
|
|
182
|
+
files = await promises_1.default.readdir(dir);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
const ids = files
|
|
188
|
+
.filter(name => name.endsWith('.json'))
|
|
189
|
+
.map(name => name.replace(/\.json$/, ''));
|
|
190
|
+
const summaries = [];
|
|
191
|
+
for (const id of ids) {
|
|
192
|
+
const record = await readRecord(projectPath, id);
|
|
193
|
+
if (record)
|
|
194
|
+
summaries.push(toSummary(record));
|
|
195
|
+
}
|
|
196
|
+
summaries.sort((a, b) => (b.createdAt - a.createdAt) || b.id.localeCompare(a.id));
|
|
197
|
+
return summaries;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Read a single change record.
|
|
201
|
+
*
|
|
202
|
+
* @param projectPath absolute path of the project root
|
|
203
|
+
* @param id `"0007"` or the alias `"latest"` (newest record)
|
|
204
|
+
*/
|
|
205
|
+
async function getChange(projectPath, id) {
|
|
206
|
+
let resolvedId = id;
|
|
207
|
+
if (!id || id === exports.LATEST_CHANGE_ID) {
|
|
208
|
+
// Resolve the newest record by scanning the changes directory rather than
|
|
209
|
+
// trusting `index.json#changeSeq`: the counter can be stale or missing
|
|
210
|
+
// (pruned records, manual edits, or a generation that rewrote index.json
|
|
211
|
+
// without carrying the counter) while the record files themselves remain
|
|
212
|
+
// the authoritative source.
|
|
213
|
+
const latestId = await resolveLatestId(projectPath);
|
|
214
|
+
if (!latestId)
|
|
215
|
+
return undefined;
|
|
216
|
+
resolvedId = latestId;
|
|
217
|
+
}
|
|
218
|
+
const record = await readRecord(projectPath, resolvedId);
|
|
219
|
+
return record ?? undefined;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Return the newest change-record id.
|
|
223
|
+
*
|
|
224
|
+
* Resolved by `createdAt` (see {@link listChanges}) so `latest` keeps pointing
|
|
225
|
+
* at the most recent generation even when ids are not chronological.
|
|
226
|
+
*/
|
|
227
|
+
async function resolveLatestId(projectRoot) {
|
|
228
|
+
const [newest] = await listChanges(projectRoot);
|
|
229
|
+
return newest?.id;
|
|
230
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.apiDiffKey = apiDiffKey;
|
|
4
|
+
exports.diffApis = diffApis;
|
|
5
|
+
exports.hasApiChanges = hasApiChanges;
|
|
6
|
+
const wormaJson_1 = require("../functions/wormaJson");
|
|
7
|
+
/**
|
|
8
|
+
* Fields compared when two APIs share the same `method` + `path` key.
|
|
9
|
+
* `name` / `tag` are included so that renames and tag moves are reported
|
|
10
|
+
* instead of being silently ignored.
|
|
11
|
+
*/
|
|
12
|
+
const COMPARED_FIELDS = [
|
|
13
|
+
'name',
|
|
14
|
+
'tag',
|
|
15
|
+
'response',
|
|
16
|
+
'requestBody',
|
|
17
|
+
'queryParameters',
|
|
18
|
+
'pathParameters',
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Stable matching key for an API.
|
|
22
|
+
*
|
|
23
|
+
* `method` + `path` is used instead of `name` because it survives function
|
|
24
|
+
* renames: a renamed API is reported as *modified* rather than
|
|
25
|
+
* removed + added.
|
|
26
|
+
*/
|
|
27
|
+
function apiDiffKey(api) {
|
|
28
|
+
return `${String(api.method ?? '').toLowerCase()} ${api.path ?? ''}`;
|
|
29
|
+
}
|
|
30
|
+
function toChange(api) {
|
|
31
|
+
return {
|
|
32
|
+
method: api.method,
|
|
33
|
+
path: api.path,
|
|
34
|
+
name: api.name,
|
|
35
|
+
tag: api.tag,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Diff two API lists at API level.
|
|
40
|
+
*
|
|
41
|
+
* @param oldApis API list as of the previous generation (from cache)
|
|
42
|
+
* @param newApis API list parsed from the current spec
|
|
43
|
+
*/
|
|
44
|
+
function diffApis(oldApis = [], newApis = []) {
|
|
45
|
+
const oldMap = new Map();
|
|
46
|
+
for (const api of oldApis) {
|
|
47
|
+
oldMap.set(apiDiffKey(api), api);
|
|
48
|
+
}
|
|
49
|
+
const newMap = new Map();
|
|
50
|
+
for (const api of newApis) {
|
|
51
|
+
newMap.set(apiDiffKey(api), api);
|
|
52
|
+
}
|
|
53
|
+
const added = [];
|
|
54
|
+
const removed = [];
|
|
55
|
+
const modified = [];
|
|
56
|
+
for (const [key, api] of newMap) {
|
|
57
|
+
if (!oldMap.has(key))
|
|
58
|
+
added.push(toChange(api));
|
|
59
|
+
}
|
|
60
|
+
for (const [key, api] of oldMap) {
|
|
61
|
+
if (!newMap.has(key))
|
|
62
|
+
removed.push(toChange(api));
|
|
63
|
+
}
|
|
64
|
+
for (const [key, api] of newMap) {
|
|
65
|
+
const previous = oldMap.get(key);
|
|
66
|
+
if (!previous)
|
|
67
|
+
continue;
|
|
68
|
+
const changedFields = COMPARED_FIELDS.filter(field => (0, wormaJson_1.stableStringify)(previous[field]) !== (0, wormaJson_1.stableStringify)(api[field]));
|
|
69
|
+
if (changedFields.length > 0) {
|
|
70
|
+
modified.push({ ...toChange(api), changedFields: [...changedFields] });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const byKey = (a, b) => apiDiffKey(a).localeCompare(apiDiffKey(b));
|
|
74
|
+
added.sort(byKey);
|
|
75
|
+
removed.sort(byKey);
|
|
76
|
+
modified.sort(byKey);
|
|
77
|
+
return { added, removed, modified };
|
|
78
|
+
}
|
|
79
|
+
/** Whether a diff result contains any change at all. */
|
|
80
|
+
function hasApiChanges(diff) {
|
|
81
|
+
return diff.added.length > 0 || diff.removed.length > 0 || diff.modified.length > 0;
|
|
82
|
+
}
|