wormajs 0.4.0 → 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/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/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 +166 -81
- package/dist/plugins/presets/payloadModifier/hepler.js +0 -347
- package/dist/plugins/presets/platform/fastapi.js +0 -22
- package/dist/template/presets/alova/partials/dts-extra-config.handlebars +0 -8
|
@@ -3,21 +3,32 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getCacheRoot = getCacheRoot;
|
|
7
|
+
exports.cacheDirPath = cacheDirPath;
|
|
6
8
|
exports.toCacheRelativePath = toCacheRelativePath;
|
|
9
|
+
exports.stableStringify = stableStringify;
|
|
10
|
+
exports.parseSpecText = parseSpecText;
|
|
11
|
+
exports.normalizeSpecText = normalizeSpecText;
|
|
12
|
+
exports.computeSpecHash = computeSpecHash;
|
|
7
13
|
exports.computeApiHash = computeApiHash;
|
|
8
14
|
exports.computePerTagHashes = computePerTagHashes;
|
|
9
15
|
exports.readCacheIndex = readCacheIndex;
|
|
10
16
|
exports.writeApiCacheEntry = writeApiCacheEntry;
|
|
11
17
|
exports.writeCacheIndex = writeCacheIndex;
|
|
12
18
|
exports.writeCacheEntry = writeCacheEntry;
|
|
19
|
+
exports.updateSourceBaselines = updateSourceBaselines;
|
|
20
|
+
exports.updateSourceBaseline = updateSourceBaseline;
|
|
13
21
|
exports.readCacheApis = readCacheApis;
|
|
14
22
|
exports.readAllCacheApis = readAllCacheApis;
|
|
15
23
|
exports.getCacheEntry = getCacheEntry;
|
|
24
|
+
exports.hasGenerationBaseline = hasGenerationBaseline;
|
|
25
|
+
exports.readChangeSeq = readChangeSeq;
|
|
16
26
|
exports.diffChangedTags = diffChangedTags;
|
|
17
27
|
exports.toCacheData = toCacheData;
|
|
18
28
|
const node_crypto_1 = require("node:crypto");
|
|
19
29
|
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
20
30
|
const node_path_1 = __importDefault(require("node:path"));
|
|
31
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
21
32
|
const config_1 = require("../config");
|
|
22
33
|
// ---- Directory-based cache format ----
|
|
23
34
|
const CACHE_SCHEMA_VERSION = 1;
|
|
@@ -46,6 +57,58 @@ function toCacheRelativePath(projectPath, outputPath) {
|
|
|
46
57
|
: node_path_1.default.resolve(projectPath, outputPath);
|
|
47
58
|
return (node_path_1.default.relative(cacheRoot, absoluteOutput) || '.').replace(/\\/g, '/');
|
|
48
59
|
}
|
|
60
|
+
// ---- Spec text normalization + hashing (requirement A) ----
|
|
61
|
+
/**
|
|
62
|
+
* Deterministic JSON serialization: object keys are sorted so that two
|
|
63
|
+
* semantically equal documents always produce the same string.
|
|
64
|
+
*/
|
|
65
|
+
function stableStringify(value) {
|
|
66
|
+
if (value === undefined)
|
|
67
|
+
return 'undefined';
|
|
68
|
+
if (value === null || typeof value !== 'object')
|
|
69
|
+
return JSON.stringify(value) ?? 'null';
|
|
70
|
+
if (Array.isArray(value))
|
|
71
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
72
|
+
const record = value;
|
|
73
|
+
return `{${Object.keys(record).sort().map(k => `${JSON.stringify(k)}:${stableStringify(record[k])}`).join(',')}}`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Parse a raw spec text (JSON or YAML) into a plain structure.
|
|
77
|
+
* Throws when the text cannot be parsed at all.
|
|
78
|
+
*/
|
|
79
|
+
function parseSpecText(text) {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(text);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
const loaded = js_yaml_1.default.load(text);
|
|
85
|
+
if (loaded !== undefined)
|
|
86
|
+
return loaded;
|
|
87
|
+
throw new Error('Unable to parse spec text as JSON or YAML');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Normalize a raw spec text into a canonical form so that cosmetic differences
|
|
92
|
+
* (key order, indentation, JSON vs YAML, trailing whitespace) collapse into the
|
|
93
|
+
* same value. Falls back to the raw (trimmed) text when it is not a structured
|
|
94
|
+
* document.
|
|
95
|
+
*/
|
|
96
|
+
function normalizeSpecText(text) {
|
|
97
|
+
try {
|
|
98
|
+
const parsed = parseSpecText(text);
|
|
99
|
+
if (parsed && typeof parsed === 'object')
|
|
100
|
+
return stableStringify(parsed);
|
|
101
|
+
}
|
|
102
|
+
catch { /* not a structured document — fall through */ }
|
|
103
|
+
return text.trim();
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Compute a stable hash of a raw spec text (JSON or YAML).
|
|
107
|
+
* Stable across key reordering, whitespace and JSON/YAML equivalence.
|
|
108
|
+
*/
|
|
109
|
+
function computeSpecHash(text) {
|
|
110
|
+
return (0, node_crypto_1.createHash)('sha256').update(normalizeSpecText(text)).digest('hex').slice(0, 16);
|
|
111
|
+
}
|
|
49
112
|
// ---- M3-B2: API hash computation ----
|
|
50
113
|
/**
|
|
51
114
|
* P2: WeakMap cache for per-Api hashes. Same Api object reused across
|
|
@@ -101,77 +164,248 @@ function computePerTagHashes(allApis, tagedApis) {
|
|
|
101
164
|
}
|
|
102
165
|
// ---- M3-B3: New format read/write ----
|
|
103
166
|
async function readCacheIndex(projectRoot) {
|
|
167
|
+
const indexFile = node_path_1.default.join(cacheDirPath(projectRoot), 'index.json');
|
|
168
|
+
// Retry on parse failures: `index.json` is replaced (not appended to) by its
|
|
169
|
+
// writers, so a concurrent writer can leave a truncated file behind. Reading
|
|
170
|
+
// it as `null` would make the next writer start from an empty index and drop
|
|
171
|
+
// `changeSeq` / `tags` / `source` baselines accumulated so far.
|
|
172
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
173
|
+
let content;
|
|
174
|
+
try {
|
|
175
|
+
content = await promises_1.default.readFile(indexFile, 'utf-8');
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return null; // no cache yet
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const index = JSON.parse(content);
|
|
182
|
+
if (index.schemaVersion !== CACHE_SCHEMA_VERSION)
|
|
183
|
+
return null;
|
|
184
|
+
if (!Array.isArray(index.entries))
|
|
185
|
+
return null;
|
|
186
|
+
return index;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// Partially written file — back off briefly and re-read.
|
|
190
|
+
if (attempt < 2)
|
|
191
|
+
await new Promise(resolve => setTimeout(resolve, 10 * (attempt + 1)));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
/** Whether `<cacheRoot>/.worma-cache/index.json` exists on disk (readable or not). */
|
|
197
|
+
async function cacheIndexExists(projectRoot) {
|
|
104
198
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const index = JSON.parse(content);
|
|
108
|
-
if (index.schemaVersion !== CACHE_SCHEMA_VERSION)
|
|
109
|
-
return null;
|
|
110
|
-
if (!Array.isArray(index.entries))
|
|
111
|
-
return null;
|
|
112
|
-
return index;
|
|
199
|
+
await promises_1.default.access(node_path_1.default.join(cacheDirPath(projectRoot), 'index.json'));
|
|
200
|
+
return true;
|
|
113
201
|
}
|
|
114
202
|
catch {
|
|
115
|
-
return
|
|
203
|
+
return false;
|
|
116
204
|
}
|
|
117
205
|
}
|
|
118
|
-
/**
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const slug = slugify(outputPath);
|
|
122
|
-
const entryFile = node_path_1.default.join(dataDir, `${slug}.json`);
|
|
123
|
-
await promises_1.default.mkdir(node_path_1.default.dirname(entryFile), { recursive: true });
|
|
124
|
-
await promises_1.default.writeFile(entryFile, JSON.stringify({ serverName, apis }));
|
|
206
|
+
/** Directory holding one cache entry's per-tag data files: `data/<slug>/<tag>.json` */
|
|
207
|
+
function dataEntryDir(projectRoot, relativePath) {
|
|
208
|
+
return node_path_1.default.join(cacheDirPath(projectRoot), 'data', slugify(relativePath));
|
|
125
209
|
}
|
|
126
|
-
/**
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
210
|
+
/** Legacy single-file location kept for backward-compatible reads: `data/<slug>.json` */
|
|
211
|
+
function legacyDataEntryFile(projectRoot, relativePath) {
|
|
212
|
+
return node_path_1.default.join(cacheDirPath(projectRoot), 'data', `${slugify(relativePath)}.json`);
|
|
213
|
+
}
|
|
214
|
+
function tagFileName(tag) {
|
|
215
|
+
return `${slugify(tag) || '_default'}.json`;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Write a single entry's API data files to disk (without updating index).
|
|
219
|
+
*
|
|
220
|
+
* Data is stored **per tag** (`data/<slug>/<tag>.json`) so that a large spec
|
|
221
|
+
* only rewrites the files of the tags that actually changed. Tag files that no
|
|
222
|
+
* longer exist in `apis` are removed.
|
|
223
|
+
*/
|
|
224
|
+
async function writeApiCacheEntry(projectRoot, outputPath, serverName, apis) {
|
|
225
|
+
const relativePath = toCacheRelativePath(projectRoot, outputPath);
|
|
226
|
+
const dir = dataEntryDir(projectRoot, relativePath);
|
|
227
|
+
await promises_1.default.mkdir(dir, { recursive: true });
|
|
228
|
+
const byTag = new Map();
|
|
229
|
+
for (const api of apis) {
|
|
230
|
+
const tag = api.tag ?? '';
|
|
231
|
+
const list = byTag.get(tag) || [];
|
|
232
|
+
list.push(api);
|
|
233
|
+
byTag.set(tag, list);
|
|
135
234
|
}
|
|
136
|
-
|
|
137
|
-
|
|
235
|
+
const kept = new Set();
|
|
236
|
+
for (const [tag, tagApis] of byTag) {
|
|
237
|
+
const fileName = tagFileName(tag);
|
|
238
|
+
kept.add(fileName);
|
|
239
|
+
await promises_1.default.writeFile(node_path_1.default.join(dir, fileName), JSON.stringify({ serverName, tag, apis: tagApis }));
|
|
138
240
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
else
|
|
144
|
-
index.entries.push(newEntry);
|
|
241
|
+
// Drop tag files of tags that disappeared from the spec
|
|
242
|
+
let existing = [];
|
|
243
|
+
try {
|
|
244
|
+
existing = await promises_1.default.readdir(dir);
|
|
145
245
|
}
|
|
146
|
-
|
|
147
|
-
await
|
|
148
|
-
|
|
246
|
+
catch { /* directory may not exist yet */ }
|
|
247
|
+
await Promise.all(existing
|
|
248
|
+
.filter(name => name.endsWith('.json') && !kept.has(name))
|
|
249
|
+
.map(name => promises_1.default.unlink(node_path_1.default.join(dir, name)).catch(() => { })));
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Serializes every write to `index.json`.
|
|
253
|
+
*
|
|
254
|
+
* Writing the index is a read → modify → write-whole-file operation shared by
|
|
255
|
+
* three call sites that all run concurrently (`flushAllData` after a
|
|
256
|
+
* generation, `checkUpdates()` baselines and `captureChange()`'s sequence
|
|
257
|
+
* counter). Chaining them makes the operation atomic with respect to itself, so
|
|
258
|
+
* no writer can drop another's `entries`, `source` baseline or `changeSeq`.
|
|
259
|
+
*/
|
|
260
|
+
let indexWriteChain = Promise.resolve();
|
|
261
|
+
function enqueueIndexWrite(task) {
|
|
262
|
+
// `.then(task, task)` keeps the chain alive even if a previous write rejected.
|
|
263
|
+
const run = indexWriteChain.then(task, task);
|
|
264
|
+
indexWriteChain = run.catch(() => { });
|
|
265
|
+
return run;
|
|
149
266
|
}
|
|
150
|
-
|
|
267
|
+
/** Write/update the cache index.json from pre-computed entries. */
|
|
268
|
+
function writeCacheIndex(projectRoot, newEntries, options) {
|
|
269
|
+
return enqueueIndexWrite(async () => {
|
|
270
|
+
const dir = cacheDirPath(projectRoot);
|
|
271
|
+
// `readCacheIndex` retries transient read failures; falling back to an
|
|
272
|
+
// empty index here would silently drop `changeSeq` and every `source` /
|
|
273
|
+
// `tags` baseline accumulated so far.
|
|
274
|
+
const index = (await readCacheIndex(projectRoot)) ?? { schemaVersion: CACHE_SCHEMA_VERSION, entries: [] };
|
|
275
|
+
if (!Array.isArray(index.entries))
|
|
276
|
+
index.entries = [];
|
|
277
|
+
for (const newEntry of newEntries) {
|
|
278
|
+
const existingIdx = index.entries.findIndex(e => e.path === newEntry.path);
|
|
279
|
+
if (existingIdx >= 0) {
|
|
280
|
+
// Preserve the detection baseline (`source`) written by checkUpdates()
|
|
281
|
+
index.entries[existingIdx] = {
|
|
282
|
+
...newEntry,
|
|
283
|
+
source: newEntry.source ?? index.entries[existingIdx].source,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
index.entries.push(newEntry);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
index.entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
291
|
+
if (options?.changeSeq !== undefined)
|
|
292
|
+
index.changeSeq = options.changeSeq;
|
|
293
|
+
await promises_1.default.mkdir(dir, { recursive: true });
|
|
294
|
+
await promises_1.default.writeFile(node_path_1.default.join(dir, 'index.json'), JSON.stringify(index));
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
async function writeCacheEntry(projectRoot, outputPath, serverName, apis, hashInfo, source) {
|
|
151
298
|
// Resolve to cache-root-relative path: in monorepo this produces e.g. "packages/a/src/api"
|
|
152
299
|
const relativePath = toCacheRelativePath(projectRoot, outputPath);
|
|
153
300
|
await writeApiCacheEntry(projectRoot, relativePath, serverName, apis);
|
|
154
|
-
await writeCacheIndex(projectRoot, [{
|
|
301
|
+
await writeCacheIndex(projectRoot, [{
|
|
302
|
+
path: relativePath,
|
|
303
|
+
serverName,
|
|
304
|
+
hash: hashInfo.hash,
|
|
305
|
+
tags: hashInfo.tags,
|
|
306
|
+
source,
|
|
307
|
+
}]);
|
|
155
308
|
}
|
|
156
|
-
/**
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
309
|
+
/**
|
|
310
|
+
* Update **only** the `source` sub-field of cache index entries, leaving the
|
|
311
|
+
* generation-side `hash` / `tags` untouched. Used by update detection
|
|
312
|
+
* (requirement A) so that checking for updates never invalidates the
|
|
313
|
+
* incremental-render baseline.
|
|
314
|
+
*
|
|
315
|
+
* All records are merged in a **single** read → write pass (no lost updates),
|
|
316
|
+
* and concurrent callers are serialized through {@link enqueueIndexWrite}.
|
|
317
|
+
*/
|
|
318
|
+
function updateSourceBaselines(projectRoot, records) {
|
|
319
|
+
if (records.length === 0)
|
|
320
|
+
return Promise.resolve();
|
|
321
|
+
return enqueueIndexWrite(async () => {
|
|
322
|
+
const dir = cacheDirPath(projectRoot);
|
|
323
|
+
const existing = await readCacheIndex(projectRoot);
|
|
324
|
+
if (!existing && await cacheIndexExists(projectRoot)) {
|
|
325
|
+
// `index.json` exists but could not be read (another writer is mid-write,
|
|
326
|
+
// or the file is corrupt). Writing a fresh index here would wipe the
|
|
327
|
+
// generation baseline (`hash` / `tags`) and `changeSeq`, so skip this
|
|
328
|
+
// baseline refresh instead — detection is best-effort, generation is not.
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const index = existing ?? { schemaVersion: CACHE_SCHEMA_VERSION, entries: [] };
|
|
332
|
+
for (const { outputPath, serverName, source } of records) {
|
|
333
|
+
const relativePath = toCacheRelativePath(projectRoot, outputPath);
|
|
334
|
+
const idx = index.entries.findIndex(e => e.path === relativePath);
|
|
335
|
+
if (idx >= 0) {
|
|
336
|
+
index.entries[idx] = {
|
|
337
|
+
...index.entries[idx],
|
|
338
|
+
source: { ...index.entries[idx].source, ...source },
|
|
339
|
+
};
|
|
168
340
|
}
|
|
169
|
-
|
|
170
|
-
|
|
341
|
+
else {
|
|
342
|
+
index.entries.push({
|
|
343
|
+
path: relativePath,
|
|
344
|
+
serverName: serverName ?? '',
|
|
345
|
+
hash: '',
|
|
346
|
+
tags: {},
|
|
347
|
+
source,
|
|
348
|
+
});
|
|
171
349
|
}
|
|
172
350
|
}
|
|
351
|
+
await promises_1.default.mkdir(dir, { recursive: true });
|
|
352
|
+
await promises_1.default.writeFile(node_path_1.default.join(dir, 'index.json'), JSON.stringify(index));
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Update a single entry's `source` baseline. Thin wrapper over
|
|
357
|
+
* {@link updateSourceBaselines} so existing call sites stay concurrency-safe.
|
|
358
|
+
*/
|
|
359
|
+
function updateSourceBaseline(projectRoot, outputPath, source, serverName = '') {
|
|
360
|
+
return updateSourceBaselines(projectRoot, [{ outputPath, serverName, source }]);
|
|
361
|
+
}
|
|
362
|
+
/** Read one entry's APIs, aggregating every `data/<slug>/<tag>.json` file. */
|
|
363
|
+
async function readEntryApis(projectRoot, relativePath, fallbackServerName = '') {
|
|
364
|
+
const dir = dataEntryDir(projectRoot, relativePath);
|
|
365
|
+
let files = [];
|
|
366
|
+
try {
|
|
367
|
+
files = await promises_1.default.readdir(dir);
|
|
173
368
|
}
|
|
174
|
-
|
|
369
|
+
catch {
|
|
370
|
+
// Legacy single-file layout (pre per-tag split)
|
|
371
|
+
try {
|
|
372
|
+
const content = JSON.parse(await promises_1.default.readFile(legacyDataEntryFile(projectRoot, relativePath), 'utf-8'));
|
|
373
|
+
return {
|
|
374
|
+
path: relativePath,
|
|
375
|
+
serverName: content.serverName || fallbackServerName,
|
|
376
|
+
apis: content.apis || [],
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const apis = [];
|
|
384
|
+
let serverName = fallbackServerName;
|
|
385
|
+
for (const name of files) {
|
|
386
|
+
if (!name.endsWith('.json'))
|
|
387
|
+
continue;
|
|
388
|
+
try {
|
|
389
|
+
const content = JSON.parse(await promises_1.default.readFile(node_path_1.default.join(dir, name), 'utf-8'));
|
|
390
|
+
if (content?.serverName)
|
|
391
|
+
serverName = content.serverName;
|
|
392
|
+
if (Array.isArray(content?.apis))
|
|
393
|
+
apis.push(...content.apis);
|
|
394
|
+
}
|
|
395
|
+
catch { /* skip corrupted data file */ }
|
|
396
|
+
}
|
|
397
|
+
return { path: relativePath, serverName, apis };
|
|
398
|
+
}
|
|
399
|
+
/** Read APIs from cache directory */
|
|
400
|
+
async function readCacheApis(projectRoot, outputPath) {
|
|
401
|
+
const relativePath = toCacheRelativePath(projectRoot, outputPath);
|
|
402
|
+
const index = await readCacheIndex(projectRoot);
|
|
403
|
+
if (!index)
|
|
404
|
+
return null;
|
|
405
|
+
const entry = index.entries.find(e => e.path === relativePath);
|
|
406
|
+
if (!entry)
|
|
407
|
+
return null;
|
|
408
|
+
return readEntryApis(projectRoot, relativePath, entry.serverName || '');
|
|
175
409
|
}
|
|
176
410
|
/**
|
|
177
411
|
* Read ALL cached API entries from the unified cache directory.
|
|
@@ -183,23 +417,14 @@ async function readAllCacheApis(projectRoot) {
|
|
|
183
417
|
if (!index)
|
|
184
418
|
return [];
|
|
185
419
|
const results = [];
|
|
186
|
-
const dataDir = node_path_1.default.join(cacheDirPath(projectRoot), 'data');
|
|
187
420
|
for (const entry of index.entries) {
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const content = JSON.parse(await promises_1.default.readFile(entryFile, 'utf-8'));
|
|
192
|
-
results.push({
|
|
193
|
-
path: entry.path,
|
|
194
|
-
serverName: content.serverName || entry.serverName || '',
|
|
195
|
-
apis: content.apis || [],
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
catch { /* skip corrupted data file */ }
|
|
421
|
+
const data = await readEntryApis(projectRoot, entry.path, entry.serverName || '');
|
|
422
|
+
if (data)
|
|
423
|
+
results.push(data);
|
|
199
424
|
}
|
|
200
425
|
return results;
|
|
201
426
|
}
|
|
202
|
-
/** Get cache entry metadata (hash + tags) without loading APIs */
|
|
427
|
+
/** Get cache entry metadata (hash + tags + source baseline) without loading APIs */
|
|
203
428
|
async function getCacheEntry(projectRoot, outputPath) {
|
|
204
429
|
const relativePath = toCacheRelativePath(projectRoot, outputPath);
|
|
205
430
|
const index = await readCacheIndex(projectRoot);
|
|
@@ -207,6 +432,21 @@ async function getCacheEntry(projectRoot, outputPath) {
|
|
|
207
432
|
return null;
|
|
208
433
|
return index.entries.find(e => e.path === relativePath) ?? null;
|
|
209
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* Whether an entry carries a usable incremental-render baseline.
|
|
437
|
+
*
|
|
438
|
+
* Entries created by update detection (`updateSourceBaseline`) only hold a
|
|
439
|
+
* `source` baseline and have no `tags`, so they must NOT be treated as
|
|
440
|
+
* "everything already rendered".
|
|
441
|
+
*/
|
|
442
|
+
function hasGenerationBaseline(entry) {
|
|
443
|
+
return !!entry?.tags && Object.keys(entry.tags).length > 0;
|
|
444
|
+
}
|
|
445
|
+
/** Read the change-record sequence counter from index.json (requirement B). */
|
|
446
|
+
async function readChangeSeq(projectRoot) {
|
|
447
|
+
const index = await readCacheIndex(projectRoot);
|
|
448
|
+
return index?.changeSeq ?? 0;
|
|
449
|
+
}
|
|
210
450
|
/** Find set of tags whose hash differs between old and new */
|
|
211
451
|
function diffChangedTags(oldTags, newTags) {
|
|
212
452
|
const changed = new Set();
|
package/dist/generate.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const poolManager_1 = require("./core/workerPool/poolManager");
|
|
4
|
+
const changeReport_1 = require("./functions/changeReport");
|
|
4
5
|
const helper_1 = require("./helper");
|
|
5
6
|
const GeneratorHelper_1 = require("./helper/config/GeneratorHelper");
|
|
6
7
|
const progress_1 = require("./helper/progress");
|
|
@@ -11,7 +12,7 @@ const progress_1 = require("./helper/progress");
|
|
|
11
12
|
* its lifecycle via {@link GeneratorProgressEvent} discriminated union events.
|
|
12
13
|
*
|
|
13
14
|
* @param config generating config
|
|
14
|
-
* @param options config rules that contains `
|
|
15
|
+
* @param options config rules that contains `projectPath`, `onProgress`
|
|
15
16
|
* @returns An array that contains the result of `generator` items in configuration whether generation is successful.
|
|
16
17
|
*/
|
|
17
18
|
async function generate(config, options) {
|
|
@@ -24,6 +25,9 @@ async function generate(config, options) {
|
|
|
24
25
|
const helper = new helper_1.ConfigHelper();
|
|
25
26
|
await helper.load(config, projectPath);
|
|
26
27
|
const generators = helper.getConfig().generator;
|
|
28
|
+
// Requirement B: one change record per `generate()` run, aggregating every
|
|
29
|
+
// generator that actually changed something.
|
|
30
|
+
const changeItems = [];
|
|
27
31
|
// Run all generators in parallel, each with its own ProgressTracker
|
|
28
32
|
const results = await Promise.all(generators.map(async (gen, i) => {
|
|
29
33
|
const generatorName = gen.input || `generator-${i}`;
|
|
@@ -45,10 +49,11 @@ async function generate(config, options) {
|
|
|
45
49
|
tracker.start();
|
|
46
50
|
try {
|
|
47
51
|
const result = await GeneratorHelper_1.GeneratorHelper.generate(gen, {
|
|
48
|
-
force: options?.force,
|
|
49
52
|
projectPath,
|
|
50
53
|
tracker,
|
|
51
54
|
});
|
|
55
|
+
if (result.change)
|
|
56
|
+
changeItems.push(result.change);
|
|
52
57
|
emit?.({
|
|
53
58
|
index: i,
|
|
54
59
|
phase: result.success ? 'done' : 'skipped',
|
|
@@ -74,6 +79,23 @@ async function generate(config, options) {
|
|
|
74
79
|
tracker.stop();
|
|
75
80
|
}
|
|
76
81
|
}));
|
|
82
|
+
// Requirement B: one record per run, written only when at least one generator
|
|
83
|
+
// observed a source-document change (keeps the history meaningful).
|
|
84
|
+
if (changeItems.length > 0) {
|
|
85
|
+
try {
|
|
86
|
+
const id = await (0, changeReport_1.captureChange)(projectPath, {
|
|
87
|
+
schemaVersion: 1,
|
|
88
|
+
createdAt: Date.now(),
|
|
89
|
+
projectPath,
|
|
90
|
+
generators: changeItems,
|
|
91
|
+
});
|
|
92
|
+
options?.onChangeRecorded?.({ id, ...(0, changeReport_1.countChanges)(changeItems) });
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
// A failure to record history must never fail the generation itself.
|
|
96
|
+
helper_1.logger.debug('Failed to capture change record', { error: error?.message });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
77
99
|
helper_1.logger.debug('Flushing template data cache', { projectPath });
|
|
78
100
|
await helper_1.TemplateHelper.flushAllData(projectPath);
|
|
79
101
|
poolManager_1.PoolManager.getInstance().releaseAll();
|
|
@@ -45,9 +45,8 @@ class ConfigHelper {
|
|
|
45
45
|
.generator.map(item => GeneratorHelper_1.GeneratorHelper.openApiData(item, this.projectPath)));
|
|
46
46
|
}
|
|
47
47
|
async generate(options) {
|
|
48
|
-
const { tracker
|
|
48
|
+
const { tracker } = options;
|
|
49
49
|
const results = await Promise.all(this.configManager.getConfig().generator.map(item => GeneratorHelper_1.GeneratorHelper.generate(item, {
|
|
50
|
-
force,
|
|
51
50
|
projectPath: this.projectPath,
|
|
52
51
|
tracker,
|
|
53
52
|
})));
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.ConfigManager = void 0;
|
|
7
7
|
const zod_validation_error_1 = require("zod-validation-error");
|
|
8
|
+
const config_1 = require("../../config");
|
|
8
9
|
const prepareConfig_1 = __importDefault(require("../../functions/prepareConfig"));
|
|
9
10
|
const GeneratorHelper_1 = require("../../helper/config/GeneratorHelper");
|
|
10
11
|
const logger_1 = require("../../helper/logger");
|
|
@@ -30,6 +31,9 @@ class ConfigManager {
|
|
|
30
31
|
// update config
|
|
31
32
|
this.config = validatedConfig;
|
|
32
33
|
this.readConfig = Object.freeze(this.config);
|
|
34
|
+
// 同步格式化配置到全局,供 utils/format 读取。
|
|
35
|
+
// 每次加载都覆盖(含 undefined),避免跨次生成或测试之间互相污染。
|
|
36
|
+
(0, config_1.setGlobalConfig)({ format: validatedConfig.format });
|
|
33
37
|
logger_1.logger.debug('Configuration loaded successfully', this.config);
|
|
34
38
|
}
|
|
35
39
|
/**
|
|
@@ -74,6 +78,9 @@ class ConfigManager {
|
|
|
74
78
|
result.generator = userConfig.generator.map(config => ({
|
|
75
79
|
...this.defaultGeneratorConfig,
|
|
76
80
|
...config,
|
|
81
|
+
// Shallow-spreading would drop the per-field defaults when the user only
|
|
82
|
+
// sets part of `performance`, so merge that object one level deep.
|
|
83
|
+
performance: { ...this.defaultGeneratorConfig.performance, ...config.performance },
|
|
77
84
|
}));
|
|
78
85
|
}
|
|
79
86
|
return result;
|