wormajs 0.1.0 → 0.2.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/constant.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // 枚举常量定义 — 统一管理项目中所有硬编码的字符串/数字常量
4
4
  // ============================================================
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.FRAMEWORK_NAMES = exports.MODULE_TYPE_TO_KIND = exports.MODULE_TYPE_DIRS = exports.ModuleTypeDir = exports.PresetTemplateName = exports.TemplateSkipDir = exports.TemplatePlaceholder = exports.PluginName = exports.ModifierScope = exports.FilterScope = exports.RenameScope = exports.ParameterIn = exports.TEMPLATE_EXTENSIONS = exports.FileExtension = exports.FrameworkName = exports.ModuleKind = exports.PlatformTypeEnum = exports.ConfigTypeEnum = exports.TemplateTypeEnum = void 0;
6
+ exports.FRAMEWORK_NAMES = exports.MODULE_TYPE_TO_KIND = exports.MODULE_TYPE_DIRS = exports.ModuleTypeDir = exports.PresetTemplateName = exports.TemplateSkipDir = exports.TemplatePlaceholder = exports.PluginName = exports.FilterScope = exports.RenameScope = exports.ParameterIn = exports.TEMPLATE_EXTENSIONS = exports.FileExtension = exports.FrameworkName = exports.ModuleKind = exports.PlatformTypeEnum = exports.ConfigTypeEnum = exports.TemplateTypeEnum = void 0;
7
7
  exports.getTypeFileExtension = getTypeFileExtension;
8
8
  /** 模板代码生成类型 */
9
9
  var TemplateTypeEnum;
@@ -86,14 +86,6 @@ var FilterScope;
86
86
  FilterScope["URL"] = "url";
87
87
  FilterScope["TAG"] = "tag";
88
88
  })(FilterScope || (exports.FilterScope = FilterScope = {}));
89
- /** 参数修改 scope 类型 */
90
- var ModifierScope;
91
- (function (ModifierScope) {
92
- ModifierScope["PARAMS"] = "params";
93
- ModifierScope["PATH_PARAMS"] = "pathParams";
94
- ModifierScope["DATA"] = "data";
95
- ModifierScope["RESPONSE"] = "response";
96
- })(ModifierScope || (exports.ModifierScope = ModifierScope = {}));
97
89
  /** 内建插件名称 */
98
90
  var PluginName;
99
91
  (function (PluginName) {
@@ -13,7 +13,6 @@ const swagger2openapi_1 = __importDefault(require("swagger2openapi"));
13
13
  const poolManager_1 = require("../../../core/workerPool/poolManager");
14
14
  const helper_1 = require("../../../helper");
15
15
  const utils_1 = require("../../../utils");
16
- const supportedExtname = ['json', 'yaml'];
17
16
  function isSwagger2(data) {
18
17
  return !!data?.swagger;
19
18
  }
@@ -61,56 +60,72 @@ function convertSwagger2Async(data) {
61
60
  });
62
61
  });
63
62
  }
64
- // Parse local openapi files
65
- async function parseLocalFile(url, projectPath = process.cwd()) {
66
- const [, extname] = /\.([^.]+)$/.exec(url) ?? [];
67
- if (!supportedExtname.includes(extname)) {
68
- throw helper_1.logger.throwError(`Unsupported file type: ${extname}`, {
69
- url,
70
- projectPath,
71
- });
72
- }
63
+ // Read local openapi spec file as raw text
64
+ async function fetchRawLocalFile(url, projectPath = process.cwd()) {
73
65
  const filePath = node_path_1.default.resolve(projectPath, url);
74
- if (extname === 'yaml') {
75
- return js_yaml_1.default.load(await promises_1.default.readFile(filePath, 'utf-8'));
76
- }
77
- // M6-C3: prefer async read, fallback to require() for environments where
78
- // fs is mocked (e.g. memfs in tests) but the real file exists on disk
79
66
  try {
80
- return JSON.parse(await promises_1.default.readFile(filePath, 'utf-8'));
67
+ return await promises_1.default.readFile(filePath, 'utf-8');
81
68
  }
82
69
  catch {
83
- // Fallback: use require() which bypasses fs mocks to reach real disk
70
+ // M6-C3: fallback to require() for environments where fs is mocked
71
+ // (e.g. memfs in tests) but the real file exists on disk. Limited to JSON.
84
72
  // eslint-disable-next-line ts/no-require-imports
85
- return require(filePath);
73
+ return JSON.stringify(require(filePath), null, 2);
86
74
  }
87
75
  }
88
- // Parse remote openapi files
89
- async function parseRemoteFile(url, fetchOptions) {
90
- const dataText = (await (0, utils_1.fetchData)(url, fetchOptions)) ?? '';
91
- let data;
92
- try {
93
- // 尝试解析为 JSON 格式
94
- data = JSON.parse(dataText);
76
+ // Fetch remote openapi spec as raw text
77
+ async function fetchRawRemoteFile(url, fetchOptions) {
78
+ return (await (0, utils_1.fetchData)(url, fetchOptions)) ?? '';
79
+ }
80
+ const isRemoteUrl = (u) => /^https?:\/\//.test(u);
81
+ /**
82
+ * Fetch the raw spec text (JSON/YAML) from the first URL that succeeds.
83
+ * Returns the raw text together with the resolved URL; throws if all URLs fail.
84
+ */
85
+ async function fetchRawText(urls, options) {
86
+ if (urls.length === 0) {
87
+ throw helper_1.logger.throwError('No URLs provided to fetch OpenAPI document');
95
88
  }
96
- catch (jsonError) {
97
- try {
98
- // JSON 解析失败,尝试解析为 YAML 格式
99
- data = js_yaml_1.default.load(dataText);
100
- }
101
- catch (yamlError) {
102
- throw helper_1.logger.throwError(`Only JSON and YAML formats are supported. Parsing failed:
103
- ${jsonError instanceof Error ? jsonError.message : String(jsonError)}
104
- ${yamlError instanceof Error ? yamlError.message : String(yamlError)}`, {
105
- url,
106
- });
107
- }
89
+ const { projectPath, fetchOptions } = options;
90
+ // All URLs race in parallel: local files are fast, remote ones use network.
91
+ // Each task fetches the raw text AND validates that it is actually an OpenAPI/Swagger
92
+ // document before resolving. Invalid candidates (e.g. an HTML error page returned by
93
+ // a fallback URL) reject, so that Promise.any falls through to the next URL
94
+ // instead of resolving with garbage text.
95
+ const tasks = urls.map((u) => {
96
+ return (async () => {
97
+ const text = isRemoteUrl(u)
98
+ ? await fetchRawRemoteFile(u, fetchOptions)
99
+ : 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`);
116
+ }
117
+ return { text, url: u };
118
+ })();
119
+ });
120
+ try {
121
+ return await Promise.any(tasks);
108
122
  }
109
- // Validate if the data is valid (prevent server from returning error responses)
110
- if (!isValidOpenApiData(data)) {
111
- throw new Error(`Data retrieved from URL ${url} is not a valid OpenAPI document`);
123
+ catch (err) {
124
+ const errors = (err instanceof AggregateError)
125
+ ? err.errors.map((e) => e.message)
126
+ : [err.message];
127
+ throw helper_1.logger.throwError(`Unable to retrieve valid OpenAPI document from any URL:\n${errors.join('\n')}`);
112
128
  }
113
- return data;
114
129
  }
115
130
  // Validate OpenAPI data
116
131
  function isValidOpenApiData(data) {
@@ -124,49 +139,56 @@ function isValidOpenApiData(data) {
124
139
  // Check if it contains required OpenAPI/Swagger structure
125
140
  return !!(data.openapi || data.swagger || data.info || data.paths);
126
141
  }
127
- const isRemoteUrl = (u) => /^https?:\/\//.test(u);
128
142
  /**
129
- * Try all URLs in parallel (local & remote) first successful one wins.
130
- * Returns the parsed data together with the resolved URL; throws if all URLs fail.
143
+ * Parse a raw spec string (JSON or YAML) into an OpenAPIDocument.
144
+ * Performs format auto-detection, validity checks, and Swagger2→OpenAPI3 conversion.
131
145
  */
132
- async function tryUrls(urls, options) {
133
- if (urls.length === 0) {
134
- throw helper_1.logger.throwError('No URLs provided to fetch OpenAPI document');
146
+ async function parseSpec(text, url) {
147
+ let data;
148
+ try {
149
+ // Try to parse as JSON first
150
+ data = JSON.parse(text);
135
151
  }
136
- const { projectPath, fetchOptions } = options;
137
- // All URLs race in parallel: local files are fast, remote ones use network
138
- const tasks = urls.map((u) => {
139
- if (isRemoteUrl(u)) {
140
- return parseRemoteFile(u, fetchOptions).then(data => ({ data, url: u }), (err) => {
141
- throw new Error(`${u}: ${err instanceof Error ? err.message : String(err)}`);
152
+ catch (jsonError) {
153
+ try {
154
+ // Fall back to YAML (also covers JSON, since JSON is a subset of YAML)
155
+ data = js_yaml_1.default.load(text);
156
+ }
157
+ catch (yamlError) {
158
+ throw helper_1.logger.throwError(`Only JSON and YAML formats are supported. Parsing failed:
159
+ ${jsonError instanceof Error ? jsonError.message : String(jsonError)}
160
+ ${yamlError instanceof Error ? yamlError.message : String(yamlError)}`, {
161
+ url,
142
162
  });
143
163
  }
144
- return parseLocalFile(u, projectPath).then(data => ({ data, url: u }));
145
- });
146
- try {
147
- return await Promise.any(tasks);
148
164
  }
149
- catch (err) {
150
- const errors = (err instanceof AggregateError)
151
- ? err.errors.map((e) => e.message)
152
- : [err.message];
153
- throw helper_1.logger.throwError(`Unable to retrieve valid OpenAPI document from any URL:\n${errors.join('\n')}`);
165
+ // Validate if the data is valid (prevent server from returning error responses)
166
+ if (!isValidOpenApiData(data)) {
167
+ throw new Error(`Data retrieved from URL ${url} is not a valid OpenAPI document`);
168
+ }
169
+ // If it is a swagger2 file convert via worker to avoid main-thread blocking
170
+ if (isSwagger2(data)) {
171
+ data = await convertSwagger2Async(data);
154
172
  }
173
+ return data;
155
174
  }
156
175
  /**
157
176
  * Parse OpenAPI document and return the resolved URL alongside the data.
158
177
  * Use this when you need to know which URL actually provided the document.
178
+ *
179
+ * If `beforeSpecParse` is provided, it is invoked with the raw spec text once
180
+ * it has been fetched (but before parsing), and its returned string replaces
181
+ * the text that will be parsed.
159
182
  */
160
183
  async function getOpenApiDataWithUrl(url, options) {
161
- const { projectPath, fetchOptions } = options ?? {};
184
+ const { projectPath, fetchOptions, beforeSpecParse } = options ?? {};
162
185
  // Normalize to array — single string or array both handled uniformly
163
186
  const urls = Array.isArray(url) ? url : [url];
164
- const { data, url: resolvedUrl } = await tryUrls(urls, { projectPath, fetchOptions });
165
- let result = data;
166
- // If it is a swagger2 file convert via worker to avoid main-thread blocking
167
- if (isSwagger2(result)) {
168
- result = await convertSwagger2Async(result);
169
- }
187
+ const { text, url: resolvedUrl } = await fetchRawText(urls, { projectPath, fetchOptions });
188
+ // Allow the caller (e.g. a `beforeSpecParse` plugin hook) to transform the
189
+ // raw spec text before it is parsed into an OpenAPIDocument.
190
+ const finalText = (beforeSpecParse ? await beforeSpecParse(text) : text) ?? text;
191
+ const result = await parseSpec(finalText, resolvedUrl);
170
192
  if (!result) {
171
193
  throw helper_1.logger.throwError(`Cannot read file from ${urls.join(', ')}`, {
172
194
  projectPath,
@@ -128,10 +128,11 @@ class GeneratorHelper {
128
128
  fetchOptions: config.fetchOptions,
129
129
  });
130
130
  }
131
- static async openApiDataWithUrl(config, projectPath) {
131
+ static async openApiDataWithUrl(config, projectPath, opts) {
132
132
  return (0, helper_1.getOpenApiDataWithUrl)(config.input, {
133
133
  projectPath,
134
134
  fetchOptions: config.fetchOptions,
135
+ beforeSpecParse: opts?.beforeSpecParse,
135
136
  });
136
137
  }
137
138
  static async generate(config, { projectPath, force, tracker }) {
@@ -152,16 +153,17 @@ class GeneratorHelper {
152
153
  });
153
154
  reportCore(5, 'starting');
154
155
  const frozenConfig = Object.freeze(config);
155
- // Plugin: handle before parse openapi
156
- reportCore(10, 'beforeOpenapiParse');
157
- helper_2.logger.debug('Running beforeOpenapiParse hook');
158
- await pluginDriver.hookParallelEach('beforeOpenapiParse', () => ({
159
- config: frozenConfig,
160
- projectPath,
161
- }));
156
+ reportCore(10, 'beforeSpecParse');
157
+ helper_2.logger.debug('Fetching and parsing OpenAPI document', { input: config.input });
162
158
  reportCore(20, 'parsing openapi document');
163
- helper_2.logger.debug('Fetching OpenAPI document', { input: config.input });
164
- const openApiResult = await this.openApiDataWithUrl(config, projectPath);
159
+ const openApiResult = await this.openApiDataWithUrl(config, projectPath, {
160
+ // Plugin: beforeSpecParse receives the raw spec string, may return a modified string
161
+ beforeSpecParse: (spec) => pluginDriver.hookPipe('beforeSpecParse', spec, (_p, current, _ctx) => ({
162
+ config: frozenConfig,
163
+ spec: current,
164
+ projectPath,
165
+ })),
166
+ });
165
167
  let document = openApiResult.data;
166
168
  const resolvedInput = openApiResult.resolvedUrl;
167
169
  if (!document) {
@@ -174,10 +176,10 @@ class GeneratorHelper {
174
176
  version: document?.info?.version,
175
177
  paths: Object.keys(document?.paths || {}).length,
176
178
  });
177
- reportCore(35, 'openapi parsed');
178
- // Plugin: handle after parse openapi (openapiParsed)
179
- helper_2.logger.debug('Running openapiParsed hook', { pluginCount });
180
- const openapiParsed = await pluginDriver.hookSeqEach('openapiParsed', (_p, prevResult, _ctx) => {
179
+ reportCore(35, 'specParsed');
180
+ // Plugin: handle after parse openapi (specParsed)
181
+ helper_2.logger.debug('Running specParsed hook', { pluginCount });
182
+ const specParsed = await pluginDriver.hookSeqEach('specParsed', (_p, prevResult, _ctx) => {
181
183
  if (prevResult) {
182
184
  document = prevResult;
183
185
  }
@@ -187,11 +189,11 @@ class GeneratorHelper {
187
189
  projectPath,
188
190
  };
189
191
  });
190
- if (openapiParsed) {
191
- document = openapiParsed;
192
- helper_2.logger.debug('openapiParsed hook modified document');
192
+ if (specParsed) {
193
+ document = specParsed;
194
+ helper_2.logger.debug('specParsed hook modified document');
193
195
  }
194
- reportCore(45, 'openapiParsed');
196
+ reportCore(45, 'specParsed');
195
197
  const output = node_path_1.default.resolve(projectPath, config.output);
196
198
  const templateType = await GeneratorHelper.getTemplateType(config, projectPath);
197
199
  helper_2.logger.debug('Resolved output and template type', { output, templateType });
@@ -26,8 +26,8 @@ exports.zFetchOptions = v3_1.z.record(v3_1.z.string(), v3_1.z.any());
26
26
  exports.zApiPlugin = v3_1.z.object({
27
27
  name: v3_1.z.string().optional(),
28
28
  config: v3_1.z.function().optional(),
29
- beforeOpenapiParse: v3_1.z.function().optional(),
30
- openapiParsed: v3_1.z.function().optional(),
29
+ beforeSpecParse: v3_1.z.function().optional(),
30
+ specParsed: v3_1.z.function().optional(),
31
31
  beforeCodeGenerate: v3_1.z.function().optional(),
32
32
  beforeFileWrite: v3_1.z.function().optional(),
33
33
  codeGenerated: v3_1.z.function().optional(),
@@ -8,9 +8,9 @@ exports.CORE_PROGRESS_SOURCE = 'core';
8
8
  */
9
9
  exports.GeneratorStage = {
10
10
  INIT: 'init',
11
- BEFORE_OPENAPI_PARSE: 'beforeOpenapiParse',
11
+ BEFORE_SPEC_PARSE: 'beforeSpecParse',
12
12
  PARSE_OPENAPI: 'parseOpenapi',
13
- OPENAPI_PARSED: 'openapiParsed',
13
+ SPEC_PARSED: 'specParsed',
14
14
  TEMPLATE_LOADED: 'templateLoaded',
15
15
  TEMPLATE_DATA_PARSED: 'templateDataParsed',
16
16
  BEFORE_CODE_GENERATE: 'beforeCodeGenerate',
@@ -15,11 +15,13 @@ function toSchemaObject(base, s) {
15
15
  delete schema.required;
16
16
  return schema;
17
17
  };
18
- // Legacy union as array (treated as oneOf)
18
+ // Native array type (elements are Schema)
19
19
  if (Array.isArray(s)) {
20
- const baseOneOf = base.oneOf || [];
20
+ const arr = s;
21
21
  cleanType(result);
22
- result.oneOf = s.map((item, idx) => toSchemaObject(baseOneOf[idx] || {}, item));
22
+ result.type = 'array';
23
+ const items = arr.map(item => toSchemaObject({}, item));
24
+ result.items = (items.length === 1 ? items[0] : items);
23
25
  return result;
24
26
  }
25
27
  // Primitive types and no-op primitives
@@ -33,18 +35,21 @@ function toSchemaObject(base, s) {
33
35
  const baseOneOf = base.oneOf || [];
34
36
  cleanType(result);
35
37
  result.oneOf = spec.oneOf.map((item, idx) => toSchemaObject(baseOneOf[idx] || {}, item));
38
+ return result;
36
39
  }
37
40
  if (s.anyOf) {
38
41
  const spec = s;
39
42
  const baseAnyOf = base.anyOf || [];
40
43
  cleanType(result);
41
44
  result.anyOf = spec.anyOf.map((item, idx) => toSchemaObject(baseAnyOf[idx] || {}, item));
45
+ return result;
42
46
  }
43
47
  if (s.allOf) {
44
48
  const spec = s;
45
49
  const baseAllOf = base.allOf || [];
46
50
  cleanType(result);
47
51
  result.allOf = spec.allOf.map((item, idx) => toSchemaObject(baseAllOf[idx] || {}, item));
52
+ return result;
48
53
  }
49
54
  // Enum: set enum and optional type
50
55
  if (s.enum) {
@@ -53,30 +58,15 @@ function toSchemaObject(base, s) {
53
58
  if (spec.type) {
54
59
  result.type = spec.type;
55
60
  }
56
- }
57
- // Array: set/merge items
58
- if (s.type === 'array') {
59
- const spec = s;
60
- result.type = 'array';
61
- const baseItems = result.items;
62
- if (Array.isArray(spec.items)) {
63
- // Tuple items: replace entire items with tuple
64
- const items = spec.items.map(item => toSchemaObject({}, item));
65
- result.items = items;
66
- }
67
- else {
68
- // Single items: merge into existing items schema
69
- const patchItem = toSchemaObject(typeof baseItems === 'object' ? baseItems : {}, spec.items);
70
- result.items = patchItem;
71
- }
72
61
  return result;
73
62
  }
74
- // Object (reference-like map): merge properties and required
63
+ // Object (reference-like map): replace properties and required with handler's spec
64
+ // (the SchemaReference returned by the handler fully replaces this field, only keeping scalar fields like description from base)
75
65
  const ref = s;
76
66
  if (ref && typeof ref === 'object') {
77
67
  result.type = 'object';
78
- const properties = { ...result.properties };
79
- const requiredSet = new Set(Array.isArray(result.required) ? result.required : []);
68
+ const properties = {};
69
+ const requiredSet = new Set();
80
70
  for (const key in ref) {
81
71
  const val = ref[key];
82
72
  if (!val) {
@@ -137,16 +127,16 @@ function toSchemaSpec(obj) {
137
127
  const type = typeof obj.type === 'string' ? obj.type : undefined;
138
128
  return { enum: obj.enum, type };
139
129
  }
140
- // Array
130
+ // Array -> native array
141
131
  if (obj.type === 'array' || obj.items) {
142
132
  const items = obj.items;
143
133
  if (Array.isArray(items)) {
144
- return { type: 'array', items: items.map((it) => toSchemaSpec(it)) };
134
+ return items.map((it) => toSchemaSpec(it));
145
135
  }
146
136
  if (items) {
147
- return { type: 'array', items: toSchemaSpec(items) };
137
+ return [toSchemaSpec(items)];
148
138
  }
149
- return { type: 'array', items: 'unknown' };
139
+ return ['unknown'];
150
140
  }
151
141
  // Object
152
142
  if (obj.type === 'object' || obj.properties) {
@@ -160,7 +150,7 @@ function toSchemaSpec(obj) {
160
150
  }
161
151
  return result;
162
152
  }
163
- // type union as array
153
+ // type union as array -> oneOf
164
154
  if (Array.isArray(obj.type)) {
165
155
  const typeArr = obj.type;
166
156
  const mapped = typeArr.map(schemaTypeToPrimitiveType);
@@ -175,21 +165,27 @@ function applyModifierSchema(schema, config, { required }) {
175
165
  }
176
166
  const cloned = { ...schema };
177
167
  const currentSpec = toSchemaSpec(cloned);
178
- const ret = config.handler(currentSpec);
168
+ // When the field is itself optional and is a primitive, wrap it as { required, type } before passing to handler
169
+ const handlerInput = (required === false && typeof currentSpec === 'string')
170
+ ? { required: false, type: currentSpec }
171
+ : currentSpec;
172
+ const ret = config.handler(handlerInput);
179
173
  if (!ret) {
180
174
  return {
181
175
  required,
182
- value: null,
176
+ schema: null,
183
177
  };
184
178
  }
185
- if (typeof ret === 'object' && 'required' in ret && 'value' in ret) {
179
+ // A returned { required, type } means changing requiredness (driven by the `type` field)
180
+ if (typeof ret === 'object' && !Array.isArray(ret) && 'required' in ret && 'type' in ret) {
181
+ const opt = ret;
186
182
  return {
187
- required: !!(ret.required ?? required),
188
- value: toSchemaObject(cloned, ret.value),
183
+ required: !!(opt.required ?? required),
184
+ schema: toSchemaObject(cloned, opt.type),
189
185
  };
190
186
  }
191
187
  return {
192
188
  required,
193
- value: toSchemaObject(cloned, ret),
189
+ schema: toSchemaObject(cloned, ret),
194
190
  };
195
191
  }
@@ -28,13 +28,13 @@ function modifySchemaProperties(schema, config) {
28
28
  if (!(0, utils_1.isMatch)(key, config.match)) {
29
29
  continue;
30
30
  }
31
- const { required: requiredOverride, value: valueSchema } = (0, hepler_1.applyModifierSchema)(props[key], config, { required: required.includes(key) });
31
+ const { required: requiredOverride, schema: schemaValue } = (0, hepler_1.applyModifierSchema)(props[key], config, { required: required.includes(key) });
32
32
  required = required.filter(r => r !== key);
33
- if (!valueSchema) {
33
+ if (!schemaValue) {
34
34
  delete props[key];
35
35
  continue;
36
36
  }
37
- props[key] = valueSchema;
37
+ props[key] = schemaValue;
38
38
  if (requiredOverride) {
39
39
  required.push(key);
40
40
  }
@@ -53,7 +53,7 @@ function modifyParameters(parameters, type, config) {
53
53
  if (!(0, utils_1.isMatch)(param.name, config.match)) {
54
54
  return param;
55
55
  }
56
- const { value: schema, required } = (0, hepler_1.applyModifierSchema)(param.schema, config, { required: !!param.required });
56
+ const { schema, required } = (0, hepler_1.applyModifierSchema)(param.schema, config, { required: !!param.required });
57
57
  if (!schema) {
58
58
  return null;
59
59
  }
@@ -73,22 +73,22 @@ function payloadModifierApiDescriptor(apiDescriptor, config) {
73
73
  const newDescriptor = { ...apiDescriptor };
74
74
  const { scope } = config;
75
75
  switch (scope) {
76
- case constant_1.ModifierScope.PARAMS:
76
+ case 'params':
77
77
  if (newDescriptor.parameters) {
78
78
  newDescriptor.parameters = modifyParameters(newDescriptor.parameters, constant_1.ParameterIn.QUERY, config);
79
79
  }
80
80
  break;
81
- case constant_1.ModifierScope.PATH_PARAMS:
81
+ case 'pathParams':
82
82
  if (newDescriptor.parameters) {
83
83
  newDescriptor.parameters = modifyParameters(newDescriptor.parameters, constant_1.ParameterIn.PATH, config);
84
84
  }
85
85
  break;
86
- case constant_1.ModifierScope.DATA:
86
+ case 'data':
87
87
  if (newDescriptor.requestBody) {
88
88
  newDescriptor.requestBody = modifySchemaProperties(newDescriptor.requestBody, config);
89
89
  }
90
90
  break;
91
- case constant_1.ModifierScope.RESPONSE:
91
+ case 'response':
92
92
  if (newDescriptor.responses) {
93
93
  newDescriptor.responses = modifySchemaProperties(newDescriptor.responses, config);
94
94
  }
@@ -131,6 +131,8 @@ function renameUrl(url, config, apiDescriptor) {
131
131
  * Recursively processes a schema (supports nested objects and arrays),
132
132
  * applying property renaming to nested objects.
133
133
  * @param schema current schema
134
+ * @param config rename configuration
135
+ * @param apiDescriptor API descriptor
134
136
  * @param level current nesting level (starts from 0)
135
137
  * @param scopeLabel used to identify the scope in duplicate-name errors
136
138
  */
@@ -158,6 +160,9 @@ function transformSchema(schema, config, apiDescriptor, level, scopeLabel) {
158
160
  }
159
161
  /**
160
162
  * Transforms object properties using the renaming rules (supports deep recursive renaming)
163
+ * @param obj object whose properties to transform
164
+ * @param config rename configuration
165
+ * @param apiDescriptor API descriptor
161
166
  * @param level current nesting level (starts from 0), passed through to the match function
162
167
  * @param scopeLabel used to identify the scope in duplicate-name errors
163
168
  */
@@ -38,6 +38,10 @@ function processApiTags(apiDescriptor, handler) {
38
38
  // Process each tag and filter out null/undefined results
39
39
  newDescriptor.tags = newDescriptor.tags
40
40
  .map((tag) => {
41
+ // Resolve the tag to keep when the modification cannot be applied:
42
+ // keep the original tag only if it is itself valid, otherwise drop it.
43
+ // This guarantees that the final output never contains an invalid tag.
44
+ const keepOriginalOrDrop = () => isValidTagName(tag) ? tag : null;
41
45
  try {
42
46
  // Call user provided handler function
43
47
  const modifiedTag = handler(tag);
@@ -47,12 +51,12 @@ function processApiTags(apiDescriptor, handler) {
47
51
  }
48
52
  // Validate if modified tag follows naming conventions
49
53
  if (!isValidTagName(modifiedTag)) {
50
- return tag; // Keep original tag if invalid
54
+ return keepOriginalOrDrop(); // Keep original tag if valid, otherwise drop
51
55
  }
52
56
  return modifiedTag.trim(); // Return trimmed modified tag
53
57
  }
54
58
  catch {
55
- return tag; // Return original tag on error
59
+ return keepOriginalOrDrop(); // Keep original tag if valid, otherwise drop on error
56
60
  }
57
61
  })
58
62
  .filter((tag) => tag != null); // Filter out null/undefined values
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wormajs",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A modern OpenAPI code generator - Generate type-safe API clients from OpenAPI specs",
5
5
  "author": "worma",
6
6
  "license": "MIT",
@@ -54,12 +54,14 @@ export interface ConfigHookParams {
54
54
  projectPath: string;
55
55
  reportProgress: ReportProgress;
56
56
  }
57
- export interface BeforeOpenapiParseHookParams {
57
+ export interface BeforeSpecParseHookParams {
58
58
  config: Readonly<GeneratorConfig>;
59
+ /** The raw OpenAPI specification text (JSON or YAML), before it is parsed. */
60
+ spec: string;
59
61
  projectPath: string;
60
62
  reportProgress: ReportProgress;
61
63
  }
62
- export interface OpenapiParsedHookParams {
64
+ export interface SpecParsedHookParams {
63
65
  config: Readonly<GeneratorConfig>;
64
66
  document: OpenAPIDocument;
65
67
  projectPath: string;
@@ -140,14 +142,16 @@ export interface ApiPlugin {
140
142
  */
141
143
  config?: (params: ConfigHookParams) => MaybePromise<GeneratorConfig | undefined | null | void>;
142
144
  /**
143
- * Called before parsing the OpenAPI file.
145
+ * Called after the raw OpenAPI spec text is fetched but before it is parsed.
146
+ * Return a (possibly modified) string to replace the spec text that will be
147
+ * parsed. Returning nothing keeps the original spec text.
144
148
  */
145
- beforeOpenapiParse?: (params: BeforeOpenapiParseHookParams) => void;
149
+ beforeSpecParse?: (params: BeforeSpecParseHookParams) => MaybePromise<string | undefined | null | void>;
146
150
  /**
147
151
  * Manipulate the openapi document after parsing.
148
152
  * Returning null does NOT replacing anything.
149
153
  */
150
- openapiParsed?: (params: OpenapiParsedHookParams) => MaybePromise<OpenAPIDocument | undefined | null | void>;
154
+ specParsed?: (params: SpecParsedHookParams) => MaybePromise<OpenAPIDocument | undefined | null | void>;
151
155
  /**
152
156
  * Called before code generation. Mutate `params.data` directly to inject
153
157
  * configuration data (no longer returns a value).
@@ -42,12 +42,14 @@ export interface ConfigHookParams {
42
42
  projectPath: string;
43
43
  reportProgress: ReportProgress;
44
44
  }
45
- export interface BeforeOpenapiParseHookParams {
45
+ export interface BeforeSpecParseHookParams {
46
46
  config: Readonly<GeneratorConfig>;
47
+ /** The raw OpenAPI specification text (JSON or YAML), before it is parsed. */
48
+ spec: string;
47
49
  projectPath: string;
48
50
  reportProgress: ReportProgress;
49
51
  }
50
- export interface OpenapiParsedHookParams {
52
+ export interface SpecParsedHookParams {
51
53
  config: Readonly<GeneratorConfig>;
52
54
  document: OpenAPIDocument;
53
55
  projectPath: string;
@@ -128,14 +130,16 @@ export interface ApiPlugin {
128
130
  */
129
131
  config?: (params: ConfigHookParams) => MaybePromise<GeneratorConfig | undefined | null | void>;
130
132
  /**
131
- * Called before parsing the OpenAPI file.
133
+ * Called after the raw OpenAPI spec text is fetched but before it is parsed.
134
+ * Return a (possibly modified) string to replace the spec text that will be
135
+ * parsed. Returning nothing keeps the original spec text.
132
136
  */
133
- beforeOpenapiParse?: (params: BeforeOpenapiParseHookParams) => void;
137
+ beforeSpecParse?: (params: BeforeSpecParseHookParams) => MaybePromise<string | undefined | null | void>;
134
138
  /**
135
139
  * Manipulate the openapi document after parsing.
136
140
  * Returning null does NOT replacing anything.
137
141
  */
138
- openapiParsed?: (params: OpenapiParsedHookParams) => MaybePromise<OpenAPIDocument | undefined | null | void>;
142
+ specParsed?: (params: SpecParsedHookParams) => MaybePromise<OpenAPIDocument | undefined | null | void>;
139
143
  /**
140
144
  * Called before code generation. Mutate `params.data` directly to inject
141
145
  * configuration data (no longer returns a value).
@@ -529,36 +533,29 @@ export interface ImportTypeOptions {
529
533
  export declare function importType(imports: Record<string, string[]>, options?: {
530
534
  files?: string[];
531
535
  }): ApiPlugin;
532
- declare enum ModifierScope {
533
- PARAMS = "params",
534
- PATH_PARAMS = "pathParams",
535
- DATA = "data",
536
- RESPONSE = "response"
537
- }
538
- export type SchemaPrimitive = "number" | "string" | "boolean" | "undefined" | "null" | "unknown" | "any" | "never" | ({} & string);
536
+ export type ModifierScope = "params" | "pathParams" | "data" | "response";
537
+ export type SchemaPrimitive = "number" | "string" | "boolean" | "undefined" | "null" | "unknown" | "any" | "never";
539
538
  /**
540
- * 表示数组类型
539
+ * Array type: a native JS array whose elements are Schemas.
540
+ * e.g. ['string'] means string[]; ['string', 'number'] means the tuple [string, number]
541
541
  */
542
- export interface SchemaArray {
543
- type: "array";
544
- items: Schema | Schema[];
545
- }
542
+ export type SchemaArray = Schema[];
546
543
  /**
547
- * 修改参数为引用类型
548
- * key末端添加上?表示为可选值
544
+ * Object/reference type.
545
+ * Append '?' to the end of a key to mark it optional.
549
546
  */
550
547
  export interface SchemaReference {
551
548
  [attr: string]: Schema;
552
549
  }
553
550
  /**
554
- * 枚举类型表示
551
+ * Enum type representation.
555
552
  */
556
553
  export interface SchemaEnum {
557
554
  enum: Array<string | number | boolean | null>;
558
555
  type?: SchemaPrimitive;
559
556
  }
560
557
  /**
561
- * 组合类型表示(与/或/交叉)
558
+ * Composite types (oneOf / anyOf / allOf).
562
559
  */
563
560
  export interface SchemaOneOf {
564
561
  oneOf: Schema[];
@@ -570,31 +567,47 @@ export interface SchemaAllOf {
570
567
  allOf: Schema[];
571
568
  }
572
569
  /**
573
- * 数据Schema
574
- * SchemaArray表示类型数组,而数组表示“或”的意思
570
+ * Standalone primitive type that is itself optional (driven by the `type` field).
571
+ * Used in handler input/output to mean "this field is optional / make it optional".
572
+ */
573
+ export interface SchemaOptional {
574
+ required: boolean;
575
+ type: Schema;
576
+ }
577
+ /**
578
+ * The data Schema.
579
+ * - SchemaArray is a native array (elements are Schemas)
580
+ * - composite types use { oneOf | anyOf | allOf: Schema[] }
581
+ * - optional object properties use a trailing '?' on the key;
582
+ * a standalone optional primitive uses the SchemaOptional wrapper
575
583
  */
576
- export type Schema = SchemaPrimitive | SchemaReference | SchemaArray | SchemaEnum | SchemaOneOf | SchemaAnyOf | SchemaAllOf | Array<SchemaPrimitive | SchemaReference | SchemaArray | SchemaEnum>;
577
- export interface ModifierConfig<T extends Schema> {
584
+ export type Schema = SchemaPrimitive | SchemaReference | SchemaArray | SchemaEnum | SchemaOneOf | SchemaAnyOf | SchemaAllOf | SchemaOptional;
585
+ export interface ModifierConfig {
578
586
  /**
579
- * 生效范围,表示处理哪个位置的参数
587
+ * The scope the modifier applies to (which parameter location to process).
580
588
  */
581
589
  scope: ModifierScope;
582
590
  /**
583
- * 匹配规则,只有匹配到的才会进行转换,不指定则转换全部
584
- * string:原参数名包含此string;RegExp:原参数名匹配此正则;函数时接收key并返回是否匹配的boolean值
591
+ * Match rule. Only matched fields are transformed; when omitted, all fields are transformed.
592
+ * - string: the original field name contains this string
593
+ * - RegExp: the original field name matches this pattern
594
+ * - function: receives the key and returns a boolean indicating a match
585
595
  */
586
596
  match?: string | RegExp | ((key: string) => boolean);
587
597
  /**
588
- * handler用于灵活修改参数类型值
589
- * @param schema Schema中的一种,由用户自行定义
590
- * @returns 返回多种参数,具体为:Schema表示修改的类型;{ required: boolean, value: Schema }表示可将当前值修改为是否必填;void | null | undefined表示移除当前字段
598
+ * handler flexibly modifies the parameter type value.
599
+ * @param schema the original field type, already converted to the user-facing Schema representation.
600
+ * When the field itself is optional and is a primitive, it is passed as { required: false, type: 'string' }.
601
+ * Narrow the type inside handler if needed (e.g. with a cast).
602
+ * @returns Schema to change the type; { required: boolean, type: Schema } to change requiredness (driven by `type`);
603
+ * void | null | undefined to remove the field.
591
604
  */
592
- handler: (schema: T) => Schema | {
605
+ handler: (schema: Schema) => Schema | {
593
606
  required: boolean;
594
- value: Schema;
607
+ type: Schema;
595
608
  } | void | null | undefined;
596
609
  }
597
- export type PayloadModifierConfig = ModifierConfig<Schema>;
610
+ export type PayloadModifierConfig = ModifierConfig;
598
611
  export declare function payloadModifier(configs: PayloadModifierConfig[]): ApiPlugin;
599
612
  /**
600
613
  * FastAPI platform plugin.