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
package/dist/bin/actions.js
CHANGED
|
@@ -38,6 +38,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.actionInit = actionInit;
|
|
40
40
|
exports.actionGen = actionGen;
|
|
41
|
+
exports.actionDiff = actionDiff;
|
|
42
|
+
exports.printRecordedChanges = printRecordedChanges;
|
|
41
43
|
const node_path_1 = __importDefault(require("node:path"));
|
|
42
44
|
const readline = __importStar(require("node:readline/promises"));
|
|
43
45
|
const config_1 = require("../config");
|
|
@@ -51,6 +53,8 @@ const renderer_1 = require("./renderer");
|
|
|
51
53
|
const theme_1 = require("./theme");
|
|
52
54
|
// eslint-disable-next-line ts/no-require-imports, perfectionist/sort-imports
|
|
53
55
|
const pkg = require('../../package.json');
|
|
56
|
+
// eslint-disable-next-line ts/no-require-imports, perfectionist/sort-imports
|
|
57
|
+
const Table = require('cli-table3');
|
|
54
58
|
async function actionInit({ type, template, project }) {
|
|
55
59
|
const renderer = new renderer_1.InitRenderer(pkg.version);
|
|
56
60
|
// Resolve project path
|
|
@@ -131,7 +135,7 @@ async function promptTemplate() {
|
|
|
131
135
|
// Default to first choice (alova) on invalid input
|
|
132
136
|
return renderer_1.INIT_TEMPLATE_CHOICES[0].value;
|
|
133
137
|
}
|
|
134
|
-
async function actionGen({ project,
|
|
138
|
+
async function actionGen({ project, debug, }) {
|
|
135
139
|
if (debug) {
|
|
136
140
|
helper_1.logger.configure({ level: 'debug' });
|
|
137
141
|
}
|
|
@@ -169,7 +173,7 @@ async function actionGen({ project, force, debug, }) {
|
|
|
169
173
|
if (projects.length === 1) {
|
|
170
174
|
// Single project: existing MultiGeneratorRenderer path — 100% unchanged behaviour
|
|
171
175
|
const proj = projects[0];
|
|
172
|
-
await generateForProject(proj
|
|
176
|
+
await generateForProject(proj);
|
|
173
177
|
}
|
|
174
178
|
else {
|
|
175
179
|
// Multi-project: new MultiProjectRenderer + sequential execution
|
|
@@ -182,15 +186,18 @@ async function actionGen({ project, force, debug, }) {
|
|
|
182
186
|
}));
|
|
183
187
|
const renderer = new renderer_1.MultiProjectRenderer(projectInfos, pkg.version);
|
|
184
188
|
const allResults = [];
|
|
189
|
+
const recorded = [];
|
|
185
190
|
for (let pi = 0; pi < projects.length; pi++) {
|
|
186
191
|
const proj = projects[pi];
|
|
187
192
|
try {
|
|
188
193
|
const results = await (0, generate_1.default)(proj.config, {
|
|
189
|
-
force,
|
|
190
194
|
projectPath: proj.dir,
|
|
191
195
|
onProgress(event) {
|
|
192
196
|
renderer.onProjectEvent(pi, event);
|
|
193
197
|
},
|
|
198
|
+
onChangeRecorded(change) {
|
|
199
|
+
recorded.push(change);
|
|
200
|
+
},
|
|
194
201
|
});
|
|
195
202
|
allResults.push(results);
|
|
196
203
|
}
|
|
@@ -205,9 +212,155 @@ async function actionGen({ project, force, debug, }) {
|
|
|
205
212
|
}
|
|
206
213
|
}
|
|
207
214
|
renderer.finalize(allResults);
|
|
215
|
+
printRecordedChanges(recorded);
|
|
208
216
|
}
|
|
209
217
|
}
|
|
210
|
-
|
|
218
|
+
// ──────────────────────────────────────────────────────────────
|
|
219
|
+
// `worma diff` — browse recorded API changes (requirement B)
|
|
220
|
+
// ──────────────────────────────────────────────────────────────
|
|
221
|
+
/**
|
|
222
|
+
* Format a timestamp in the **local** time zone (`YYYY-MM-DD HH:mm:ss`).
|
|
223
|
+
*
|
|
224
|
+
* `toISOString()` renders UTC, which made every record look ~8h off for anyone
|
|
225
|
+
* outside UTC and is the reason a record created at noon showed up as 04:xx.
|
|
226
|
+
*/
|
|
227
|
+
function formatTime(ts) {
|
|
228
|
+
const date = new Date(ts);
|
|
229
|
+
if (Number.isNaN(date.getTime()))
|
|
230
|
+
return '-';
|
|
231
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
232
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Build a bordered `cli-table3` instance with our theme colours applied to the
|
|
236
|
+
* header (the library's own head/border colours are disabled so our `theme`
|
|
237
|
+
* palette wins). ANSI escape codes are measured as zero-width by cli-table3's
|
|
238
|
+
* internal `string-width` layout, so coloured cells stay aligned.
|
|
239
|
+
*/
|
|
240
|
+
function createTable(head) {
|
|
241
|
+
return new Table({
|
|
242
|
+
head,
|
|
243
|
+
style: { head: [], border: [] },
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/** `worma diff` / `worma diff <id>` / `worma diff latest` */
|
|
247
|
+
async function actionDiff(id, { list, project }) {
|
|
248
|
+
const projectPath = project
|
|
249
|
+
? (node_path_1.default.isAbsolute(project) ? project : node_path_1.default.resolve(process.cwd(), project))
|
|
250
|
+
: process.cwd();
|
|
251
|
+
// Mirror `actionGen`: always resolve the cache from CWD so monorepo
|
|
252
|
+
// sub-packages share one unified cache root.
|
|
253
|
+
(0, config_1.setGlobalConfig)({ cacheRoot: process.cwd() });
|
|
254
|
+
const { countChanges, getChange, listChanges } = await Promise.resolve().then(() => __importStar(require('../functions/changeReport')));
|
|
255
|
+
if (!id || list) {
|
|
256
|
+
const summaries = await listChanges(projectPath);
|
|
257
|
+
if (summaries.length === 0) {
|
|
258
|
+
console.log(`\n ${theme_1.theme.dim('No change records found.')}`);
|
|
259
|
+
console.log(` ${theme_1.theme.dim('Run `worma gen` after changing your spec to create one.')}\n`);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const table = createTable([
|
|
263
|
+
theme_1.theme.label('ID'),
|
|
264
|
+
theme_1.theme.label('CREATED'),
|
|
265
|
+
theme_1.theme.label('OUTPUTS'),
|
|
266
|
+
theme_1.theme.label('CHANGES'),
|
|
267
|
+
]);
|
|
268
|
+
summaries.forEach(s => table.push([
|
|
269
|
+
s.id,
|
|
270
|
+
formatTime(s.createdAt),
|
|
271
|
+
s.outputs.join(', ') || '-',
|
|
272
|
+
`+${s.summary.added} / -${s.summary.removed} / ~${s.summary.modified}`,
|
|
273
|
+
]));
|
|
274
|
+
console.log('');
|
|
275
|
+
console.log(table.toString());
|
|
276
|
+
console.log('');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const change = await getChange(projectPath, id);
|
|
280
|
+
if (!change) {
|
|
281
|
+
console.log(`\n ${theme_1.theme.warning('?')} No change record found for "${id}".\n`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
console.log('');
|
|
285
|
+
console.log(` ${theme_1.theme.label(`Change ${change.id}`)} ${theme_1.theme.dim(formatTime(change.createdAt))}`);
|
|
286
|
+
console.log('');
|
|
287
|
+
const totals = countChanges(change.generators);
|
|
288
|
+
for (const gen of change.generators) {
|
|
289
|
+
console.log(` ${theme_1.theme.header(gen.serverName ? `${gen.output} (${gen.serverName})` : gen.output)}`);
|
|
290
|
+
if (gen.changes.length === 0) {
|
|
291
|
+
console.log(` ${theme_1.theme.dim('no changes')}`);
|
|
292
|
+
console.log('');
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
// Every change is one row: component changes carry their affected
|
|
296
|
+
// operations in `affects`, listed under the change itself (one per line).
|
|
297
|
+
const table = createTable([
|
|
298
|
+
'',
|
|
299
|
+
theme_1.theme.label('TYPE'),
|
|
300
|
+
theme_1.theme.label('TARGET'),
|
|
301
|
+
theme_1.theme.label('ITEM'),
|
|
302
|
+
theme_1.theme.label('CHANGE'),
|
|
303
|
+
theme_1.theme.label('LEVEL'),
|
|
304
|
+
]);
|
|
305
|
+
for (const row of gen.changes) {
|
|
306
|
+
table.push([
|
|
307
|
+
opText(row.op),
|
|
308
|
+
theme_1.theme.dim(row.kind),
|
|
309
|
+
row.target,
|
|
310
|
+
row.item ?? '',
|
|
311
|
+
changeCell(row),
|
|
312
|
+
levelText(row.level),
|
|
313
|
+
]);
|
|
314
|
+
}
|
|
315
|
+
console.log(table.toString());
|
|
316
|
+
console.log('');
|
|
317
|
+
}
|
|
318
|
+
console.log(` ${theme_1.theme.label('Total:')} ${theme_1.theme.success(`+${totals.added} added`)}, ${theme_1.theme.error(`-${totals.removed} removed`)}, ${theme_1.theme.warning(`~${totals.modified} modified`)}`);
|
|
319
|
+
console.log('');
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* `CHANGE` cell of one row: the change itself plus, for a component change, the
|
|
323
|
+
* operations it affects — one per line, so nothing is folded away.
|
|
324
|
+
*/
|
|
325
|
+
function changeCell(row) {
|
|
326
|
+
return [row.detail, ...(row.affects ?? [])].filter(Boolean).join('\n');
|
|
327
|
+
}
|
|
328
|
+
/** `+` / `-` / `~` cell with the matching palette colour. */
|
|
329
|
+
function opText(op) {
|
|
330
|
+
if (op === '+')
|
|
331
|
+
return theme_1.theme.success(op);
|
|
332
|
+
if (op === '-')
|
|
333
|
+
return theme_1.theme.error(op);
|
|
334
|
+
return theme_1.theme.warning(op);
|
|
335
|
+
}
|
|
336
|
+
/** Severity cell with the matching palette colour. */
|
|
337
|
+
function levelText(level) {
|
|
338
|
+
if (level === 'breaking')
|
|
339
|
+
return theme_1.theme.error(level);
|
|
340
|
+
if (level === 'additive')
|
|
341
|
+
return theme_1.theme.success(level);
|
|
342
|
+
return theme_1.theme.dim(level);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Close a `worma gen` run with the change-record pointer: the id(s) written by
|
|
346
|
+
* this run plus the command showing their details.
|
|
347
|
+
*
|
|
348
|
+
* A run that recorded nothing (the source document did not change) prints no
|
|
349
|
+
* closing line at all.
|
|
350
|
+
*/
|
|
351
|
+
function printRecordedChanges(recorded) {
|
|
352
|
+
if (recorded.length === 0)
|
|
353
|
+
return;
|
|
354
|
+
const totals = recorded.reduce((acc, change) => ({
|
|
355
|
+
added: acc.added + change.added,
|
|
356
|
+
removed: acc.removed + change.removed,
|
|
357
|
+
modified: acc.modified + change.modified,
|
|
358
|
+
}), { added: 0, removed: 0, modified: 0 });
|
|
359
|
+
const ids = recorded.map(change => change.id).join(', ');
|
|
360
|
+
console.log(`\n ${theme_1.theme.success('✔')} Changes recorded: ${theme_1.theme.label(ids)} ${theme_1.theme.dim(`(+${totals.added}/-${totals.removed}/~${totals.modified})`)}`);
|
|
361
|
+
console.log(` ${theme_1.theme.dim('Run `worma diff latest` to view the details.')}\n`);
|
|
362
|
+
}
|
|
363
|
+
async function generateForProject(entry) {
|
|
211
364
|
const { dir, configPath, config, generators } = entry;
|
|
212
365
|
// Initialize renderer — prints pre-flight (Phase 1), starts live-update (Phase 2)
|
|
213
366
|
const renderer = new renderer_1.MultiGeneratorRenderer(generators, pkg.version, configPath);
|
|
@@ -216,8 +369,8 @@ async function generateForProject(entry, force) {
|
|
|
216
369
|
renderer.setActive(i);
|
|
217
370
|
}
|
|
218
371
|
// Unified entry — generate() creates per-gen trackers internally
|
|
372
|
+
const recorded = [];
|
|
219
373
|
const results = await (0, generate_1.default)(config, {
|
|
220
|
-
force,
|
|
221
374
|
projectPath: dir,
|
|
222
375
|
onProgress(event) {
|
|
223
376
|
switch (event.phase) {
|
|
@@ -238,8 +391,12 @@ async function generateForProject(entry, force) {
|
|
|
238
391
|
break;
|
|
239
392
|
}
|
|
240
393
|
},
|
|
394
|
+
onChangeRecorded(change) {
|
|
395
|
+
recorded.push(change);
|
|
396
|
+
},
|
|
241
397
|
});
|
|
242
398
|
// Finalize — Phase 3: stop live-update, show concise ✔/✖ summary
|
|
243
399
|
const failedCount = results.filter(r => !r).length;
|
|
244
400
|
renderer.finalize(failedCount);
|
|
401
|
+
printRecordedChanges(recorded);
|
|
245
402
|
}
|
package/dist/bin/cli.js
CHANGED
|
@@ -19,9 +19,15 @@ program
|
|
|
19
19
|
program
|
|
20
20
|
.command('gen')
|
|
21
21
|
.description('generate API from OpenAPI specs')
|
|
22
|
-
.option('-f, --force', 'force generate api')
|
|
23
22
|
.option('-d, --debug', 'enable debug logging')
|
|
24
23
|
.option('-p, --project <path>', 'project directory (single project mode)')
|
|
25
24
|
.action(actions_1.actionGen);
|
|
25
|
+
program
|
|
26
|
+
.command('diff')
|
|
27
|
+
.description('browse recorded API changes')
|
|
28
|
+
.argument('[id]', 'change id (e.g. 0007) or `latest`; omit to list all')
|
|
29
|
+
.option('-l, --list', 'list all recorded changes')
|
|
30
|
+
.option('-p, --project <path>', 'project directory')
|
|
31
|
+
.action(actions_1.actionDiff);
|
|
26
32
|
program.parse(process.argv);
|
|
27
33
|
/* c8 ignore stop */
|
package/dist/bin/renderer.js
CHANGED
|
@@ -161,9 +161,6 @@ class MultiGeneratorRenderer {
|
|
|
161
161
|
else if (st.status === 'failed') {
|
|
162
162
|
row += `\n ${theme_1.theme.error(`✖ ${st.error || 'failed'}`)}`;
|
|
163
163
|
}
|
|
164
|
-
else if (st.status === 'skipped') {
|
|
165
|
-
row += ` ${theme_1.theme.dim('up-to-date')}`;
|
|
166
|
-
}
|
|
167
164
|
// Add blank line between rows for readability
|
|
168
165
|
lines.push(row);
|
|
169
166
|
lines.push('');
|
|
@@ -201,7 +198,7 @@ class MultiGeneratorRenderer {
|
|
|
201
198
|
console.log(theme_1.theme.success('\n✔ Generated successfully!\n'));
|
|
202
199
|
}
|
|
203
200
|
else {
|
|
204
|
-
console.log(theme_1.theme.dim('\n
|
|
201
|
+
console.log(theme_1.theme.dim('\n Generation finished with failures. See the log above for details.\n'));
|
|
205
202
|
}
|
|
206
203
|
}
|
|
207
204
|
}
|
|
@@ -400,9 +397,6 @@ class MultiProjectRenderer {
|
|
|
400
397
|
else if (st.status === 'failed') {
|
|
401
398
|
row += `\n ${theme_1.theme.error(`✖ ${st.error || 'failed'}`)}`;
|
|
402
399
|
}
|
|
403
|
-
else if (st.status === 'skipped') {
|
|
404
|
-
row += ` ${theme_1.theme.dim('up-to-date')}`;
|
|
405
|
-
}
|
|
406
400
|
lines.push(row);
|
|
407
401
|
lines.push('');
|
|
408
402
|
}
|
|
@@ -471,7 +465,7 @@ class MultiProjectRenderer {
|
|
|
471
465
|
console.log(theme_1.theme.success('\n✔ Generated successfully!\n'));
|
|
472
466
|
}
|
|
473
467
|
else {
|
|
474
|
-
console.log(theme_1.theme.dim('\n
|
|
468
|
+
console.log(theme_1.theme.dim('\n Generation finished with failures. See the log above for details.\n'));
|
|
475
469
|
}
|
|
476
470
|
}
|
|
477
471
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkUpdates = checkUpdates;
|
|
4
|
+
const helper_1 = require("./core/parser/openApiParser/helper");
|
|
5
|
+
const prepareConfig_1 = require("./functions/prepareConfig");
|
|
6
|
+
const wormaJson_1 = require("./functions/wormaJson");
|
|
7
|
+
/**
|
|
8
|
+
* Detect whether the configured OpenAPI sources changed since the last
|
|
9
|
+
* recorded baseline.
|
|
10
|
+
*
|
|
11
|
+
* This is a **source-level, side-effect free** check:
|
|
12
|
+
*
|
|
13
|
+
* - it only hashes the raw spec text — no parsing, no plugin hooks;
|
|
14
|
+
* - it only reads/writes the `source` sub-field of `index.json` entries, never
|
|
15
|
+
* the generation-side `hash` / `tags`;
|
|
16
|
+
* - when no baseline exists yet (`new`) it writes the baseline silently and
|
|
17
|
+
* does *not* report a change (first run must not nag the user).
|
|
18
|
+
*
|
|
19
|
+
* Nothing on the user's disk is rewritten — callers decide what to do with the
|
|
20
|
+
* result (the VS Code extension asks for confirmation before generating).
|
|
21
|
+
*/
|
|
22
|
+
async function checkUpdates(config, options) {
|
|
23
|
+
const projectPath = options?.projectPath ?? process.cwd();
|
|
24
|
+
const generators = config?.generator ?? [];
|
|
25
|
+
const cacheIndex = await (0, wormaJson_1.readCacheIndex)(projectPath);
|
|
26
|
+
const hasGenerationBaseline = !!cacheIndex?.entries?.some(entry => !!entry.tags && Object.keys(entry.tags).length > 0);
|
|
27
|
+
// Collect `new` baselines and persist them in a single pass after the
|
|
28
|
+
// concurrent checks finish. Writing inside the `Promise.all` below would let
|
|
29
|
+
// the generators race on the same `index.json` (read → modify → write), so
|
|
30
|
+
// only the last writer's baseline would survive.
|
|
31
|
+
const baselineWrites = [];
|
|
32
|
+
const updates = await Promise.all(generators.map(async (gen, index) => {
|
|
33
|
+
// Run plugin `config` hooks (the same step the generator pipeline performs
|
|
34
|
+
// via ConfigManager/prepareConfig) so that `gen.input` is populated. Without
|
|
35
|
+
// this, configs that supply the OpenAPI source through a platform plugin
|
|
36
|
+
// (e.g. `swagger('petstore.json')`) leave `gen.input` empty, and the source
|
|
37
|
+
// fetch resolves to the project directory itself → "Cannot find module
|
|
38
|
+
// '<projectPath>'".
|
|
39
|
+
const prepared = await (0, prepareConfig_1.prepareConfig)(gen, projectPath);
|
|
40
|
+
const output = prepared.output ?? '';
|
|
41
|
+
const info = { index, output, serverName: prepared.serverName, status: 'unchanged' };
|
|
42
|
+
let text;
|
|
43
|
+
let resolvedInput;
|
|
44
|
+
try {
|
|
45
|
+
const urls = Array.isArray(prepared.input) ? prepared.input : [prepared.input ?? ''];
|
|
46
|
+
const raw = await (0, helper_1.getRawSpecText)(urls, {
|
|
47
|
+
projectPath,
|
|
48
|
+
fetchOptions: gen.fetchOptions,
|
|
49
|
+
});
|
|
50
|
+
text = raw.text;
|
|
51
|
+
resolvedInput = raw.url;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
info.status = 'error';
|
|
55
|
+
info.error = error?.message ?? String(error);
|
|
56
|
+
return info;
|
|
57
|
+
}
|
|
58
|
+
const hash = (0, wormaJson_1.computeSpecHash)(text);
|
|
59
|
+
info.resolvedInput = resolvedInput;
|
|
60
|
+
info.hash = hash;
|
|
61
|
+
let previous;
|
|
62
|
+
try {
|
|
63
|
+
previous = (await (0, wormaJson_1.getCacheEntry)(projectPath, output))?.source?.rawHash;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
previous = undefined;
|
|
67
|
+
}
|
|
68
|
+
if (previous === undefined) {
|
|
69
|
+
// First sight of this source — record the baseline, stay silent.
|
|
70
|
+
info.status = 'new';
|
|
71
|
+
baselineWrites.push({
|
|
72
|
+
outputPath: output,
|
|
73
|
+
serverName: prepared.serverName ?? '',
|
|
74
|
+
source: {
|
|
75
|
+
resolvedInput,
|
|
76
|
+
rawHash: hash,
|
|
77
|
+
updatedAt: Date.now(),
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else if (previous !== hash) {
|
|
82
|
+
info.status = 'changed';
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
info.status = 'unchanged';
|
|
86
|
+
}
|
|
87
|
+
return info;
|
|
88
|
+
}));
|
|
89
|
+
if (baselineWrites.length > 0)
|
|
90
|
+
await (0, wormaJson_1.updateSourceBaselines)(projectPath, baselineWrites);
|
|
91
|
+
return {
|
|
92
|
+
projectPath,
|
|
93
|
+
updates,
|
|
94
|
+
hasChanges: updates.some(u => u.status === 'changed'),
|
|
95
|
+
hasGenerationBaseline,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
exports.default = checkUpdates;
|
package/dist/config.js
CHANGED
|
@@ -6,6 +6,13 @@ const DEFAULT_CONFIG = {
|
|
|
6
6
|
cacheDir: '.worma-cache',
|
|
7
7
|
/** Overrides cacheDir's parent directory for monorepo unified cache. */
|
|
8
8
|
cacheRoot: undefined,
|
|
9
|
+
/**
|
|
10
|
+
* Maximum number of `changes/<NNNN>.json` records to keep.
|
|
11
|
+
* `0` (or any non-positive value) keeps every record.
|
|
12
|
+
*/
|
|
13
|
+
changeHistoryLimit: 100,
|
|
14
|
+
/** 用户自定义的产物格式化配置,未设置时使用内置默认值 */
|
|
15
|
+
format: undefined,
|
|
9
16
|
Error,
|
|
10
17
|
templateData: new Map(),
|
|
11
18
|
};
|
package/dist/constant.js
CHANGED
|
@@ -26,7 +26,6 @@ var PlatformTypeEnum;
|
|
|
26
26
|
(function (PlatformTypeEnum) {
|
|
27
27
|
PlatformTypeEnum["SWAGGER"] = "swagger";
|
|
28
28
|
PlatformTypeEnum["KNIFE4J"] = "knife4j";
|
|
29
|
-
PlatformTypeEnum["FASTAPI"] = "fastapi";
|
|
30
29
|
PlatformTypeEnum["YAPI"] = "yapi";
|
|
31
30
|
})(PlatformTypeEnum || (exports.PlatformTypeEnum = PlatformTypeEnum = {}));
|
|
32
31
|
/** Module system type */
|
|
@@ -96,9 +95,9 @@ var PluginName;
|
|
|
96
95
|
PluginName["IMPORT_TYPE"] = "importType";
|
|
97
96
|
PluginName["AI_DOC"] = "aiDoc";
|
|
98
97
|
PluginName["APIFOX"] = "apifox";
|
|
98
|
+
PluginName["POSTMAN"] = "postman";
|
|
99
99
|
PluginName["SWAGGER"] = "swagger";
|
|
100
100
|
PluginName["KNIFE4J"] = "knife4j";
|
|
101
|
-
PluginName["FASTAPI"] = "fastapi";
|
|
102
101
|
PluginName["YAPI"] = "yapi";
|
|
103
102
|
PluginName["TEMPLATE_ALOVA"] = "templateAlova";
|
|
104
103
|
PluginName["TEMPLATE_ALOVA_GLOBALS"] = "templateAlovaGlobals";
|
package/dist/core/WorkerPool.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WorkerPool = void 0;
|
|
4
4
|
exports.pickPoolSize = pickPoolSize;
|
|
5
|
+
exports.resolvePoolSize = resolvePoolSize;
|
|
5
6
|
/**
|
|
6
7
|
* 9.3.1: Generic WorkerPool — pure thread pool for task/result patterns.
|
|
7
8
|
* Handles lifecycle, task distribution, and result collection.
|
|
@@ -21,6 +22,19 @@ function pickPoolSize(apiCount) {
|
|
|
21
22
|
return Math.min(Math.ceil(cpu * 0.75), cpu);
|
|
22
23
|
return Math.max(2, cpu - 1);
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the worker pool size from the user-facing `performance.workerPool` strategy:
|
|
27
|
+
* - `false` disables workers entirely (conversion stays on the main thread)
|
|
28
|
+
* - a number pins the pool size (clamped to >= 0)
|
|
29
|
+
* - `'auto'` / omitted uses the adaptive {@link pickPoolSize} heuristic
|
|
30
|
+
*/
|
|
31
|
+
function resolvePoolSize(apiCount, strategy) {
|
|
32
|
+
if (strategy === false)
|
|
33
|
+
return 0;
|
|
34
|
+
if (typeof strategy === 'number')
|
|
35
|
+
return Math.max(0, Math.floor(strategy));
|
|
36
|
+
return pickPoolSize(apiCount);
|
|
37
|
+
}
|
|
24
38
|
class WorkerPool {
|
|
25
39
|
options;
|
|
26
40
|
workers = [];
|
|
@@ -177,7 +177,6 @@ exports.generateDefaultValues = generateDefaultValues;
|
|
|
177
177
|
* @returns Object containing type and interface default values
|
|
178
178
|
*/
|
|
179
179
|
function generateDefaultValuesFormat(sourceCode) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
});
|
|
180
|
+
// 产物是对象字面量,用 .json 让 oxfmt 按 JSON 解析(oxfmt 无 prettier 的 parser 选项,按扩展名推断)
|
|
181
|
+
return (0, utils_1.format)('default-values.json', (0, exports.generateDefaultValues)(sourceCode));
|
|
183
182
|
}
|
|
@@ -22,7 +22,7 @@ class CallingCodeLoader {
|
|
|
22
22
|
if (api.requestBodyComment) {
|
|
23
23
|
configStrArr.push(`data: ${await this.transform(api.requestBodyComment.replace(/\*/g, ''))}`);
|
|
24
24
|
}
|
|
25
|
-
return (0, utils_1.format)(`${api.name}({${configStrArr.join(',\n')}})`, {
|
|
25
|
+
return (0, utils_1.format)('calling-code.ts', `${api.name}({${configStrArr.join(',\n')}})`, {
|
|
26
26
|
printWidth: 40, // Shorten print width to force line breaks
|
|
27
27
|
tabWidth: 2,
|
|
28
28
|
semi: false, // Remove the trailing semicolon
|
|
@@ -3,6 +3,7 @@ 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.getRawSpecText = getRawSpecText;
|
|
6
7
|
exports.getOpenApiDataWithUrl = getOpenApiDataWithUrl;
|
|
7
8
|
exports.getOpenApiData = getOpenApiData;
|
|
8
9
|
const node_fs_1 = require("node:fs");
|
|
@@ -81,8 +82,11 @@ const isRemoteUrl = (u) => /^https?:\/\//.test(u);
|
|
|
81
82
|
/**
|
|
82
83
|
* Fetch the raw spec text (JSON/YAML) from the first URL that succeeds.
|
|
83
84
|
* Returns the raw text together with the resolved URL; throws if all URLs fail.
|
|
85
|
+
*
|
|
86
|
+
* Exported as `getRawSpecText` so that update detection (requirement A) can
|
|
87
|
+
* hash the source without parsing it or running any plugin hook.
|
|
84
88
|
*/
|
|
85
|
-
async function
|
|
89
|
+
async function getRawSpecText(urls, options) {
|
|
86
90
|
if (urls.length === 0) {
|
|
87
91
|
throw helper_1.logger.throwError('No URLs provided to fetch OpenAPI document');
|
|
88
92
|
}
|
|
@@ -97,22 +101,13 @@ async function fetchRawText(urls, options) {
|
|
|
97
101
|
const text = isRemoteUrl(u)
|
|
98
102
|
? await fetchRawRemoteFile(u, fetchOptions)
|
|
99
103
|
: await fetchRawLocalFile(u, projectPath);
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
probe = js_yaml_1.default.load(text);
|
|
109
|
-
}
|
|
110
|
-
catch (yamlError) {
|
|
111
|
-
throw new Error(`${u}: ${(jsonError instanceof Error ? jsonError.message : String(jsonError))} (YAML: ${yamlError instanceof Error ? yamlError.message : String(yamlError)})`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
if (!isValidOpenApiData(probe)) {
|
|
115
|
-
throw new Error(`${u} did not yield a valid OpenAPI/Swagger document`);
|
|
104
|
+
// Nothing is parsed here on purpose: the OpenAPI validity check runs in
|
|
105
|
+
// parseSpec, i.e. after `beforeSpecParse` may rewrite the text. Only
|
|
106
|
+
// payloads that can never be a spec (HTML error pages, e.g. a Swagger UI
|
|
107
|
+
// page hit instead of its spec document) are rejected so that Promise.any
|
|
108
|
+
// falls through to the next URL.
|
|
109
|
+
if (!couldBeSpecText(text)) {
|
|
110
|
+
throw new Error(`${u} did not yield a usable OpenAPI/Swagger payload`);
|
|
116
111
|
}
|
|
117
112
|
return { text, url: u };
|
|
118
113
|
})();
|
|
@@ -127,6 +122,24 @@ async function fetchRawText(urls, options) {
|
|
|
127
122
|
throw helper_1.logger.throwError(`Unable to retrieve valid OpenAPI document from any URL:\n${errors.join('\n')}`);
|
|
128
123
|
}
|
|
129
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Keywords every OpenAPI/Swagger document carries, either as a JSON key
|
|
127
|
+
* (`"openapi": "3.0.3"`) or as a YAML key (`openapi: 3.0.3`). Escaped forms are
|
|
128
|
+
* matched as well, so an envelope such as `{ "output": "{\"openapi\":...}" }`
|
|
129
|
+
* still passes and can be unwrapped by `beforeSpecParse`. The optional leading
|
|
130
|
+
* `\\` accounts for the backslash JSON adds when the spec is embedded as a
|
|
131
|
+
* string (e.g. the Postman transformation response).
|
|
132
|
+
*/
|
|
133
|
+
const SPEC_KEYWORD_RE = /\\?["']?(?:openapi|swagger|paths)\\?["']?\s*:/;
|
|
134
|
+
/**
|
|
135
|
+
* Cheap, parse-free guard for a fetched candidate: the text must mention a spec
|
|
136
|
+
* keyword, so HTML pages and unrelated JSON payloads are rejected early and
|
|
137
|
+
* Promise.any falls through to the next URL. The authoritative validity check
|
|
138
|
+
* still happens in `parseSpec`, i.e. after `beforeSpecParse` may rewrite the text.
|
|
139
|
+
*/
|
|
140
|
+
function couldBeSpecText(text) {
|
|
141
|
+
return SPEC_KEYWORD_RE.test(text);
|
|
142
|
+
}
|
|
130
143
|
// Validate OpenAPI data
|
|
131
144
|
function isValidOpenApiData(data) {
|
|
132
145
|
if (!data || typeof data !== 'object') {
|
|
@@ -184,7 +197,7 @@ async function getOpenApiDataWithUrl(url, options) {
|
|
|
184
197
|
const { projectPath, fetchOptions, beforeSpecParse } = options ?? {};
|
|
185
198
|
// Normalize to array — single string or array both handled uniformly
|
|
186
199
|
const urls = Array.isArray(url) ? url : [url];
|
|
187
|
-
const { text, url: resolvedUrl } = await
|
|
200
|
+
const { text, url: resolvedUrl } = await getRawSpecText(urls, { projectPath, fetchOptions });
|
|
188
201
|
// Allow the caller (e.g. a `beforeSpecParse` plugin hook) to transform the
|
|
189
202
|
// raw spec text before it is parsed into an OpenAPIDocument.
|
|
190
203
|
const finalText = (beforeSpecParse ? await beforeSpecParse(text) : text) ?? text;
|
|
@@ -196,7 +209,7 @@ async function getOpenApiDataWithUrl(url, options) {
|
|
|
196
209
|
fetchOptions,
|
|
197
210
|
});
|
|
198
211
|
}
|
|
199
|
-
return { data: result, resolvedUrl };
|
|
212
|
+
return { data: result, resolvedUrl, rawText: text };
|
|
200
213
|
}
|
|
201
214
|
/**
|
|
202
215
|
* Parse OpenAPI document from config input.
|
|
@@ -60,8 +60,14 @@ async function pMap(items, fn, concurrency) {
|
|
|
60
60
|
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
|
|
61
61
|
return results;
|
|
62
62
|
}
|
|
63
|
-
/**
|
|
64
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the transform-phase concurrency limit.
|
|
65
|
+
* An explicit positive `performance.transformConcurrency` wins; otherwise it is
|
|
66
|
+
* computed from the CPU core count.
|
|
67
|
+
*/
|
|
68
|
+
function autoConcurrency(configured) {
|
|
69
|
+
if (typeof configured === 'number' && configured > 0)
|
|
70
|
+
return Math.max(1, Math.floor(configured));
|
|
65
71
|
const cpuCount = Math.max(1, (0, node_os_1.cpus)().length);
|
|
66
72
|
return Math.min(64, Math.max(8, cpuCount * 4));
|
|
67
73
|
}
|
|
@@ -129,7 +135,8 @@ class TemplateParser {
|
|
|
129
135
|
return result;
|
|
130
136
|
}
|
|
131
137
|
async parseApiMethods(apiMethods, templateData) {
|
|
132
|
-
const
|
|
138
|
+
const perf = this.options.generatorConfig.performance;
|
|
139
|
+
const concurrency = autoConcurrency(perf?.transformConcurrency);
|
|
133
140
|
const apiMethodArray = (await pMap(apiMethods, apiMethod => this.transformApiMethods(apiMethod), concurrency)).filter(apiMethod => !!apiMethod);
|
|
134
141
|
const refsMap = Object.fromEntries(this.refNameMap);
|
|
135
142
|
const usedRefs = this.openApiHelper
|
|
@@ -137,7 +144,7 @@ class TemplateParser {
|
|
|
137
144
|
.filterUsedReferences(Object.keys(refsMap));
|
|
138
145
|
this.refNameMap = new Map(Object.entries((0, openapi_1.optimizeRefsMap)(refsMap, usedRefs)));
|
|
139
146
|
// M4-C1: Pre-process schemas via worker pool for parallel schema→TS conversion
|
|
140
|
-
const poolSize = (0, WorkerPool_1.
|
|
147
|
+
const poolSize = (0, WorkerPool_1.resolvePoolSize)(apiMethodArray.length, perf?.workerPool);
|
|
141
148
|
if (poolSize > 0) {
|
|
142
149
|
const tasks = this.collectSchemaTasks(apiMethodArray);
|
|
143
150
|
if (tasks.length > 0) {
|
|
@@ -147,10 +154,11 @@ class TemplateParser {
|
|
|
147
154
|
const { existsSync: fex } = await Promise.resolve().then(() => __importStar(require('node:fs')));
|
|
148
155
|
const workerScript = fex(jsWp) ? jsWp : tsWp;
|
|
149
156
|
// Reuse workers only within the same generator output. The workerData document is immutable after spawn,
|
|
150
|
-
// so generators in the same project must not share a pool.
|
|
157
|
+
// so generators in the same project must not share a pool. The pool size is part of the key so a
|
|
158
|
+
// reconfiguration never silently reuses a pool with a different size.
|
|
151
159
|
const outputDir = node_path_1.default.resolve(this.options.projectPath, this.options.generatorConfig.output);
|
|
152
160
|
const pool = poolManager_1.PoolManager.getInstance().get({
|
|
153
|
-
key: `schemaWorker_${outputDir}`,
|
|
161
|
+
key: `schemaWorker_${outputDir}_${poolSize}`,
|
|
154
162
|
workerScript,
|
|
155
163
|
sharedContext: {
|
|
156
164
|
document: this.document,
|
|
@@ -173,12 +181,17 @@ class TemplateParser {
|
|
|
173
181
|
.forEach((api) => {
|
|
174
182
|
this.parseApi(api, templateData);
|
|
175
183
|
});
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
184
|
+
if (perf?.deterministicSort === false) {
|
|
185
|
+
// Opt-out: keep the collection order, which may drift with worker scheduling.
|
|
186
|
+
templateData.components = [...new Set(this.schemasMap.values())];
|
|
187
|
+
templateData.componentNames = [...this.schemasMap.keys()];
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
// sort by name lexicographically to ensure deterministic output order (avoid Map insertion-order drift under concurrency)
|
|
191
|
+
const sorted = [...this.schemasMap.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
192
|
+
templateData.componentNames = sorted.map(([k]) => k);
|
|
193
|
+
templateData.components = sorted.map(([, v]) => v);
|
|
194
|
+
}
|
|
182
195
|
}
|
|
183
196
|
/**
|
|
184
197
|
* M4-C1: Collect all unique schema objects from API methods for batch processing.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WorkerPool = exports.pickPoolSize = void 0;
|
|
3
|
+
exports.WorkerPool = exports.resolvePoolSize = exports.pickPoolSize = void 0;
|
|
4
4
|
var WorkerPool_1 = require("../WorkerPool");
|
|
5
5
|
Object.defineProperty(exports, "pickPoolSize", { enumerable: true, get: function () { return WorkerPool_1.pickPoolSize; } });
|
|
6
|
+
Object.defineProperty(exports, "resolvePoolSize", { enumerable: true, get: function () { return WorkerPool_1.resolvePoolSize; } });
|
|
6
7
|
Object.defineProperty(exports, "WorkerPool", { enumerable: true, get: function () { return WorkerPool_1.WorkerPool; } });
|