@batype/chainspec 2.0.0 → 2.1.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/README.md CHANGED
@@ -15,7 +15,7 @@ React + Vite本地生成器,普通Node/browser SDK与React/SWR入口分开。*
15
15
 
16
16
  ## 一次完整流程
17
17
 
18
- 导入或粘贴JSON → 配置命名空间/分组/服务地址 → 生成 → 搜索接口并预览调用、类型与诊断 → 下载完整SDK ZIP。输入与配置改动后,旧结果暂停复制/下载。输入面板可以折叠。支持亮色、暗色、系统主题和手机布局。
18
+ 导入文件、粘贴JSON或从URL获取 → 编辑可复用的JSON配置 → 生成 → 搜索接口并预览调用、类型与诊断 → 下载完整SDK ZIP。输入与配置改动后,旧结果暂停复制/下载。输入面板可以折叠。支持亮色、暗色、系统主题和手机布局。
19
19
 
20
20
  支持Swagger2.0、OpenAPI3.0/3.1 JSON的常见子集,本地引用、递归模型、枚举、组合类型、参数与JSON/平面表单。没有模型的响应使用unknown并提示;不是完整的OpenAPI或JSON Schema实现。远程引用、YAML和Swagger UI HTML需先转换。
21
21
 
@@ -111,7 +111,7 @@ npm run dev
111
111
  # http://127.0.0.1:4184/chainspec/
112
112
  ```
113
113
 
114
- CLI从本地JSON生成一个**新目录**,不会覆盖已有目录:
114
+ CLI支持本地JSON、HTTP(S) JSON链接和配置文件。默认创建新目录;重复生成显式加`--update`,只替换未经修改、无额外文件的CLI生成目录:
115
115
 
116
116
  ```sh
117
117
  npm run build:core
@@ -119,12 +119,60 @@ node bin/chainspec.mjs --in openapi.json --out ./new-sdk --namespace main --stri
119
119
  # --group-by tag / --base-url URL / --no-hooks
120
120
  ```
121
121
 
122
+ ### 一份 JSON 配置,网页与代码库共用
123
+
124
+ 网页的配置区直接编辑JSON,支持导入/导出,完整SDK ZIP也包含`chainspec.config.json`。配置格式版本`version: 1`与生成器npm版本独立。
125
+
126
+ ```json
127
+ {
128
+ "version": 1,
129
+ "input": "https://example.com/api-json",
130
+ "output": "./src/api",
131
+ "namespace": "main",
132
+ "groupBy": "tag",
133
+ "hooks": true,
134
+ "controllerNames": {
135
+ "短信管理": "sms"
136
+ }
137
+ }
138
+ ```
139
+
140
+ ```sh
141
+ npm install -D @batype/chainspec@^2.1.0
142
+ npx chainspec --config chainspec.config.json
143
+ # 接口更新后重新生成:
144
+ npx chainspec --config chainspec.config.json --update
145
+ # 也可直接从链接生成:
146
+ npx chainspec --in https://example.com/api-json --out ./new-sdk
147
+ ```
148
+
149
+ 加入你项目的package.json:`"sdk:generate": "chainspec --config chainspec.config.json --update"`,以后执行`npm run sdk:generate`即可。
150
+
151
+ `input`也可以是`./openapi.json`,在代码库保留对应文件即可。配置文件中的本地input/output路径相对于配置文件;显式CLI参数覆盖JSON,并相对于当前工作目录。还可配置`stripPrefix`、`baseUrl`。未知键及错误类型直接报错,配置文件不执行JavaScript。网页只读取当前导入的JSON,不根据本地路径自动读磁盘;配置中的URL需要点击“获取JSON”。输出目录只用于CLI。
152
+
153
+ 首个Tag保留为分组依据,中文标签仍保留在说明和搜索中。调用链只生成ASCII标识符:显式`controllerNames`优先,未映射的非ASCII分组从路径推导并提示;同一Tag保持同一分组,重名使用稳定后缀。建议为中文Tag显式映射并提交配置,让名字在路径变化后仍保持稳定。2.0已有中文调用名的SDK需重新生成并修改调用;原有英文别名与v2入参约定保持不变。
154
+
155
+ `--update`通过CLI生成的文件清单和SHA256检查所有生成文件;手改文件、额外文件、符号链接、未由CLI管理的旧目录会拒绝更新。自定义请求与业务代码放在SDK目录外。没有`--update`仍不会覆盖已有目录。
156
+
157
+ `path`与`query`可以同时存在。例如`/users/{id}/orders?page=1`:
158
+
159
+ ```ts
160
+ API.main.users.listOrders.request({
161
+ path: { id: '42' },
162
+ query: { page: 1 }
163
+ }, { headers: { 'X-Client': 'web' } });
164
+ // 如果文档还声明body:request({path, query}, body, options)。
165
+ // useRequest使用同样的位置;具体字段由文档推导。
166
+ ```
167
+
122
168
  Node生成器API:
123
169
 
124
170
  ```ts
125
- import {generateSDK} from '@batype/chainspec';
171
+ import {generateSDK, parseConfig, fetchDocument} from '@batype/chainspec';
126
172
  const project=generateSDK(jsonText,{namespace:'main',groupBy:'path',hooks:true});
127
173
  // project.files: 文件名→内容;operations与diagnostics供自定义工具展示
174
+ // const config = parseConfig(configJSON);
175
+ // const remote = generateSDK(await fetchDocument(config.input), config);
128
176
  ```
129
177
 
130
178
  已正式发布到[npm:@batype/chainspec](https://www.npmjs.com/package/@batype/chainspec),也可下载[GitHub Release tarball](https://github.com/batype/chainspec/releases)。
@@ -138,7 +186,7 @@ npx --yes --package=@batype/chainspec chainspec --in openapi.json --out ./new-sd
138
186
 
139
187
  ## 数据与安全边界
140
188
 
141
- 生产CSP `connect-src 'none'`;Worker只解析本地文档。仅主题持久化,无云历史或遥测。输入最大8MiB、5000操作/模型。清空后移除当前输入和结果。不要未经检查就公开包含业务模型、地址和枚举的生成SDK。仓库、截图和测试使用合成API;用户提供的文档不进入公开源码。
189
+ 只有点击获取链接或执行CLI时才GET指定的JSON;浏览器不携带Cookie/认证、不经过代理,目标站需允许CORS,HTTPS页面不能获取HTTP资源。生产CSP仅为URL导入允许HTTP(S)连接;Worker仍只解析文档,不获取外部引用。URL导入有20秒超时及8MiB流式限制,可取消。仅主题持久化,无云历史或遥测。输入最大8MiB、5000操作/模型。清空后移除当前输入和结果。不要未经检查就公开包含业务模型、地址和枚举的生成SDK。仓库、截图和测试使用合成API;用户提供的文档不进入公开源码。
142
190
 
143
191
  类型不验证服务端数据;不猜鉴权凭据;不保证复杂序列化和所有Schema约束。int64依然是JS number,需要精度时由服务器返回string。参阅[限制](docs/limits.md)。
144
192
 
package/bin/chainspec.mjs CHANGED
@@ -1,81 +1,95 @@
1
1
  #!/usr/bin/env node
2
- import { readFile, mkdir, writeFile, lstat, rm } from "node:fs/promises";
3
- import { resolve } from "node:path";
4
- import { generateSDK, MAX_BYTES, VERSION } from "../lib/core/index.js";
5
- const args = process.argv.slice(2),
6
- opts = {};
7
- let inputPath, output;
2
+ import { readFile, mkdir, writeFile, lstat, rm, readdir, rename } from "node:fs/promises";
3
+ import { resolve, dirname, basename, join } from "node:path";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { generateSDK, parseConfig, fetchDocument, MAX_BYTES, VERSION } from "../lib/core/index.js";
6
+ const args = process.argv.slice(2), opts = {};
7
+ const marker = ".chainspec-generated.json";
8
+ const digest = text => createHash("sha256").update(text).digest("hex");
9
+ let inputPath, output, configPath, update = false;
10
+ async function localJSON(path) {
11
+ const stat = await lstat(path);
12
+ if (!stat.isFile() || stat.size > MAX_BYTES)
13
+ throw Error("Input must be a regular JSON file of at most 8 MiB");
14
+ return new TextDecoder("utf-8", { fatal: true }).decode(await readFile(path));
15
+ }
16
+ async function exists(path) {
17
+ try { return await lstat(path); }
18
+ catch (e) { if (e.code === "ENOENT") return null; throw e; }
19
+ }
8
20
  try {
9
21
  if (args.includes("--help") || !args.length) {
10
- console.log(
11
- "chainspec --in openapi.json --out ./new-sdk [--namespace main] [--strip-prefix /v1/api] [--group-by path|tag] [--base-url URL] [--no-hooks]\nReads local JSON only; creates a new output directory, never overwrites it.",
12
- );
13
- process.exit(0);
14
- }
15
- if (args.includes("--version")) {
16
- console.log(VERSION);
22
+ console.log("chainspec --config chainspec.config.json [--update]\nchainspec --in openapi.json|https://example.com/api-json --out ./new-sdk [--namespace main] [--strip-prefix /v1/api] [--group-by path|tag] [--base-url URL] [--no-hooks]\nPaths in JSON resolve relative to the config file; explicit CLI flags override JSON.\nCreates a new directory. --update replaces only an unchanged CLI-generated directory.");
17
23
  process.exit(0);
18
24
  }
25
+ if (args.includes("--version")) { console.log(VERSION); process.exit(0); }
19
26
  for (let i = 0; i < args.length; i++) {
20
27
  const flag = args[i];
21
- if (flag === "--no-hooks") {
22
- opts.hooks = false;
23
- continue;
24
- }
25
- if (
26
- ![
27
- "--in",
28
- "--out",
29
- "--namespace",
30
- "--strip-prefix",
31
- "--group-by",
32
- "--base-url",
33
- ].includes(flag)
34
- )
28
+ if (flag === "--no-hooks") { opts.hooks = false; continue; }
29
+ if (flag === "--update") { update = true; continue; }
30
+ if (!["--config", "--in", "--out", "--namespace", "--strip-prefix", "--group-by", "--base-url"].includes(flag))
35
31
  throw Error("Unknown option; use --help");
36
32
  const val = args[++i];
37
33
  if (!val || val.startsWith("--")) throw Error("Missing option value");
38
- if (flag === "--in") inputPath = val;
34
+ if (flag === "--config") configPath = resolve(val);
35
+ else if (flag === "--in") inputPath = val;
39
36
  else if (flag === "--out") output = val;
40
- else
41
- opts[
42
- {
43
- "--namespace": "namespace",
44
- "--strip-prefix": "stripPrefix",
45
- "--group-by": "groupBy",
46
- "--base-url": "baseUrl",
47
- }[flag]
48
- ] = val;
37
+ else opts[{"--namespace":"namespace", "--strip-prefix":"stripPrefix", "--group-by":"groupBy", "--base-url":"baseUrl"}[flag]] = val;
49
38
  }
50
- if (!inputPath || !output) throw Error("--in and --out are required");
51
- if (opts.groupBy && !["path", "tag"].includes(opts.groupBy))
52
- throw Error("--group-by must be path or tag");
53
- const stat = await lstat(inputPath);
54
- if (!stat.isFile() || stat.size > MAX_BYTES)
55
- throw Error("Input must be a regular JSON file of at most 8 MiB");
56
- const result = generateSDK(
57
- new TextDecoder("utf-8", { fatal: true }).decode(await readFile(inputPath)),
58
- opts,
59
- );
60
- const dir = resolve(output);
61
- await mkdir(dir, { mode: 0o700 });
39
+ const config = configPath ? parseConfig(await localJSON(configPath)) : undefined;
40
+ const base = configPath ? dirname(configPath) : process.cwd();
41
+ const source = inputPath ?? config?.input;
42
+ const destination = output ? resolve(output) : config ? resolve(base, config.output) : undefined;
43
+ if (!source || !destination) throw Error("--in and --out, or --config, are required");
44
+ const settings = { ...config, ...opts };
45
+ if (settings.groupBy && !["path", "tag"].includes(settings.groupBy)) throw Error("--group-by must be path or tag");
46
+ const text = /^[a-z][a-z\d+.-]*:/i.test(source)
47
+ ? await fetchDocument(source)
48
+ : await localJSON(resolve(inputPath ? process.cwd() : base, source));
49
+ const result = generateSDK(text, settings);
50
+ const dir = destination;
51
+ const old = await exists(dir);
52
+ if (old) {
53
+ if (!update) throw Object.assign(Error("Output already exists; choose a new directory or use --update."), { code: "EEXIST" });
54
+ if (!old.isDirectory() || old.isSymbolicLink()) throw Error("Refusing to update a symlink or non-directory");
55
+ const ownerStat = await exists(join(dir, marker));
56
+ if (!ownerStat?.isFile() || ownerStat.isSymbolicLink()) throw Error("Output is not managed by this CLI; choose a new directory");
57
+ const owner = JSON.parse(await localJSON(join(dir, marker)));
58
+ if (owner.generator !== "ChainSpec" || !owner.files || typeof owner.files !== "object" || Array.isArray(owner.files))
59
+ throw Error("Invalid generated-directory marker");
60
+ const names = await readdir(dir);
61
+ if (names.some(name => name !== marker && !Object.hasOwn(owner.files, name)))
62
+ throw Error("Output contains extra files; move custom code outside the generated directory");
63
+ for (const [name, hash] of Object.entries(owner.files)) {
64
+ if (name !== basename(name) || name === marker || !names.includes(name)) throw Error("Generated files changed; choose a new directory");
65
+ const stat = await lstat(join(dir, name));
66
+ if (!stat.isFile() || stat.isSymbolicLink() || digest(await readFile(join(dir, name))) !== hash)
67
+ throw Error("Generated files changed; choose a new directory to preserve edits");
68
+ }
69
+ }
70
+ await mkdir(dirname(dir), { recursive: true });
71
+ const stage = join(dirname(dir), `.${basename(dir)}.chainspec-${randomUUID()}`);
72
+ await mkdir(stage, { mode: 0o700 });
62
73
  try {
74
+ const files = Object.fromEntries(Object.entries(result.files).map(([name, text]) => [name, digest(text)]));
63
75
  for (const [file, text] of Object.entries(result.files))
64
- await writeFile(resolve(dir, file), text, { flag: "wx", mode: 0o600 });
65
- } catch (e) {
66
- await rm(dir, { recursive: true, force: true });
67
- throw e;
68
- }
69
- console.log(
70
- `Generated ${result.operations.length} operations, ${result.schemaCount} schemas, ${result.diagnostics.length} diagnostics.\nOutput: ${dir}`,
71
- );
76
+ await writeFile(join(stage, file), text, { flag: "wx", mode: 0o600 });
77
+ await writeFile(join(stage, marker), JSON.stringify({ generator: "ChainSpec", version: VERSION, files }, null, 2), { flag: "wx", mode: 0o600 });
78
+ if (old) {
79
+ const backup = `${stage}.previous`;
80
+ await rename(dir, backup);
81
+ try { await rename(stage, dir); }
82
+ catch (e) { await rename(backup, dir); throw e; }
83
+ await rm(backup, { recursive: true });
84
+ } else {
85
+ // Reserve destination first so a concurrently created directory is never replaced.
86
+ await mkdir(dir, { mode: 0o700 });
87
+ try { for (const name of await readdir(stage)) await rename(join(stage, name), join(dir, name)); }
88
+ catch (e) { await rm(dir, { recursive: true, force: true }); throw e; }
89
+ }
90
+ } finally { await rm(stage, { recursive: true, force: true }); }
91
+ console.log(`Generated ${result.operations.length} operations, ${result.schemaCount} schemas, ${result.diagnostics.length} diagnostics.\nOutput: ${dir}`);
72
92
  } catch (error) {
73
- console.error(
74
- error?.code === "EEXIST"
75
- ? "Output already exists; choose a new directory."
76
- : error instanceof Error
77
- ? error.message
78
- : "Generation failed",
79
- );
93
+ console.error(error?.code === "EEXIST" ? "Output already exists; choose a new directory or use --update." : error instanceof Error ? error.message : "Generation failed");
80
94
  process.exitCode = 1;
81
95
  }
@@ -0,0 +1,10 @@
1
+ import type { GeneratorOptions } from "./model.js";
2
+ /** Shared by the web editor and CLI. Paths resolve relative to the config file. */
3
+ export type ChainSpecConfig = GeneratorOptions & {
4
+ version: 1;
5
+ input: string;
6
+ output: string;
7
+ };
8
+ export declare const DEFAULT_CONFIG: ChainSpecConfig;
9
+ export declare function parseConfig(input: string | unknown): ChainSpecConfig;
10
+ export declare function documentURL(input: string): URL;
@@ -0,0 +1,67 @@
1
+ export const DEFAULT_CONFIG = {
2
+ version: 1,
3
+ input: "./openapi.json",
4
+ output: "./src/api",
5
+ namespace: "main",
6
+ groupBy: "path",
7
+ hooks: true,
8
+ controllerNames: {},
9
+ };
10
+ const keys = new Set([
11
+ "version", "input", "output", "namespace", "groupBy", "stripPrefix",
12
+ "controllerNames", "baseUrl", "hooks",
13
+ ]);
14
+ export function parseConfig(input) {
15
+ let value = input;
16
+ if (typeof input === "string") {
17
+ try {
18
+ value = JSON.parse(input);
19
+ }
20
+ catch {
21
+ throw new Error("配置必须是有效 JSON");
22
+ }
23
+ }
24
+ if (!value || typeof value !== "object" || Array.isArray(value))
25
+ throw new Error("配置必须是 JSON 对象");
26
+ for (const key of Object.keys(value))
27
+ if (!keys.has(key))
28
+ throw new Error(`未知配置项:${key}`);
29
+ if (value.version !== 1)
30
+ throw new Error("配置 version 必须为 1");
31
+ for (const key of ["input", "output"])
32
+ if (typeof value[key] !== "string" || !value[key].trim())
33
+ throw new Error(`配置 ${key} 必须为非空字符串`);
34
+ for (const key of ["namespace", "stripPrefix", "baseUrl"])
35
+ if (value[key] !== undefined && typeof value[key] !== "string")
36
+ throw new Error(`配置 ${key} 必须为字符串`);
37
+ if (value.namespace !== undefined && !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value.namespace))
38
+ throw new Error("命名空间需为英文标识符");
39
+ if (value.groupBy !== undefined && !["path", "tag"].includes(value.groupBy))
40
+ throw new Error("groupBy 必须为 path 或 tag");
41
+ if (value.hooks !== undefined && typeof value.hooks !== "boolean")
42
+ throw new Error("hooks 必须为布尔值");
43
+ if (value.stripPrefix && !value.stripPrefix.startsWith("/"))
44
+ throw new Error("stripPrefix 必须以 / 开头");
45
+ if (value.controllerNames !== undefined) {
46
+ if (!value.controllerNames || typeof value.controllerNames !== "object" || Array.isArray(value.controllerNames))
47
+ throw new Error("controllerNames 必须为原分组到英文名称的对象");
48
+ for (const alias of Object.values(value.controllerNames))
49
+ if (typeof alias !== "string" || !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(alias))
50
+ throw new Error("controllerNames 的值必须为英文标识符,例如 sms");
51
+ }
52
+ if (/^[a-z][a-z\d+.-]*:/i.test(value.input))
53
+ documentURL(value.input);
54
+ return { ...value, controllerNames: { ...value.controllerNames } };
55
+ }
56
+ export function documentURL(input) {
57
+ let url;
58
+ try {
59
+ url = new URL(input);
60
+ }
61
+ catch {
62
+ throw new Error("请输入完整的 HTTP(S) JSON 地址");
63
+ }
64
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash)
65
+ throw new Error("文档地址仅支持 HTTP(S),不能包含用户名、密码或片段");
66
+ return url;
67
+ }
@@ -1,5 +1,8 @@
1
1
  import type { GeneratorOptions, GeneratedProject } from "./model.js";
2
2
  export type { GeneratorOptions, GeneratedProject, Diagnostic, Operation, } from "./model.js";
3
3
  export { MAX_BYTES, MAX_OPERATIONS } from "./model.js";
4
- export declare const VERSION = "2.0.0";
4
+ export { parseConfig, DEFAULT_CONFIG } from "./config.js";
5
+ export type { ChainSpecConfig } from "./config.js";
6
+ export { fetchDocument } from "./source.js";
7
+ export declare const VERSION = "2.1.0";
5
8
  export declare function generateSDK(input: string | Record<string, any>, options?: GeneratorOptions): GeneratedProject;
package/lib/core/index.js CHANGED
@@ -2,7 +2,9 @@ import { parseDocument, normalize } from "./parse.js";
2
2
  import { createSchemaWriter } from "./schema.js";
3
3
  import { RUNTIME_SOURCE, HOOKS_SOURCE, RUNTIME_LICENSE } from "./templates.js";
4
4
  export { MAX_BYTES, MAX_OPERATIONS } from "./model.js";
5
- export const VERSION = "2.0.0";
5
+ export { parseConfig, DEFAULT_CONFIG } from "./config.js";
6
+ export { fetchDocument } from "./source.js";
7
+ export const VERSION = "2.1.0";
6
8
  const q = JSON.stringify;
7
9
  const banner = "// Generated by ChainSpec. Do not edit; regenerate from your OpenAPI document.\n";
8
10
  export function generateSDK(input, options = {}) {
package/lib/core/parse.js CHANGED
@@ -14,12 +14,12 @@ const reserved = new Set(["__proto__", "prototype", "constructor", "then"]);
14
14
  export function nameOf(value, fallback = "endpoint") {
15
15
  const words = value
16
16
  .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
17
- .split(/[^\p{ID_Continue}$]+/u)
17
+ .split(/[^A-Za-z0-9_$]+/)
18
18
  .filter(Boolean);
19
19
  let result = words
20
20
  .map((w, i) => i ? w[0].toUpperCase() + w.slice(1) : w[0].toLowerCase() + w.slice(1))
21
21
  .join("") || fallback;
22
- if (!/^[\p{ID_Start}_$]/u.test(result))
22
+ if (!/^[A-Za-z_$]/.test(result))
23
23
  result = "_" + result;
24
24
  return reserved.has(result) ? result + "Api" : result;
25
25
  }
@@ -52,7 +52,7 @@ export function parseDocument(input) {
52
52
  }
53
53
  export function normalize(doc, options, diagnostics) {
54
54
  const namespace = options.namespace ?? "main";
55
- if (!/^[\p{ID_Start}_$][\p{ID_Continue}$]*$/u.test(namespace) ||
55
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(namespace) ||
56
56
  reserved.has(namespace))
57
57
  throw new Error("命名空间需是有效标识符,不能使用保留的对象属性");
58
58
  const candidates = [
@@ -71,6 +71,7 @@ export function normalize(doc, options, diagnostics) {
71
71
  const operations = [];
72
72
  const used = new Set();
73
73
  const groupSources = new Map();
74
+ const assignedGroups = new Map();
74
75
  for (const path of Object.keys(doc.paths).sort()) {
75
76
  if (!path.startsWith("/") || path.includes("?") || path.includes("#"))
76
77
  throw new Error("paths中的路径需以/开头且不能包含query/hash");
@@ -92,7 +93,16 @@ export function normalize(doc, options, diagnostics) {
92
93
  "default";
93
94
  const originalGroup = op["x-sdk-controller"] ??
94
95
  (options.groupBy === "tag" ? (tags[0] ?? pathGroup) : pathGroup);
95
- let controller = nameOf(options.controllerNames?.[originalGroup] ?? originalGroup, "default");
96
+ const alias = options.controllerNames?.[originalGroup];
97
+ if (alias !== undefined && !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(alias))
98
+ throw new Error(`分组「${originalGroup}」的别名需为英文标识符`);
99
+ let controller = assignedGroups.get(originalGroup) ?? nameOf(alias ?? (/^[\x00-\x7F]+$/.test(originalGroup) ? originalGroup : pathGroup), "group_" + hash(originalGroup));
100
+ if (!assignedGroups.has(originalGroup) && alias === undefined && /[^\x00-\x7F]/.test(originalGroup))
101
+ diagnostics.push({
102
+ level: "warning", code: "group-name-fallback",
103
+ message: `分组「${originalGroup}」使用路径推导名称 ${controller};可在 controllerNames 中显式映射`,
104
+ location: path,
105
+ });
96
106
  if (groupSources.has(controller) &&
97
107
  groupSources.get(controller) !== originalGroup) {
98
108
  controller += "_" + hash(originalGroup);
@@ -104,8 +114,10 @@ export function normalize(doc, options, diagnostics) {
104
114
  });
105
115
  }
106
116
  groupSources.set(controller, originalGroup);
117
+ assignedGroups.set(originalGroup, controller);
107
118
  const originalId = typeof op.operationId === "string" ? op.operationId : undefined;
108
- let name = nameOf(originalId ?? method + " " + stripped.replace(/\{([^}]+)\}/g, "by $1"));
119
+ const methodSource = method + " " + stripped.replace(/\{([^}]+)\}/g, "by $1");
120
+ let name = nameOf(originalId && /^[\x00-\x7F]+$/.test(originalId) ? originalId : methodSource);
109
121
  if (used.has(`${controller}.${name}`)) {
110
122
  name += "_" + method + "_" + hash(path);
111
123
  diagnostics.push({
@@ -0,0 +1,2 @@
1
+ /** Only called after an explicit URL import / CLI invocation. Never fetches refs. */
2
+ export declare function fetchDocument(input: string, signal?: AbortSignal): Promise<string>;
@@ -0,0 +1,43 @@
1
+ import { MAX_BYTES } from "./model.js";
2
+ import { parseDocument } from "./parse.js";
3
+ import { documentURL } from "./config.js";
4
+ /** Only called after an explicit URL import / CLI invocation. Never fetches refs. */
5
+ export async function fetchDocument(input, signal) {
6
+ const url = documentURL(input);
7
+ const abort = AbortSignal.any([AbortSignal.timeout(20_000), ...(signal ? [signal] : [])]);
8
+ const response = await fetch(url, {
9
+ method: "GET", credentials: "omit", referrerPolicy: "no-referrer",
10
+ headers: { Accept: "application/json" }, signal: abort,
11
+ });
12
+ if (!response.ok) {
13
+ await response.body?.cancel();
14
+ throw new Error(`获取文档失败:HTTP ${response.status}`);
15
+ }
16
+ if (Number(response.headers.get("content-length")) > MAX_BYTES) {
17
+ await response.body?.cancel();
18
+ throw new Error("远程文档最大 8 MiB");
19
+ }
20
+ if (!response.body)
21
+ throw new Error("远程文档为空");
22
+ const reader = response.body.getReader();
23
+ const decoder = new TextDecoder("utf-8", { fatal: true });
24
+ let bytes = 0, text = "";
25
+ try {
26
+ while (true) {
27
+ const { done, value } = await reader.read();
28
+ if (done)
29
+ break;
30
+ bytes += value.byteLength;
31
+ if (bytes > MAX_BYTES)
32
+ throw new Error("远程文档最大 8 MiB");
33
+ text += decoder.decode(value, { stream: true });
34
+ }
35
+ text += decoder.decode();
36
+ }
37
+ finally {
38
+ await reader.cancel().catch(() => { });
39
+ reader.releaseLock();
40
+ }
41
+ parseDocument(text);
42
+ return text;
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@batype/chainspec",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Generate typed chainable TypeScript SDKs and SWR hooks from OpenAPI JSON, locally.",
5
5
  "type": "module",
6
6
  "license": "MIT",