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.
Files changed (52) hide show
  1. package/dist/bin/actions.js +162 -5
  2. package/dist/bin/cli.js +7 -1
  3. package/dist/bin/renderer.js +2 -8
  4. package/dist/checkUpdates.js +98 -0
  5. package/dist/config.js +7 -0
  6. package/dist/constant.js +1 -2
  7. package/dist/core/WorkerPool.js +14 -0
  8. package/dist/core/loader/callingCodeLoader/helper.js +2 -3
  9. package/dist/core/loader/callingCodeLoader/index.js +1 -1
  10. package/dist/core/parser/openApiParser/helper.js +32 -19
  11. package/dist/core/parser/templateParser/index.js +25 -12
  12. package/dist/core/workerPool/index.js +2 -1
  13. package/dist/functions/changeReport.js +230 -0
  14. package/dist/functions/diffApis.js +82 -0
  15. package/dist/functions/diffDocument.js +542 -0
  16. package/dist/functions/sourceSnapshot.js +107 -0
  17. package/dist/functions/wormaJson.js +306 -66
  18. package/dist/generate.js +24 -2
  19. package/dist/helper/config/ConfigHelper.js +1 -2
  20. package/dist/helper/config/ConfigManager.js +7 -0
  21. package/dist/helper/config/GeneratorHelper.js +74 -21
  22. package/dist/helper/config/zType.js +24 -1
  23. package/dist/helper/template/index.js +60 -4
  24. package/dist/index.js +17 -1
  25. package/dist/plugins/index.js +2 -2
  26. package/dist/plugins/presets/aiDoc.js +4 -0
  27. package/dist/plugins/presets/payloadModifier/dsl.js +147 -0
  28. package/dist/plugins/presets/payloadModifier/index.js +122 -135
  29. package/dist/plugins/presets/payloadModifier/patch.js +171 -0
  30. package/dist/plugins/presets/payloadModifier/scope.js +109 -0
  31. package/dist/plugins/presets/platform/index.js +1 -3
  32. package/dist/plugins/presets/postman.js +105 -0
  33. package/dist/template/presets/ai-doc/SKILL.md.handlebars +1 -1
  34. package/dist/template/presets/alova/common/services/{tag}.d.cts.handlebars +1 -1
  35. package/dist/template/presets/alova/module/services/{tag}.d.ts.handlebars +1 -1
  36. package/dist/template/presets/alova/partials/dts-fn-declare.handlebars +1 -1
  37. package/dist/template/presets/alova/partials/dts-types.handlebars +12 -0
  38. package/dist/template/presets/alova/typescript/services/{tag}.ts.handlebars +11 -6
  39. package/dist/template/presets/axios/partials/dts-types.handlebars +9 -5
  40. package/dist/template/presets/axios/typescript/services/{tag}.ts.handlebars +9 -5
  41. package/dist/template/presets/fetch/partials/dts-types.handlebars +8 -5
  42. package/dist/template/presets/fetch/typescript/services/{tag}.ts.handlebars +9 -5
  43. package/dist/template/presets/ky/partials/dts-types.handlebars +8 -5
  44. package/dist/template/presets/ky/typescript/services/{tag}.ts.handlebars +9 -5
  45. package/dist/utils/format.js +62 -15
  46. package/dist/utils/template.js +1 -1
  47. package/package.json +3 -2
  48. package/typings/index.d.ts +266 -13
  49. package/typings/plugins.d.ts +171 -80
  50. package/dist/plugins/presets/payloadModifier/hepler.js +0 -289
  51. package/dist/plugins/presets/platform/fastapi.js +0 -22
  52. package/dist/template/presets/alova/partials/dts-extra-config.handlebars +0 -8
@@ -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, force, debug, }) {
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, force);
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
- async function generateForProject(entry, force) {
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 */
@@ -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 Try `worma gen -f` to force regenerate.\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 Try `worma gen -f` to force regenerate.\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";
@@ -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
- return (0, utils_1.format)((0, exports.generateDefaultValues)(sourceCode), {
181
- parser: 'json',
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 fetchRawText(urls, options) {
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
- // Quick parse + validity check (full parse + Swagger2→OpenAPI3 conversion
101
- // happens later in parseSpec, after beforeSpecParse may rewrite the text).
102
- let probe;
103
- try {
104
- probe = JSON.parse(text);
105
- }
106
- catch (jsonError) {
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 fetchRawText(urls, { projectPath, fetchOptions });
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
- /** Automatically compute a reasonable concurrency limit based on CPU core count */
64
- function autoConcurrency() {
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 concurrency = autoConcurrency();
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.pickPoolSize)(apiMethodArray.length);
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
- templateData.components = [...new Set(this.schemasMap.values())];
177
- templateData.componentNames = [...this.schemasMap.keys()];
178
- // sort by name lexicographically to ensure deterministic output order (avoid Map insertion-order drift under concurrency)
179
- const sorted = [...this.schemasMap.entries()].sort(([a], [b]) => a.localeCompare(b));
180
- templateData.componentNames = sorted.map(([k]) => k);
181
- templateData.components = sorted.map(([, v]) => v);
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; } });