wormajs 0.4.0 → 1.0.0-beta.1
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 +182 -5
- package/dist/bin/cli.js +8 -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 +265 -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 +113 -5
- package/dist/index.js +18 -1
- package/dist/plugins/index.js +2 -2
- package/dist/plugins/presets/aiDoc.js +27 -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 +279 -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
|
@@ -10,9 +10,11 @@ const constant_1 = require("../../constant");
|
|
|
10
10
|
const parser_1 = require("../../core/parser");
|
|
11
11
|
const helper_1 = require("../../core/parser/openApiParser/helper");
|
|
12
12
|
const getAutoTemplateType_1 = __importDefault(require("../../functions/getAutoTemplateType"));
|
|
13
|
+
const sourceSnapshot_1 = require("../../functions/sourceSnapshot");
|
|
13
14
|
const wormaJson_1 = require("../../functions/wormaJson");
|
|
14
15
|
const helper_2 = require("../../helper");
|
|
15
16
|
const progress_1 = require("../../helper/progress");
|
|
17
|
+
const format_1 = require("../../utils/format");
|
|
16
18
|
const zType_1 = require("./zType");
|
|
17
19
|
class GeneratorHelper {
|
|
18
20
|
static instance;
|
|
@@ -29,6 +31,15 @@ class GeneratorHelper {
|
|
|
29
31
|
bodyMediaType: 'application/json',
|
|
30
32
|
type: constant_1.ConfigTypeEnum.AUTO,
|
|
31
33
|
defaultRequire: false,
|
|
34
|
+
/**
|
|
35
|
+
* `transformConcurrency` is intentionally omitted: leaving it unset means
|
|
36
|
+
* "auto" (`min(64, max(8, cpus*4))`), which the parser resolves at runtime.
|
|
37
|
+
*/
|
|
38
|
+
performance: {
|
|
39
|
+
workerPool: 'auto',
|
|
40
|
+
writeConcurrency: 32,
|
|
41
|
+
deterministicSort: true,
|
|
42
|
+
},
|
|
32
43
|
});
|
|
33
44
|
static getInstance() {
|
|
34
45
|
if (!GeneratorHelper.instance) {
|
|
@@ -52,8 +63,13 @@ class GeneratorHelper {
|
|
|
52
63
|
* @returns GeneratorHelper instance
|
|
53
64
|
*/
|
|
54
65
|
async load(config) {
|
|
55
|
-
// Merge with default config
|
|
56
|
-
|
|
66
|
+
// Merge with default config. `performance` is an object, so a plain spread
|
|
67
|
+
// would drop the per-field defaults as soon as the user sets only part of it.
|
|
68
|
+
const mergedConfig = {
|
|
69
|
+
...this.defaultConfig,
|
|
70
|
+
...config,
|
|
71
|
+
performance: { ...this.defaultConfig.performance, ...config.performance },
|
|
72
|
+
};
|
|
57
73
|
// Validate configuration
|
|
58
74
|
const validatedConfig = await GeneratorHelper.validateConfig(mergedConfig);
|
|
59
75
|
// Update config
|
|
@@ -137,7 +153,7 @@ class GeneratorHelper {
|
|
|
137
153
|
beforeSpecParse: opts?.beforeSpecParse,
|
|
138
154
|
});
|
|
139
155
|
}
|
|
140
|
-
static async generate(config, { projectPath,
|
|
156
|
+
static async generate(config, { projectPath, tracker }) {
|
|
141
157
|
const reporter = (plugin) => tracker?.reporterFor(plugin.name ?? 'plugin') ?? progress_1.noopReportProgress;
|
|
142
158
|
const pluginDriver = new helper_2.PluginDriver(config.plugins, { reporter });
|
|
143
159
|
const reportCore = (progress, message) => {
|
|
@@ -147,7 +163,6 @@ class GeneratorHelper {
|
|
|
147
163
|
const pluginNames = (config.plugins || []).map(p => p.name).filter(Boolean);
|
|
148
164
|
helper_2.logger.debug('Starting generation process', {
|
|
149
165
|
projectPath,
|
|
150
|
-
force,
|
|
151
166
|
input: config.input,
|
|
152
167
|
output: config.output,
|
|
153
168
|
plugins: pluginCount,
|
|
@@ -173,6 +188,11 @@ class GeneratorHelper {
|
|
|
173
188
|
reportCore(100, 'skipped: no openapi document');
|
|
174
189
|
return { success: false, resolvedInput };
|
|
175
190
|
}
|
|
191
|
+
// Requirement: the change baseline is the **source** document — the one
|
|
192
|
+
// parsed from the `beforeSpecParse` output, taken before the `specParsed`
|
|
193
|
+
// hooks run. `document` is the very object those hooks receive and they may
|
|
194
|
+
// mutate it in place, so the snapshot has to be taken right here.
|
|
195
|
+
const sourceDocumentText = (0, wormaJson_1.stableStringify)(document);
|
|
176
196
|
helper_2.logger.debug('OpenAPI document parsed successfully', {
|
|
177
197
|
resolvedUrl: resolvedInput,
|
|
178
198
|
version: document?.info?.version,
|
|
@@ -247,22 +267,21 @@ class GeneratorHelper {
|
|
|
247
267
|
// P2: Pass tagedApis to avoid re-grouping; apiHashCache avoids re-hashing same Api objects
|
|
248
268
|
const newApis = templateData.allApis || [];
|
|
249
269
|
const newHashInfo = (0, wormaJson_1.computePerTagHashes)(newApis, templateData.tagedApis);
|
|
270
|
+
// `generate()` is "call means generate": there is deliberately NO whole-run
|
|
271
|
+
// skip here any more. Change detection is `checkUpdates()`'s job; rendering
|
|
272
|
+
// stays incremental (only changed tags are re-rendered) as a pure
|
|
273
|
+
// optimization that does not alter the output.
|
|
274
|
+
const oldEntry = await (0, wormaJson_1.getCacheEntry)(projectPath, config.output);
|
|
250
275
|
let changedTags;
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
changedTags = (0, wormaJson_1.diffChangedTags)(oldEntry.tags, newHashInfo.tags);
|
|
261
|
-
helper_2.logger.debug('Incremental update detected', {
|
|
262
|
-
totalTags: Object.keys(newHashInfo.tags).length,
|
|
263
|
-
changedTags: changedTags.size,
|
|
264
|
-
});
|
|
265
|
-
}
|
|
276
|
+
if ((0, wormaJson_1.hasGenerationBaseline)(oldEntry)) {
|
|
277
|
+
changedTags = (0, wormaJson_1.diffChangedTags)(oldEntry.tags, newHashInfo.tags);
|
|
278
|
+
helper_2.logger.debug('Incremental update detected', {
|
|
279
|
+
totalTags: Object.keys(newHashInfo.tags).length,
|
|
280
|
+
changedTags: changedTags.size,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
helper_2.logger.debug('No render baseline — rendering every tag');
|
|
266
285
|
}
|
|
267
286
|
reportCore(70, 'beforeCodeGenerate');
|
|
268
287
|
helper_2.logger.debug('Running beforeCodeGenerate hook');
|
|
@@ -288,7 +307,7 @@ class GeneratorHelper {
|
|
|
288
307
|
// 9.2.1: Unified streaming pipeline — render + beforeFileWrite hooks + write
|
|
289
308
|
const perf = config.performance;
|
|
290
309
|
const writeConcurrency = perf?.writeConcurrency ?? 32;
|
|
291
|
-
const formatFile =
|
|
310
|
+
const formatFile = (0, format_1.isFormatEnabled)();
|
|
292
311
|
helper_2.logger.debug('Starting template generation', {
|
|
293
312
|
writeConcurrency,
|
|
294
313
|
formatFile,
|
|
@@ -340,7 +359,41 @@ class GeneratorHelper {
|
|
|
340
359
|
reportCore(100, `failed: ${codeGenError.message}`);
|
|
341
360
|
throw codeGenError;
|
|
342
361
|
}
|
|
343
|
-
|
|
362
|
+
// Requirement A: refresh the source-level baseline with the very same raw
|
|
363
|
+
// text that was just parsed — zero extra requests. Only the `source`
|
|
364
|
+
// sub-field is touched, so the render baseline (`hash` / `tags`) is kept.
|
|
365
|
+
if (openApiResult.rawText) {
|
|
366
|
+
await (0, wormaJson_1.updateSourceBaseline)(projectPath, config.output, {
|
|
367
|
+
resolvedInput,
|
|
368
|
+
rawHash: (0, wormaJson_1.computeSpecHash)(openApiResult.rawText),
|
|
369
|
+
updatedAt: Date.now(),
|
|
370
|
+
}, config.serverName ?? '');
|
|
371
|
+
}
|
|
372
|
+
// Requirement B: the source-document diff runs only **after** a successful
|
|
373
|
+
// generation, so a failed run never pays for the comparison nor advances the
|
|
374
|
+
// baseline the next run diffs against.
|
|
375
|
+
let change;
|
|
376
|
+
try {
|
|
377
|
+
const sourceChanges = await (0, sourceSnapshot_1.collectSourceChanges)({
|
|
378
|
+
projectRoot: projectPath,
|
|
379
|
+
outputPath: config.output,
|
|
380
|
+
documentText: sourceDocumentText,
|
|
381
|
+
resolvedInput,
|
|
382
|
+
});
|
|
383
|
+
if (sourceChanges) {
|
|
384
|
+
change = {
|
|
385
|
+
output: config.output,
|
|
386
|
+
serverName: config.serverName || templateData.title || '',
|
|
387
|
+
resolvedInput,
|
|
388
|
+
changes: sourceChanges,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
// Recording change history must never fail the generation itself.
|
|
394
|
+
helper_2.logger.debug('Failed to detect source changes', { error: error?.message });
|
|
395
|
+
}
|
|
396
|
+
return { success: true, resolvedInput, change };
|
|
344
397
|
}
|
|
345
398
|
}
|
|
346
399
|
exports.GeneratorHelper = GeneratorHelper;
|
|
@@ -3,7 +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.zConfig = exports.zGeneratorConfig = exports._zGeneratorConfig = exports.zApiPlugin = exports.zFetchOptions = exports.zHandleApi = exports.zApiDescriptor = exports.zTemplateResult = exports.zTemplateType = exports.zConfigType = void 0;
|
|
6
|
+
exports.zConfig = exports.zGeneratorConfig = exports._zGeneratorConfig = exports.zPerformanceConfig = exports.zApiPlugin = exports.zFetchOptions = exports.zHandleApi = exports.zApiDescriptor = exports.zTemplateResult = exports.zTemplateType = exports.zConfigType = void 0;
|
|
7
7
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
8
|
const v3_1 = require("zod/v3"); // v4 is unstable, temporarily using v3
|
|
9
9
|
/**
|
|
@@ -34,6 +34,20 @@ exports.zApiPlugin = v3_1.z.object({
|
|
|
34
34
|
getTemplate: v3_1.z.function().optional(),
|
|
35
35
|
onHandlebarsCreated: v3_1.z.function().optional(),
|
|
36
36
|
});
|
|
37
|
+
/**
|
|
38
|
+
* Performance tuning options. Declared explicitly so zod's default strip
|
|
39
|
+
* behavior does not drop `generator[].performance` during validation.
|
|
40
|
+
*/
|
|
41
|
+
exports.zPerformanceConfig = v3_1.z.object({
|
|
42
|
+
/** `'auto'` adapts the pool size to the API count, a number pins it, `false` disables workers */
|
|
43
|
+
workerPool: v3_1.z.union([v3_1.z.literal('auto'), v3_1.z.number(), v3_1.z.literal(false)]).optional(),
|
|
44
|
+
/** Max concurrency for the transform phase; defaults to `min(64, max(8, cpus*4))` */
|
|
45
|
+
transformConcurrency: v3_1.z.number().optional(),
|
|
46
|
+
/** Max parallelism for file writes; default 32 */
|
|
47
|
+
writeConcurrency: v3_1.z.number().optional(),
|
|
48
|
+
/** Sort components alphabetically for deterministic output; default true */
|
|
49
|
+
deterministicSort: v3_1.z.boolean().optional(),
|
|
50
|
+
});
|
|
37
51
|
exports._zGeneratorConfig = v3_1.z.object({
|
|
38
52
|
/**
|
|
39
53
|
* Openapi file path, it supports json and yaml file, and network url.
|
|
@@ -109,6 +123,10 @@ exports._zGeneratorConfig = v3_1.z.object({
|
|
|
109
123
|
* plugin will be executed before `handleApi`
|
|
110
124
|
*/
|
|
111
125
|
plugins: v3_1.z.array(exports.zApiPlugin).optional(),
|
|
126
|
+
/**
|
|
127
|
+
* Performance tuning options for code generation.
|
|
128
|
+
*/
|
|
129
|
+
performance: exports.zPerformanceConfig.optional(),
|
|
112
130
|
/**
|
|
113
131
|
* Filter or convert the generated api function and return a new `apiDescriptor` to generate the api.
|
|
114
132
|
* When this function is not specified, `apiDescriptor` object is not converted.
|
|
@@ -143,6 +161,11 @@ exports._zGeneratorConfig = v3_1.z.object({
|
|
|
143
161
|
});
|
|
144
162
|
exports.zGeneratorConfig = exports._zGeneratorConfig;
|
|
145
163
|
exports.zConfig = v3_1.z.object({
|
|
164
|
+
/**
|
|
165
|
+
* 产物格式化配置(oxfmt)。不做结构校验:字段原样透传给 oxfmt,由 oxfmt 自行校验并报错。
|
|
166
|
+
* 声明在 schema 中仅为避免 zod 的默认 strip 行为把该字段丢弃。
|
|
167
|
+
*/
|
|
168
|
+
format: v3_1.z.any().optional(),
|
|
146
169
|
/**
|
|
147
170
|
* API generation settings are arrays. Each item represents an automatically generated rule, including the generated input and output directories, specification file addresses, etc.
|
|
148
171
|
* Currently, only OpenAPI specifications are supported, including OpenAPI 2.0 and 3.0 specifications.
|
|
@@ -61,6 +61,28 @@ function joinPath(...parts) {
|
|
|
61
61
|
const p = node_path_1.default.join(...parts);
|
|
62
62
|
return p ? normalizeSlashes(p) : p;
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Recursively delete a directory, ignoring anything that is not there.
|
|
66
|
+
* Implemented with readdir/unlink/rmdir instead of `fs.rm` so it also works on
|
|
67
|
+
* virtual filesystems that do not implement `rm`.
|
|
68
|
+
*/
|
|
69
|
+
async function removeDirRecursive(dir) {
|
|
70
|
+
let entries = [];
|
|
71
|
+
try {
|
|
72
|
+
entries = await promises_1.default.readdir(dir, { withFileTypes: true });
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
const full = node_path_1.default.join(dir, entry.name);
|
|
79
|
+
if (entry.isDirectory())
|
|
80
|
+
await removeDirRecursive(full);
|
|
81
|
+
else
|
|
82
|
+
await promises_1.default.unlink(full).catch(() => { });
|
|
83
|
+
}
|
|
84
|
+
await promises_1.default.rmdir(dir).catch(() => { });
|
|
85
|
+
}
|
|
64
86
|
class TemplateHelper {
|
|
65
87
|
static instance;
|
|
66
88
|
/** Runtime cache of parsed template data, keyed by `${projectPath}::${output}`. Flushed to disk via flushAllData. */
|
|
@@ -286,7 +308,7 @@ class TemplateHelper {
|
|
|
286
308
|
}
|
|
287
309
|
/**
|
|
288
310
|
* 9.2.3: Private file writing — now internal to the streaming pipeline.
|
|
289
|
-
*
|
|
311
|
+
* oxfmt formatting applied at file level (9.5.2).
|
|
290
312
|
*/
|
|
291
313
|
async outputFiles(files, output, writeConcurrency = 32, formatFile = true) {
|
|
292
314
|
const entries = Object.entries(files);
|
|
@@ -308,14 +330,14 @@ class TemplateHelper {
|
|
|
308
330
|
const batch = entries.slice(i, i + concurrency);
|
|
309
331
|
await Promise.all(batch.map(async ([rp, content]) => {
|
|
310
332
|
const op = node_path_1.default.isAbsolute(rp) ? rp : node_path_1.default.join(output, rp);
|
|
311
|
-
// 9.5.2: Apply
|
|
333
|
+
// 9.5.2: Apply oxfmt at file level for .ts/.js files
|
|
312
334
|
let finalContent = content;
|
|
313
335
|
if (formatFile && /\.(?:ts|js|mjs|cjs|tsx|jsx)$/.test(rp)) {
|
|
314
336
|
try {
|
|
315
|
-
finalContent = await (0, utils_1.format)(content);
|
|
337
|
+
finalContent = await (0, utils_1.format)(rp, content);
|
|
316
338
|
}
|
|
317
339
|
catch {
|
|
318
|
-
//
|
|
340
|
+
// oxfmt format failed, use original content
|
|
319
341
|
}
|
|
320
342
|
}
|
|
321
343
|
return promises_1.default.writeFile(op, finalContent);
|
|
@@ -365,7 +387,20 @@ class TemplateHelper {
|
|
|
365
387
|
// --- Phase 1: Per-tag streaming (render → collect → batch write) ---
|
|
366
388
|
const nonDirTagTpls = tpls.filter(f => !f.insideTagDir && f.templateType === 'tag');
|
|
367
389
|
const dirTpls = tpls.filter(f => f.insideTagDir);
|
|
368
|
-
|
|
390
|
+
// Incremental rendering normally skips a tag whose hash is unchanged, which
|
|
391
|
+
// assumes its previously generated files are still on disk. If those files
|
|
392
|
+
// were deleted manually while the cache survived, the unchanged tag would
|
|
393
|
+
// never be emitted again. So an unchanged tag is also re-rendered whenever
|
|
394
|
+
// any of its expected artifacts is missing from the output directory.
|
|
395
|
+
let effectiveTags;
|
|
396
|
+
if (changedTags) {
|
|
397
|
+
const unchangedTags = tags.filter(t => !changedTags.has(t));
|
|
398
|
+
const missingTags = await this.collectTagsWithMissingArtifacts(unchangedTags, nonDirTagTpls, dirTpls, tagApisMap, outputDir);
|
|
399
|
+
effectiveTags = tags.filter(t => changedTags.has(t) || missingTags.has(t));
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
effectiveTags = tags;
|
|
403
|
+
}
|
|
369
404
|
let tagFilesWritten = 0;
|
|
370
405
|
logger_1.logger.debug('Phase 1: Per-tag streaming', {
|
|
371
406
|
totalTags: tags.length,
|
|
@@ -431,10 +466,83 @@ class TemplateHelper {
|
|
|
431
466
|
}
|
|
432
467
|
}
|
|
433
468
|
await this.applyHooksAndWrite(globalFiles, outputDir, beforeFileWrite, undefined, undefined, undefined, writeConcurrency, formatFile, allFilePaths);
|
|
469
|
+
// --- Phase 3: drop orphaned tag artifacts ---
|
|
470
|
+
// Tags that existed before but are gone from the spec must not leave stale
|
|
471
|
+
// files/directories behind in the output directory.
|
|
472
|
+
await this.removeOrphanTagArtifacts(changedTags, tags, tpls, outputDir);
|
|
434
473
|
logger_1.logger.debug('Phase 2 complete', { globalFilesWritten: Object.keys(globalFiles).length });
|
|
435
474
|
logger_1.logger.debug('Generation summary', { totalOutputFiles: allFilePaths.length });
|
|
436
475
|
return { filePaths: allFilePaths };
|
|
437
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Detect tags whose previously generated artifacts are missing from disk.
|
|
479
|
+
*
|
|
480
|
+
* Mirrors the path derivation used by `renderOne` / `expandByApi` to compute
|
|
481
|
+
* the expected output path of every per-tag file without rendering anything,
|
|
482
|
+
* then returns the tags that must be re-rendered because at least one of
|
|
483
|
+
* their files is gone.
|
|
484
|
+
*/
|
|
485
|
+
async collectTagsWithMissingArtifacts(candidateTags, nonDirTagTpls, dirTpls, tagApisMap, outputDir) {
|
|
486
|
+
const missingTags = new Set();
|
|
487
|
+
for (const tag of candidateTags) {
|
|
488
|
+
const tagApis = tagApisMap.get(tag);
|
|
489
|
+
const expectedPaths = [];
|
|
490
|
+
// Flat `[tag]`-named templates, e.g. `services/[tag].ts.handlebars`
|
|
491
|
+
for (const tf of nonDirTagTpls) {
|
|
492
|
+
expectedPaths.push(stripExt(normalizeSlashes(tf.relativePath.replace(constant_1.TemplatePlaceholder.TAG, tag))));
|
|
493
|
+
}
|
|
494
|
+
// Tag-dir templates, e.g. `[tag]/index.ts` and `[tag]/[api].ts`
|
|
495
|
+
for (const tf of dirTpls) {
|
|
496
|
+
if (tf.templateType === 'api') {
|
|
497
|
+
for (const api of tagApis?.apis || []) {
|
|
498
|
+
expectedPaths.push(stripExt(normalizeSlashes(tf.relativePath
|
|
499
|
+
.replace(constant_1.TemplatePlaceholder.TAG, tag)
|
|
500
|
+
.replace(constant_1.TemplatePlaceholder.API, api.name))));
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
expectedPaths.push(stripExt(normalizeSlashes(tf.relativePath.replace(constant_1.TemplatePlaceholder.TAG, tag))));
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
for (const relPath of expectedPaths) {
|
|
508
|
+
if (!(await (0, utils_1.existsPromise)(node_path_1.default.join(outputDir, relPath)))) {
|
|
509
|
+
missingTags.add(tag);
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return missingTags;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Remove generated artifacts of tags that no longer exist in the spec.
|
|
518
|
+
*
|
|
519
|
+
* `changedTags` contains both changed and removed tags; the ones missing from
|
|
520
|
+
* `tags` (the current tag list) are the removed ones. For those we delete:
|
|
521
|
+
* - the `[tag]` directory produced by tag-dir templates
|
|
522
|
+
* - the files produced by `[tag]`-named templates
|
|
523
|
+
*/
|
|
524
|
+
async removeOrphanTagArtifacts(changedTags, tags, tpls, outputDir) {
|
|
525
|
+
if (!changedTags || changedTags.size === 0)
|
|
526
|
+
return;
|
|
527
|
+
const removedTags = [...changedTags].filter(tag => !tags.includes(tag));
|
|
528
|
+
if (removedTags.length === 0)
|
|
529
|
+
return;
|
|
530
|
+
const tagDirTpls = tpls.filter(f => f.insideTagDir);
|
|
531
|
+
const tagTpls = tpls.filter(f => !f.insideTagDir && f.templateType === 'tag');
|
|
532
|
+
for (const tag of removedTags) {
|
|
533
|
+
// Tag-dir templates: delete the whole `[tag]` directory (may be nested)
|
|
534
|
+
const dirs = new Set(tagDirTpls.map(tf => node_path_1.default.dirname(tf.relativePath.replace(constant_1.TemplatePlaceholder.TAG, tag))));
|
|
535
|
+
for (const dir of dirs) {
|
|
536
|
+
await removeDirRecursive(node_path_1.default.join(outputDir, dir));
|
|
537
|
+
}
|
|
538
|
+
// Flat `[tag]`-named templates: delete the resolved file
|
|
539
|
+
for (const tf of tagTpls) {
|
|
540
|
+
const relPath = stripExt(normalizeSlashes(tf.relativePath.replace(constant_1.TemplatePlaceholder.TAG, tag)));
|
|
541
|
+
await promises_1.default.unlink(node_path_1.default.join(outputDir, relPath)).catch(() => { });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
logger_1.logger.debug('Removed orphaned tag artifacts', { tags: removedTags });
|
|
545
|
+
}
|
|
438
546
|
async applyHooksAndWrite(files, outputDir, beforeFileWrite, templateType, tag, api, writeConcurrency, formatFile, allFilePaths) {
|
|
439
547
|
if (beforeFileWrite) {
|
|
440
548
|
// Apply beforeFileWrite to each file sequentially for determinism
|
package/dist/index.js
CHANGED
|
@@ -17,13 +17,30 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
17
17
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
18
|
};
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.resolveWorkspaces = exports.logger = exports.generate = exports.defineConfig = exports.createConfig = exports.setGlobalConfig = void 0;
|
|
20
|
+
exports.resolveWorkspaces = exports.logger = exports.generate = exports.writeSourceSnapshot = exports.sourceDocumentHash = exports.readSourceSnapshot = exports.diffSourceDocument = exports.diffApis = exports.apiDiffKey = exports.removeChange = exports.listChanges = exports.LATEST_CHANGE_ID = exports.getChange = exports.countChanges = exports.defineConfig = exports.createConfig = exports.setGlobalConfig = exports.checkUpdates = void 0;
|
|
21
|
+
var checkUpdates_1 = require("./checkUpdates");
|
|
22
|
+
Object.defineProperty(exports, "checkUpdates", { enumerable: true, get: function () { return checkUpdates_1.checkUpdates; } });
|
|
21
23
|
var config_1 = require("./config");
|
|
22
24
|
Object.defineProperty(exports, "setGlobalConfig", { enumerable: true, get: function () { return config_1.setGlobalConfig; } });
|
|
23
25
|
var createConfig_1 = require("./createConfig");
|
|
24
26
|
Object.defineProperty(exports, "createConfig", { enumerable: true, get: function () { return __importDefault(createConfig_1).default; } });
|
|
25
27
|
var defineConfig_1 = require("./defineConfig");
|
|
26
28
|
Object.defineProperty(exports, "defineConfig", { enumerable: true, get: function () { return defineConfig_1.defineConfig; } });
|
|
29
|
+
var changeReport_1 = require("./functions/changeReport");
|
|
30
|
+
Object.defineProperty(exports, "countChanges", { enumerable: true, get: function () { return changeReport_1.countChanges; } });
|
|
31
|
+
Object.defineProperty(exports, "getChange", { enumerable: true, get: function () { return changeReport_1.getChange; } });
|
|
32
|
+
Object.defineProperty(exports, "LATEST_CHANGE_ID", { enumerable: true, get: function () { return changeReport_1.LATEST_CHANGE_ID; } });
|
|
33
|
+
Object.defineProperty(exports, "listChanges", { enumerable: true, get: function () { return changeReport_1.listChanges; } });
|
|
34
|
+
Object.defineProperty(exports, "removeChange", { enumerable: true, get: function () { return changeReport_1.removeChange; } });
|
|
35
|
+
var diffApis_1 = require("./functions/diffApis");
|
|
36
|
+
Object.defineProperty(exports, "apiDiffKey", { enumerable: true, get: function () { return diffApis_1.apiDiffKey; } });
|
|
37
|
+
Object.defineProperty(exports, "diffApis", { enumerable: true, get: function () { return diffApis_1.diffApis; } });
|
|
38
|
+
var diffDocument_1 = require("./functions/diffDocument");
|
|
39
|
+
Object.defineProperty(exports, "diffSourceDocument", { enumerable: true, get: function () { return diffDocument_1.diffSourceDocument; } });
|
|
40
|
+
var sourceSnapshot_1 = require("./functions/sourceSnapshot");
|
|
41
|
+
Object.defineProperty(exports, "readSourceSnapshot", { enumerable: true, get: function () { return sourceSnapshot_1.readSourceSnapshot; } });
|
|
42
|
+
Object.defineProperty(exports, "sourceDocumentHash", { enumerable: true, get: function () { return sourceSnapshot_1.sourceDocumentHash; } });
|
|
43
|
+
Object.defineProperty(exports, "writeSourceSnapshot", { enumerable: true, get: function () { return sourceSnapshot_1.writeSourceSnapshot; } });
|
|
27
44
|
var generate_1 = require("./generate");
|
|
28
45
|
Object.defineProperty(exports, "generate", { enumerable: true, get: function () { return __importDefault(generate_1).default; } });
|
|
29
46
|
var helper_1 = require("./helper");
|
package/dist/plugins/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.ky = exports.config = exports.axios = exports.alovaGlobals = exports.alova = exports.yapi = exports.swagger = exports.knife4j =
|
|
17
|
+
exports.ky = exports.config = exports.axios = exports.alovaGlobals = exports.alova = exports.yapi = exports.swagger = exports.knife4j = void 0;
|
|
18
18
|
__exportStar(require("./createPlugin"), exports);
|
|
19
19
|
__exportStar(require("./presets/aiDoc"), exports);
|
|
20
20
|
__exportStar(require("./presets/apifox"), exports);
|
|
@@ -22,10 +22,10 @@ __exportStar(require("./presets/filterApi"), exports);
|
|
|
22
22
|
__exportStar(require("./presets/importType"), exports);
|
|
23
23
|
__exportStar(require("./presets/payloadModifier"), exports);
|
|
24
24
|
var platform_1 = require("./presets/platform");
|
|
25
|
-
Object.defineProperty(exports, "fastapi", { enumerable: true, get: function () { return platform_1.fastapi; } });
|
|
26
25
|
Object.defineProperty(exports, "knife4j", { enumerable: true, get: function () { return platform_1.knife4j; } });
|
|
27
26
|
Object.defineProperty(exports, "swagger", { enumerable: true, get: function () { return platform_1.swagger; } });
|
|
28
27
|
Object.defineProperty(exports, "yapi", { enumerable: true, get: function () { return platform_1.yapi; } });
|
|
28
|
+
__exportStar(require("./presets/postman"), exports);
|
|
29
29
|
__exportStar(require("./presets/rename"), exports);
|
|
30
30
|
__exportStar(require("./presets/tagModifier"), exports);
|
|
31
31
|
// Template preset plugins
|
|
@@ -73,8 +73,15 @@ function aiDoc(config) {
|
|
|
73
73
|
});
|
|
74
74
|
if (agentValue) {
|
|
75
75
|
const agentsToInstall = resolveInstallAgents(agentValue);
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
if (agentsToInstall.length) {
|
|
77
|
+
for (const agent of agentsToInstall) {
|
|
78
|
+
installSkill(aidocsDir, agent, projectPath);
|
|
79
|
+
}
|
|
80
|
+
// The skill now lives in each agent's own skills directory. Keeping the
|
|
81
|
+
// generated copy under the output directory would store the same files
|
|
82
|
+
// twice, so it is dropped — but only after *every* install succeeded:
|
|
83
|
+
// a failing install throws above and leaves the source in place.
|
|
84
|
+
removeSkillSourceDir(aidocsDir);
|
|
78
85
|
}
|
|
79
86
|
}
|
|
80
87
|
},
|
|
@@ -147,6 +154,24 @@ function installSkill(skillPath, agent, projectPath) {
|
|
|
147
154
|
throw logger_1.logger.throwError(error);
|
|
148
155
|
}
|
|
149
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Delete the generated skill directory once it has been installed.
|
|
159
|
+
*
|
|
160
|
+
* `skills add` copies the whole directory into the agent's own skills folder,
|
|
161
|
+
* so the copy under the generator output is pure duplication.
|
|
162
|
+
*
|
|
163
|
+
* A removal failure is reported but never thrown: the install already
|
|
164
|
+
* succeeded, and failing the whole generation over a leftover directory would
|
|
165
|
+
* be worse than keeping it.
|
|
166
|
+
*/
|
|
167
|
+
function removeSkillSourceDir(skillPath) {
|
|
168
|
+
try {
|
|
169
|
+
node_fs_1.default.rmSync(skillPath, { recursive: true, force: true });
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
console.error(`${prefix}Failed to remove the generated skill directory "${skillPath}".`, error?.stack ?? error);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
150
175
|
/**
|
|
151
176
|
* Parse a `key=value` configuration file (same format as an environment file).
|
|
152
177
|
*
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TYPE_FAMILY_KEYS = void 0;
|
|
4
|
+
exports.isSchemaPrimitive = isSchemaPrimitive;
|
|
5
|
+
exports.clearTypeFamily = clearTypeFamily;
|
|
6
|
+
exports.stripRef = stripRef;
|
|
7
|
+
exports.dslToSchemaObject = dslToSchemaObject;
|
|
8
|
+
exports.inferEnumType = inferEnumType;
|
|
9
|
+
const VALID_PRIMITIVES = new Set([
|
|
10
|
+
'number',
|
|
11
|
+
'string',
|
|
12
|
+
'boolean',
|
|
13
|
+
'undefined',
|
|
14
|
+
'null',
|
|
15
|
+
'unknown',
|
|
16
|
+
'any',
|
|
17
|
+
'never',
|
|
18
|
+
]);
|
|
19
|
+
/**
|
|
20
|
+
* Keys describing the type shape. They are dropped when a type is replaced so that no
|
|
21
|
+
* leftover of the previous type survives (e.g. a stale `format: 'int64'`).
|
|
22
|
+
* Everything else — documentation keys above all — is kept.
|
|
23
|
+
*/
|
|
24
|
+
exports.TYPE_FAMILY_KEYS = [
|
|
25
|
+
'type',
|
|
26
|
+
'properties',
|
|
27
|
+
'items',
|
|
28
|
+
'enum',
|
|
29
|
+
'oneOf',
|
|
30
|
+
'anyOf',
|
|
31
|
+
'allOf',
|
|
32
|
+
'format',
|
|
33
|
+
'required',
|
|
34
|
+
];
|
|
35
|
+
function isSchemaPrimitive(value) {
|
|
36
|
+
return typeof value === 'string' && VALID_PRIMITIVES.has(value);
|
|
37
|
+
}
|
|
38
|
+
/** Rejects anything that is not a known type expression. */
|
|
39
|
+
function assertPrimitive(value) {
|
|
40
|
+
if (!VALID_PRIMITIVES.has(value)) {
|
|
41
|
+
throw new Error(`[payloadModifier] Invalid schema type "${value}". Must be one of: ${[...VALID_PRIMITIVES].join(', ')}`);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
/** Copies a schema keeping every documentation key and dropping every type-family key. */
|
|
46
|
+
function clearTypeFamily(schema) {
|
|
47
|
+
const rest = { ...schema };
|
|
48
|
+
for (const key of exports.TYPE_FAMILY_KEYS) {
|
|
49
|
+
delete rest[key];
|
|
50
|
+
}
|
|
51
|
+
return rest;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Drops the internal `_$ref` marker stamped by the dereference step. Without this, a
|
|
55
|
+
* replaced node would still look like a reference to the original component and the
|
|
56
|
+
* downstream merge step would restore the component instead of keeping the change.
|
|
57
|
+
*/
|
|
58
|
+
function stripRef(schema) {
|
|
59
|
+
if (!schema || typeof schema !== 'object') {
|
|
60
|
+
return schema;
|
|
61
|
+
}
|
|
62
|
+
const rest = { ...schema };
|
|
63
|
+
delete rest._$ref;
|
|
64
|
+
return rest;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Converts a spec DSL expression into a raw OpenAPI schema object.
|
|
68
|
+
* This is the only conversion direction the plugin has: nothing ever converts a schema
|
|
69
|
+
* back into the DSL, so no documentation can be lost along the way.
|
|
70
|
+
*/
|
|
71
|
+
function dslToSchemaObject(dsl) {
|
|
72
|
+
if (typeof dsl === 'string') {
|
|
73
|
+
return { type: assertPrimitive(dsl) };
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(dsl)) {
|
|
76
|
+
return { type: 'array', items: dslToItems(dsl) };
|
|
77
|
+
}
|
|
78
|
+
const spec = dsl;
|
|
79
|
+
if (Array.isArray(spec.oneOf)) {
|
|
80
|
+
return { oneOf: toSchemaList(spec.oneOf) };
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(spec.anyOf)) {
|
|
83
|
+
return { anyOf: toSchemaList(spec.anyOf) };
|
|
84
|
+
}
|
|
85
|
+
if (Array.isArray(spec.allOf)) {
|
|
86
|
+
return { allOf: toSchemaList(spec.allOf) };
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(spec.enum)) {
|
|
89
|
+
return enumToSchemaObject(spec.enum, spec.type);
|
|
90
|
+
}
|
|
91
|
+
// object shorthand
|
|
92
|
+
const properties = {};
|
|
93
|
+
for (const key of Object.keys(spec)) {
|
|
94
|
+
properties[key] = dslToSchemaObject(spec[key]);
|
|
95
|
+
}
|
|
96
|
+
return { type: 'object', properties, required: Object.keys(spec) };
|
|
97
|
+
}
|
|
98
|
+
function toSchemaList(list) {
|
|
99
|
+
return list.map(item => dslToSchemaObject(item));
|
|
100
|
+
}
|
|
101
|
+
/** A single element becomes `items`, multiple elements become a tuple (array) `items`. */
|
|
102
|
+
function dslToItems(list) {
|
|
103
|
+
const items = toSchemaList(list);
|
|
104
|
+
return items.length === 1 ? items[0] : items;
|
|
105
|
+
}
|
|
106
|
+
function enumToSchemaObject(values, declared) {
|
|
107
|
+
const result = { enum: values };
|
|
108
|
+
const type = declared !== undefined
|
|
109
|
+
? enumTypeToSchemaType(assertPrimitive(declared), values)
|
|
110
|
+
: inferEnumType(values);
|
|
111
|
+
if (type) {
|
|
112
|
+
result.type = type;
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Maps a declared DSL type onto the OpenAPI type of an enum: `number` becomes `integer`
|
|
118
|
+
* when every value is an integer, otherwise it stays `number`. TS-only types such as
|
|
119
|
+
* `any` or `never` are written through as-is.
|
|
120
|
+
*/
|
|
121
|
+
function enumTypeToSchemaType(declared, values) {
|
|
122
|
+
if (declared === 'number') {
|
|
123
|
+
return values.every(value => typeof value === 'number' && Number.isInteger(value))
|
|
124
|
+
? 'integer'
|
|
125
|
+
: 'number';
|
|
126
|
+
}
|
|
127
|
+
return declared;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Infers the OpenAPI type of an enum from its values. Returns `undefined` for mixed or
|
|
131
|
+
* empty values so no (possibly wrong) type is written.
|
|
132
|
+
*/
|
|
133
|
+
function inferEnumType(values) {
|
|
134
|
+
if (!values.length) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
if (values.every(value => typeof value === 'string')) {
|
|
138
|
+
return 'string';
|
|
139
|
+
}
|
|
140
|
+
if (values.every(value => typeof value === 'boolean')) {
|
|
141
|
+
return 'boolean';
|
|
142
|
+
}
|
|
143
|
+
if (values.every(value => typeof value === 'number')) {
|
|
144
|
+
return values.every(value => Number.isInteger(value)) ? 'integer' : 'number';
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|