@batype/chainspec 1.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,10 +15,34 @@ 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
 
22
+ ## 2.0 调用约定
23
+
24
+ | 文档声明 | request / useRequest 参数 |
25
+ |---|---|
26
+ | 无请求体 | `(params, options)` |
27
+ | 有请求体 | `(params, body, options)` |
28
+
29
+ params包含`path/query/cookies`;`headers`统一放在options(标准HeadersInit),与signal、拦截器同级。是否有body取决于OpenAPI声明,而不是只看HTTP方法。没有path/query时用`{}`占位;可选body暂不传时,用`undefined`保留第二个位置。
30
+
31
+ ```ts
32
+ // GET:params + options
33
+ API.main.users.getUser.request({path:{id:'42'}},{headers:{'X-Client':'web'}});
34
+ // POST:params + body + options
35
+ API.main.users.createUser.request({}, {name:'Ada',email:'ada@example.com'}, {headers:{'X-Client':'web'}});
36
+ // React 写hook:绑定body后,在事件里trigger();也可trigger(newBody)覆盖。
37
+ const bound=API.main.users.createUser.useRequest({}, {name:'Ada',email:'ada@example.com'}, {headers:{'X-Client':'web'}});
38
+ // 或者暂不绑定body,在事件里deferred.trigger(body)。
39
+ const deferred=API.main.users.createUser.useRequest({}, undefined, {headers:{'X-Client':'web'}});
40
+ ```
41
+
42
+ GET/HEAD hook options中的enabled、swr与headers同级;不再使用v1的`options.request.headers`。写hook不自动发送请求,必填body未绑定时,trigger也必须提供body。无body的写操作使用`useRequest(params, options)`及`trigger()`。
43
+
44
+ 这是对v1生成SDK的破坏性变更。迁移步骤见[docs/migration-v2.md](docs/migration-v2.md)。
45
+
22
46
  ## 使用下载的SDK
23
47
 
24
48
  将sdk文件夹放进TypeScript工程。普通入口不依赖React或SWR:
@@ -49,8 +73,8 @@ function UserView(){
49
73
  return <span>{data?.name}</span>;
50
74
  }
51
75
  function Invite(){
52
- const {trigger,isMutating}=API.main.users.createUser.useRequest();
53
- return <button disabled={isMutating} onClick={()=>trigger({body:{name:'Ada',email:'ada@example.com'}})}>邀请</button>;
76
+ const {trigger,isMutating}=API.main.users.createUser.useRequest({}, undefined, {headers:{'X-Client':'web'}});
77
+ return <button disabled={isMutating} onClick={()=>trigger({name:'Ada',email:'ada@example.com'})}>邀请</button>;
54
78
  }
55
79
  ```
56
80
 
@@ -87,7 +111,7 @@ npm run dev
87
111
  # http://127.0.0.1:4184/chainspec/
88
112
  ```
89
113
 
90
- CLI从本地JSON生成一个**新目录**,不会覆盖已有目录:
114
+ CLI支持本地JSON、HTTP(S) JSON链接和配置文件。默认创建新目录;重复生成显式加`--update`,只替换未经修改、无额外文件的CLI生成目录:
91
115
 
92
116
  ```sh
93
117
  npm run build:core
@@ -95,19 +119,74 @@ node bin/chainspec.mjs --in openapi.json --out ./new-sdk --namespace main --stri
95
119
  # --group-by tag / --base-url URL / --no-hooks
96
120
  ```
97
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
+
98
168
  Node生成器API:
99
169
 
100
170
  ```ts
101
- import {generateSDK} from '@batype/chainspec';
171
+ import {generateSDK, parseConfig, fetchDocument} from '@batype/chainspec';
102
172
  const project=generateSDK(jsonText,{namespace:'main',groupBy:'path',hooks:true});
103
173
  // project.files: 文件名→内容;operations与diagnostics供自定义工具展示
174
+ // const config = parseConfig(configJSON);
175
+ // const remote = generateSDK(await fetchDocument(config.input), config);
176
+ ```
177
+
178
+ 已正式发布到[npm:@batype/chainspec](https://www.npmjs.com/package/@batype/chainspec),也可下载[GitHub Release tarball](https://github.com/batype/chainspec/releases)。
179
+
180
+ ```sh
181
+ npm install -D @batype/chainspec
182
+ npx --yes --package=@batype/chainspec chainspec --in openapi.json --out ./new-sdk --namespace main
104
183
  ```
105
184
 
106
- npm正式可用状态以官方注册表为准;未发布时可使用源码或[GitHub Release tarball](https://github.com/batype/chainspec/releases)。生成器npm入口不需要React、SWR或ZIP依赖。生成SDK自身的React入口才需要SWR。
185
+ 生成器npm入口不需要React、SWR或ZIP依赖。生成SDK自身的React入口才需要SWR。
107
186
 
108
187
  ## 数据与安全边界
109
188
 
110
- 生产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;用户提供的文档不进入公开源码。
111
190
 
112
191
  类型不验证服务端数据;不猜鉴权凭据;不保证复杂序列化和所有Schema约束。int64依然是JS number,需要精度时由服务器返回string。参阅[限制](docs/limits.md)。
113
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 = "1.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 = "1.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 = {}) {
@@ -27,16 +29,19 @@ export function generateSDK(input, options = {}) {
27
29
  types += "export interface Operations {\n";
28
30
  for (const op of operations) {
29
31
  const fields = [];
30
- for (const loc of ["path", "query", "header", "cookie"]) {
32
+ for (const loc of ["path", "query", "cookie"]) {
31
33
  const ps = op.parameters.filter((p) => p.in === loc);
32
34
  if (!ps.length)
33
35
  continue;
34
- const field = loc === "header" ? "headers" : loc === "cookie" ? "cookies" : loc;
36
+ const field = loc === "cookie" ? "cookies" : loc;
35
37
  fields.push(`${field}${ps.some((p) => p.required) ? "" : "?"}: { ${ps.map((p) => `${q(p.name)}${p.required ? "" : "?"}: ${writer.type(p.schema, "input", op.id + "." + loc + "." + p.name)};`).join(" ")} };`);
36
38
  }
37
- if (op.body)
38
- fields.push(`body${op.body.required ? "" : "?"}: ${writer.type(op.body.schema, "input", op.id + ".body")};`);
39
- types += ` ${q(op.id)}: { input: { ${fields.join(" ")} }; output: ${writer.type(op.response, "output", op.id + ".response")}; };\n`;
39
+ const body = op.body
40
+ ? writer.type(op.body.schema, "input", op.id + ".body")
41
+ : "never";
42
+ const headerParams = op.parameters.filter((p) => p.in === "header");
43
+ const headers = `{ ${headerParams.map((p) => `${q(p.name)}${p.required ? "" : "?"}: ${writer.type(p.schema, "input", op.id + ".header." + p.name)};`).join(" ")} }`;
44
+ types += ` ${q(op.id)}: { params: ${fields.length ? `{ ${fields.join(" ")} }` : "Record<string, never>"}; body: ${body}; headers: ${headers}; output: ${writer.type(op.response, "output", op.id + ".response")}; };\n`;
40
45
  }
41
46
  types += "}\n";
42
47
  const groups = new Map();
@@ -61,7 +66,7 @@ export function generateSDK(input, options = {}) {
61
66
  ? "QueryEndpoint"
62
67
  : "MutationEndpoint"
63
68
  : "Endpoint";
64
- text += `${op.deprecated ? "/** @deprecated */\n" : ""} ${q(op.name)}: ${ep}<${index(op.id, "input")},${index(op.id, "output")}>;\n`;
69
+ text += `${op.deprecated ? "/** @deprecated */\n" : ""} ${q(op.name)}: ${ep}<${index(op.id, "params")},${index(op.id, "body")},${index(op.id, "output")},${Boolean(op.body)},${op.body?.required ?? false}>;\n`;
65
70
  }
66
71
  text += " };\n";
67
72
  }
@@ -77,11 +82,12 @@ export function generateSDK(input, options = {}) {
77
82
  controller: op.controller,
78
83
  name: op.name,
79
84
  parameters: op.parameters.map(({ schema, ...p }) => p),
85
+ hasBody: Boolean(op.body),
80
86
  bodyRequired: op.body?.required,
81
87
  mediaType: op.body?.mediaType,
82
88
  responseType: op.responseType,
83
89
  };
84
- let endpoint = `client.endpoint<${index(op.id, "input")},${index(op.id, "output")}>(${q(definition)})`;
90
+ let endpoint = `client.endpoint<${index(op.id, "params")},${index(op.id, "body")},${index(op.id, "output")},${Boolean(op.body)},${op.body?.required ?? false}>(${q(definition)})`;
85
91
  if (react)
86
92
  endpoint = `${op.method === "GET" || op.method === "HEAD" ? "withQuery" : "withMutation"}(${endpoint})`;
87
93
  text += `${q(op.name)}: ${endpoint},\n`;
@@ -120,7 +126,7 @@ export function generateSDK(input, options = {}) {
120
126
  const first = operations.find((o) => o.method === "GET") ?? operations[0];
121
127
  const access = `API.${namespace}[${q(first.controller)}][${q(first.name)}]`;
122
128
  files["README.md"] =
123
- `# Generated SDK\n\nGenerated locally with ChainSpec ${VERSION}. Copy these files into your TypeScript project. No runtime code evaluation or request is performed by the generator.\n\n## Setup\n\nNode22+ or a browser with Fetch, Headers, FormData, Blob. TypeScript moduleResolution Bundler or NodeNext; .js imports resolve to the corresponding .ts sources.\n\nPlain SDK: import {createAPI} from './index.js'. No React/SWR dependency.\n\n${options.hooks === false ? "" : "React SDK: npm install swr@^2.5.1 react@^19.3.1, then import {createReactAPI} from './react.js'. Use the returned API as a module-level singleton (or memoize it per Provider/SSR request). Do not create clients on every render.\n\n"}## Calls\n\n\`\`\`ts\nimport {createAPI} from './index.js';\nconst API=createAPI({baseUrl:${q(baseUrl)},cacheScope:()=> 'tenant:user:auth-revision'});\n// Inspect generated Operations input before calling:\n// ${access}.request({query:{...},path:{...},body:{...}});\n\`\`\`\n\n.request() returns the documented response body. Input sections are path/query/headers/cookies/body. Required arguments are required in TypeScript and checked for presence by the default runtime. Schema validation is NOT performed. Missing response schemas are unknown. JS numbers can lose int64 precision: ask your server for strings if required.\n\nGET/HEAD .useRequest(input, {enabled,request,swr}) reads with SWR; null input or enabled:false pauses it. Other methods .useRequest({request,swr}) (also .useMutation) return trigger(input), never send automatically. Hooks must be called unconditionally at React component/custom-hook top level. HEAD returns no body in Fetch.\n\nCache keys isolate client instances, base URL, operation, inputs, request headers and cacheScope. Change cacheScope after switching users/tenants or changing authorization within one client. Do not put tokens in cacheScope. Logout should also clear your SWR Provider cache. Request interceptors do not automatically change cache keys.\n\n## Extension points\n\n- request: async context => ({data,status,headers:new Headers()}) replaces the entire Fetch transport. context includes URL, method, headers, structured body, mediaType, operation and AbortSignal. Custom adapters must preserve the declared response shape; returning an Axios response unchanged is NOT compatible (normalize response.headers).\n- beforeRequest: async context => context (or void); global handlers run before per-call handlers. Add auth headers, tracing, or cancellation here.\n- afterResponse: async (response,context) => response (or void), for successful responses only. Unwrapping changes the schema contract; do not unwrap unless the server schema already describes the unwrapped body.\n- onError: async (error,context) => void; errors are rethrown. Non-2xx status becomes ApiError with status/data/headers. Network and AbortError are preserved. No automatic retry in .request(). SWR has its own configurable retries.\n\nSupported query serialization: form (arrays and flat objects), spaceDelimited, pipeDelimited, flat deepObject. Path/header simple serialization; header objects and nested query objects need a custom serializer. Cookie parameters require a custom Node request adapter (Fetch in browsers cannot set Cookie headers). JSON, flat multipart and URL-encoded bodies supported; nested forms need a custom adapter. JSON-schema constraints, auth schemes, oneOf exclusivity, server variables, parameter serialization and streaming may need application validation.\n\nThe generator supports local refs, not remote refs. Review manifest.json diagnostics, media choices and operation names. Do not add this SDK or its spec to a public repository without checking that disclosure is intended.\n`;
129
+ `# Generated SDK\n\nGenerated locally with ChainSpec ${VERSION}. Copy these files into your TypeScript project. No runtime code evaluation or request is performed by the generator.\n\n## Setup\n\nNode22+ or a browser with Fetch, Headers, FormData, Blob. TypeScript moduleResolution Bundler or NodeNext; .js imports resolve to the corresponding .ts sources.\n\nPlain SDK: import {createAPI} from './index.js'. No React/SWR dependency.\n\n${options.hooks === false ? "" : "React SDK: npm install swr@^2.5.1 react@^19.3.1, then import {createReactAPI} from './react.js'. Use the returned API as a module-level singleton (or memoize it per Provider/SSR request). Do not create clients on every render.\n\n"}## Calls\n\n\`\`\`ts\nimport {createAPI} from './index.js';\nconst API=createAPI({baseUrl:${q(baseUrl)},cacheScope:()=> 'tenant:user:auth-revision'});\n// Inspect generated Operations params/body before calling:\n// ${access}.request(params${first.body ? ", body" : ""}, options);\n\`\`\`\n\n.request() returns the documented response body. The first params object contains path/query/cookies. With a documented request body: request(params, body, options). Without one: request(params?, options). Headers are options.headers (standard HeadersInit) and may also be set globally or by beforeRequest. Empty params are {} when a body is present. Operations exposes params/body/headers/output types. Required arguments are required in TypeScript and checked for presence by the default runtime. Schema validation is NOT performed. Missing response schemas are unknown. JS numbers can lose int64 precision: ask your server for strings if required.\n\nGET/HEAD .useRequest(params, {enabled,headers,signal,swr}) reads with SWR; null params or enabled:false pauses it. If a query operation documents a body, use the same three positions as request. Other methods .useRequest(params, body, {headers,signal,swr}) (also .useMutation) bind params and an optional body, never send automatically. trigger() uses the bound body; trigger(body) supplies or overrides it. If a required body was not bound, TypeScript requires trigger(body). For operations without a body use .useRequest(params, options) then trigger(). Trigger options, such as throwOnError, are its second argument. Hook options are flat: headers/signal/interceptors are alongside swr, not under request. Hooks must be called unconditionally at React component/custom-hook top level. HEAD returns no body in Fetch.\n\nCache keys isolate client instances, base URL, operation, inputs, request headers and cacheScope. Change cacheScope after switching users/tenants or changing authorization within one client. Do not put tokens in cacheScope. Logout should also clear your SWR Provider cache. Request interceptors do not automatically change cache keys.\n\n## Extension points\n\n- request: async context => ({data,status,headers:new Headers()}) replaces the entire Fetch transport. context includes URL, method, headers, structured body, mediaType, operation and AbortSignal. Custom adapters must preserve the declared response shape; returning an Axios response unchanged is NOT compatible (normalize response.headers).\n- beforeRequest: async context => context (or void); global handlers run before per-call handlers. Add auth headers, tracing, or cancellation here.\n- afterResponse: async (response,context) => response (or void), for successful responses only. Unwrapping changes the schema contract; do not unwrap unless the server schema already describes the unwrapped body.\n- onError: async (error,context) => void; errors are rethrown. Non-2xx status becomes ApiError with status/data/headers. Network and AbortError are preserved. No automatic retry in .request(). SWR has its own configurable retries.\n\nSupported query serialization: form (arrays and flat objects), spaceDelimited, pipeDelimited, flat deepObject. Path/header simple serialization; header objects and nested query objects need a custom serializer. Cookie parameters require a custom Node request adapter (Fetch in browsers cannot set Cookie headers). JSON, flat multipart and URL-encoded bodies supported; nested forms need a custom adapter. JSON-schema constraints, auth schemes, oneOf exclusivity, server variables, parameter serialization and streaming may need application validation.\n\nThe generator supports local refs, not remote refs. Review manifest.json diagnostics, media choices and operation names. Do not add this SDK or its spec to a public repository without checking that disclosure is intended.\n`;
124
130
  diagnostics.push({
125
131
  level: "info",
126
132
  code: "static-types",
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
+ }
@@ -1,3 +1,3 @@
1
- export declare const RUNTIME_SOURCE = "/** ChainSpec runtime. Generated SDK types describe the spec, not runtime validation. */\nexport type Parameter = {\n name: string;\n in: \"path\" | \"query\" | \"header\" | \"cookie\";\n required?: boolean;\n style?: string;\n explode?: boolean;\n};\nexport type Definition = {\n id: string;\n method: string;\n path: string;\n controller: string;\n name: string;\n parameters: Parameter[];\n bodyRequired?: boolean;\n mediaType?: string;\n responseType: \"json\" | \"text\" | \"blob\" | \"void\" | \"auto\";\n};\nexport type Input = {\n path?: Record<string, unknown>;\n query?: Record<string, unknown>;\n headers?: Record<string, unknown>;\n cookies?: Record<string, unknown>;\n body?: unknown;\n};\nexport interface RequestContext {\n url: string;\n method: string;\n headers: Headers;\n body: unknown;\n mediaType?: string;\n signal?: AbortSignal;\n readonly operation: Definition;\n}\nexport interface TransportResponse {\n data: unknown;\n status: number;\n headers: Headers;\n}\nexport type RequestAdapter = (\n context: RequestContext,\n) => Promise<TransportResponse>;\nexport type BeforeRequest = (\n context: RequestContext,\n) => void | RequestContext | Promise<void | RequestContext>;\nexport type AfterResponse = (\n response: TransportResponse,\n context: RequestContext,\n) => void | TransportResponse | Promise<void | TransportResponse>;\nexport type OnError = (\n error: unknown,\n context: RequestContext | undefined,\n) => void | Promise<void>;\nexport interface RequestOptions {\n signal?: AbortSignal;\n headers?: HeadersInit;\n beforeRequest?: BeforeRequest[];\n afterResponse?: AfterResponse[];\n onError?: OnError[];\n}\nexport interface ClientConfig {\n baseUrl?: string;\n headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);\n request?: RequestAdapter;\n beforeRequest?: BeforeRequest[];\n afterResponse?: AfterResponse[];\n onError?: OnError[];\n /** Change on tenant/user/auth changes. Never put bearer tokens here. */ cacheScope?:\n | string\n | (() => string);\n}\nexport type RequestArgs<I> = {} extends I\n ? [input?: I, options?: RequestOptions]\n : [input: I, options?: RequestOptions];\nexport interface Endpoint<I, R> {\n readonly definition: Definition;\n request(...args: RequestArgs<I>): Promise<R>;\n getKey(input?: I, options?: RequestOptions): readonly unknown[];\n}\nexport class ApiError extends Error {\n constructor(\n public status: number,\n public data: unknown,\n public headers: Headers,\n ) {\n super(`HTTP ${status}`);\n this.name = \"ApiError\";\n }\n}\nconst scalar = (value: unknown): string => {\n if (value === null) return \"\";\n if ([\"string\", \"number\", \"boolean\"].includes(typeof value))\n return String(value);\n throw new Error(\"Parameter must be a scalar or a declared array/object\");\n};\nconst isRecord = (v: unknown): v is Record<string, unknown> =>\n typeof v === \"object\" && v !== null && !Array.isArray(v);\nfunction queryEntries(\n name: string,\n value: unknown,\n style = \"form\",\n explode = true,\n): [string, string][] {\n if (value === undefined) return [];\n if (style === \"deepObject\") {\n if (!isRecord(value)) throw new Error(\"deepObject needs an object\");\n return Object.entries(value)\n .filter(([, v]) => v !== undefined)\n .map(([k, v]) => [`${name}[${k}]`, scalar(v)]);\n }\n if (Array.isArray(value)) {\n if (style === \"form\" && explode) return value.map((v) => [name, scalar(v)]);\n const delimiter =\n style === \"spaceDelimited\" ? \" \" : style === \"pipeDelimited\" ? \"|\" : \",\";\n return [[name, value.map(scalar).join(delimiter)]];\n }\n if (isRecord(value)) {\n const values = Object.entries(value).filter(([, v]) => v !== undefined);\n return explode\n ? values.map(([k, v]) => [k, scalar(v)])\n : [[name, values.flatMap(([k, v]) => [k, scalar(v)]).join(\",\")]];\n }\n return [[name, scalar(value)]];\n}\nfunction pathValue(value: unknown, explode = false): string {\n const enc = (v: unknown) => encodeURIComponent(scalar(v));\n if (Array.isArray(value)) return value.map(enc).join(\",\");\n if (isRecord(value))\n return Object.entries(value)\n .map(([k, v]) =>\n explode ? `${enc(k)}=${enc(v)}` : `${enc(k)},${enc(v)}`,\n )\n .join(\",\");\n return enc(value);\n}\nexport async function defaultRequest(\n ctx: RequestContext,\n): Promise<TransportResponse> {\n const headers = new Headers(ctx.headers);\n let body: BodyInit | undefined;\n if (ctx.body !== undefined) {\n if (ctx.method === \"GET\" || ctx.method === \"HEAD\")\n throw new Error(\n \"Fetch cannot send GET/HEAD bodies; configure a request adapter\",\n );\n if (ctx.mediaType?.includes(\"json\")) {\n body = JSON.stringify(ctx.body);\n if (!headers.has(\"Content-Type\"))\n headers.set(\"Content-Type\", ctx.mediaType);\n } else if (ctx.mediaType?.startsWith(\"multipart/form-data\")) {\n if (!isRecord(ctx.body))\n throw new Error(\"Multipart body must be an object\");\n const form = new FormData();\n for (const [name, value] of Object.entries(ctx.body)) {\n if (value === undefined) continue;\n for (const v of Array.isArray(value) ? value : [value])\n form.append(name, v instanceof Blob ? v : scalar(v));\n }\n body = form;\n headers.delete(\"Content-Type\");\n } else if (ctx.mediaType?.startsWith(\"application/x-www-form-urlencoded\")) {\n if (!isRecord(ctx.body)) throw new Error(\"Form body must be an object\");\n const form = new URLSearchParams();\n for (const [name, value] of Object.entries(ctx.body))\n for (const [k, v] of queryEntries(name, value)) form.append(k, v);\n body = form.toString();\n if (!headers.has(\"Content-Type\"))\n headers.set(\"Content-Type\", ctx.mediaType);\n } else if (\n typeof ctx.body === \"string\" ||\n ctx.body instanceof Blob ||\n ctx.body instanceof ArrayBuffer ||\n ArrayBuffer.isView(ctx.body)\n ) {\n body = ctx.body as BodyInit;\n if (ctx.mediaType && !headers.has(\"Content-Type\"))\n headers.set(\"Content-Type\", ctx.mediaType);\n } else\n throw new Error(\"Unsupported body format; configure a request adapter\");\n }\n const response = await fetch(ctx.url, {\n method: ctx.method,\n headers,\n body,\n signal: ctx.signal,\n });\n let data: unknown;\n const mode = ctx.operation.responseType;\n if (\n response.status === 204 ||\n response.status === 205 ||\n ctx.method === \"HEAD\"\n )\n data = undefined;\n else if (!response.ok) {\n const text = await response.text();\n try {\n data = JSON.parse(text);\n } catch {\n data = text;\n }\n } else if (mode === \"void\") data = undefined;\n else if (\n mode === \"blob\" ||\n (mode === \"auto\" &&\n !response.headers.get(\"Content-Type\")?.includes(\"json\") &&\n !response.headers.get(\"Content-Type\")?.startsWith(\"text/\"))\n )\n data = await response.blob();\n else {\n const text = await response.text();\n data =\n mode === \"json\" ||\n (mode === \"auto\" &&\n response.headers.get(\"Content-Type\")?.includes(\"json\"))\n ? text\n ? JSON.parse(text)\n : undefined\n : text;\n }\n return { data, status: response.status, headers: response.headers };\n}\nconst runtimeId = Math.random().toString(36).slice(2);\nlet nextClient = 0;\nexport function createClient(config: ClientConfig = {}) {\n const clientId = `chainspec-${runtimeId}-${++nextClient}`;\n const base = config.baseUrl ?? \"\";\n const before = [...(config.beforeRequest ?? [])],\n after = [...(config.afterResponse ?? [])],\n errors = [...(config.onError ?? [])];\n return {\n endpoint<I, R>(definition: Definition): Endpoint<I, R> {\n const def = Object.freeze({\n ...definition,\n parameters: definition.parameters.map((p) => Object.freeze({ ...p })),\n });\n async function execute(\n input: Input = {},\n options: RequestOptions = {},\n ): Promise<R> {\n let context: RequestContext | undefined;\n try {\n let path = def.path;\n for (const p of def.parameters.filter((p) => p.in === \"path\")) {\n const value = input.path?.[p.name];\n if (value === undefined || value === null)\n throw new Error(`Missing path parameter: ${p.name}`);\n path = path.split(`{${p.name}}`).join(pathValue(value, p.explode));\n }\n if (/\\{[^}]+\\}/.test(path))\n throw new Error(\"Unresolved path parameter\");\n const query = new URLSearchParams();\n const headers = new Headers(\n typeof config.headers === \"function\"\n ? await config.headers()\n : config.headers,\n );\n new Headers(options.headers).forEach((v, k) => headers.set(k, v));\n for (const p of def.parameters.filter((p) => p.in !== \"path\")) {\n const bag =\n p.in === \"query\"\n ? input.query\n : p.in === \"header\"\n ? input.headers\n : input.cookies;\n const value = bag?.[p.name];\n if (p.required && (value === undefined || value === null))\n throw new Error(`Missing ${p.in} parameter: ${p.name}`);\n if (value === undefined) continue;\n if (p.in === \"query\")\n for (const [k, v] of queryEntries(\n p.name,\n value,\n p.style,\n p.explode ?? true,\n ))\n query.append(k, v);\n else if (p.in === \"header\")\n headers.set(\n p.name,\n Array.isArray(value)\n ? value.map(scalar).join(\",\")\n : scalar(value),\n );\n else if (config.request)\n headers.append(\n \"Cookie\",\n `${p.name}=${encodeURIComponent(scalar(value))}`,\n );\n else\n throw new Error(\n \"Cookie parameters require a custom Node request adapter; browser Fetch cannot set Cookie headers\",\n );\n }\n if (def.bodyRequired && input.body === undefined)\n throw new Error(\"Request body is required\");\n const url = `${base.replace(/\\/$/, \"\")}/${path.replace(/^\\//, \"\")}${query.size ? `?${query}` : \"\"}`;\n context = {\n url,\n method: def.method,\n headers,\n body: input.body,\n mediaType: def.mediaType,\n signal: options.signal,\n operation: def,\n };\n for (const fn of [...before, ...(options.beforeRequest ?? [])])\n context = (await fn(context)) ?? context;\n let response = await (config.request ?? defaultRequest)(context);\n if (\n !Number.isInteger(response.status) ||\n response.status < 100 ||\n response.status > 599\n )\n throw new Error(\"Adapter must return {data,status,headers}\");\n if (response.status < 200 || response.status >= 300)\n throw new ApiError(\n response.status,\n response.data,\n response.headers,\n );\n for (const fn of [...after, ...(options.afterResponse ?? [])])\n response = (await fn(response, context)) ?? response;\n return response.data as R;\n } catch (error) {\n for (const fn of [...errors, ...(options.onError ?? [])])\n await fn(error, context);\n throw error;\n }\n }\n return {\n definition: def,\n request: execute as Endpoint<I, R>[\"request\"],\n getKey(input?: I, options?: RequestOptions) {\n const scope =\n typeof config.cacheScope === \"function\"\n ? config.cacheScope()\n : (config.cacheScope ?? \"\");\n return [\n clientId,\n base,\n scope,\n def.id,\n input ?? {},\n Array.from(new Headers(options?.headers).entries()),\n ];\n },\n };\n },\n };\n}\n";
2
- export declare const HOOKS_SOURCE = "import useSWR, { type SWRConfiguration, type SWRResponse } from \"swr\";\nimport useSWRMutation, {\n type SWRMutationConfiguration,\n type SWRMutationResponse,\n} from \"swr/mutation\";\nimport type { Endpoint, RequestArgs, RequestOptions } from \"./runtime.js\";\nexport type QueryOptions<R> = {\n enabled?: boolean;\n request?: RequestOptions;\n swr?: SWRConfiguration<R, Error>;\n};\nexport type QueryArgs<I, R> = {} extends I\n ? [input?: I | null, options?: QueryOptions<R>]\n : [input: I | null, options?: QueryOptions<R>];\nexport type MutationOptions<I, R> = {\n request?: RequestOptions;\n swr?: SWRMutationConfiguration<R, Error, readonly unknown[], I>;\n};\nexport type QueryEndpoint<I, R> = Endpoint<I, R> & {\n useRequest(...args: QueryArgs<I, R>): SWRResponse<R, Error>;\n};\nexport type MutationEndpoint<I, R> = Endpoint<I, R> & {\n useRequest(\n options?: MutationOptions<I, R>,\n ): SWRMutationResponse<R, Error, readonly unknown[], I>;\n useMutation(\n options?: MutationOptions<I, R>,\n ): SWRMutationResponse<R, Error, readonly unknown[], I>;\n};\nexport function withQuery<I, R>(endpoint: Endpoint<I, R>): QueryEndpoint<I, R> {\n return Object.assign(endpoint, {\n useRequest(...[input, options]: QueryArgs<I, R>) {\n const enabled = input !== null && options?.enabled !== false;\n return useSWR<R, Error>(\n enabled ? endpoint.getKey(input ?? undefined, options?.request) : null,\n () =>\n endpoint.request(\n ...([input ?? undefined, options?.request] as RequestArgs<I>),\n ),\n options?.swr,\n );\n },\n });\n}\nexport function withMutation<I, R>(\n endpoint: Endpoint<I, R>,\n): MutationEndpoint<I, R> {\n function useMutation(options?: MutationOptions<I, R>) {\n return useSWRMutation<R, Error, readonly unknown[], I>(\n endpoint.getKey(undefined, options?.request),\n (_key, { arg }) =>\n endpoint.request(...([arg, options?.request] as RequestArgs<I>)),\n options?.swr,\n );\n }\n return Object.assign(endpoint, { useRequest: useMutation, useMutation });\n}\n";
1
+ export declare const RUNTIME_SOURCE = "/** ChainSpec runtime. Generated SDK types describe the spec, not runtime validation. */\nexport type Parameter = {\n name: string;\n in: \"path\" | \"query\" | \"header\" | \"cookie\";\n required?: boolean;\n style?: string;\n explode?: boolean;\n};\nexport type Definition = {\n id: string;\n method: string;\n path: string;\n controller: string;\n name: string;\n parameters: Parameter[];\n hasBody: boolean;\n bodyRequired?: boolean;\n mediaType?: string;\n responseType: \"json\" | \"text\" | \"blob\" | \"void\" | \"auto\";\n};\nexport type Input = {\n path?: Record<string, unknown>;\n query?: Record<string, unknown>;\n cookies?: Record<string, unknown>;\n};\nexport interface RequestContext {\n url: string;\n method: string;\n headers: Headers;\n body: unknown;\n mediaType?: string;\n signal?: AbortSignal;\n readonly operation: Definition;\n}\nexport interface TransportResponse {\n data: unknown;\n status: number;\n headers: Headers;\n}\nexport type RequestAdapter = (\n context: RequestContext,\n) => Promise<TransportResponse>;\nexport type BeforeRequest = (\n context: RequestContext,\n) => void | RequestContext | Promise<void | RequestContext>;\nexport type AfterResponse = (\n response: TransportResponse,\n context: RequestContext,\n) => void | TransportResponse | Promise<void | TransportResponse>;\nexport type OnError = (\n error: unknown,\n context: RequestContext | undefined,\n) => void | Promise<void>;\nexport interface RequestOptions {\n signal?: AbortSignal;\n headers?: HeadersInit;\n beforeRequest?: BeforeRequest[];\n afterResponse?: AfterResponse[];\n onError?: OnError[];\n}\nexport interface ClientConfig {\n baseUrl?: string;\n headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);\n request?: RequestAdapter;\n beforeRequest?: BeforeRequest[];\n afterResponse?: AfterResponse[];\n onError?: OnError[];\n /** Change on tenant/user/auth changes. Never put bearer tokens here. */ cacheScope?:\n | string\n | (() => string);\n}\nexport type RequestArgs<\n P,\n B,\n H extends boolean,\n Required extends boolean,\n> = H extends true\n ? Required extends true\n ? [params: P, body: B, options?: RequestOptions]\n : [params: P, body?: B, options?: RequestOptions]\n : {} extends P\n ? [params?: P, options?: RequestOptions]\n : [params: P, options?: RequestOptions];\nexport interface Endpoint<\n P,\n B,\n R,\n H extends boolean = false,\n Required extends boolean = false,\n> {\n readonly definition: Definition;\n request(...args: RequestArgs<P, B, H, Required>): Promise<R>;\n getKey(...args: RequestArgs<P, B, H, Required>): readonly unknown[];\n}\n/** Body position is fixed by the generated operation contract, never inferred from object keys. */\nexport function splitArgs(\n definition: Definition,\n args: readonly unknown[],\n): { params: Input; body: unknown; options: RequestOptions } {\n return {\n params: (args[0] ?? {}) as Input,\n body: definition.hasBody ? args[1] : undefined,\n options: (args[definition.hasBody ? 2 : 1] ?? {}) as RequestOptions,\n };\n}\nexport class ApiError extends Error {\n constructor(\n public status: number,\n public data: unknown,\n public headers: Headers,\n ) {\n super(`HTTP ${status}`);\n this.name = \"ApiError\";\n }\n}\nconst scalar = (value: unknown): string => {\n if (value === null) return \"\";\n if ([\"string\", \"number\", \"boolean\"].includes(typeof value))\n return String(value);\n throw new Error(\"Parameter must be a scalar or a declared array/object\");\n};\nconst isRecord = (v: unknown): v is Record<string, unknown> =>\n typeof v === \"object\" && v !== null && !Array.isArray(v);\nfunction queryEntries(\n name: string,\n value: unknown,\n style = \"form\",\n explode = true,\n): [string, string][] {\n if (value === undefined) return [];\n if (style === \"deepObject\") {\n if (!isRecord(value)) throw new Error(\"deepObject needs an object\");\n return Object.entries(value)\n .filter(([, v]) => v !== undefined)\n .map(([k, v]) => [`${name}[${k}]`, scalar(v)]);\n }\n if (Array.isArray(value)) {\n if (style === \"form\" && explode) return value.map((v) => [name, scalar(v)]);\n const delimiter =\n style === \"spaceDelimited\" ? \" \" : style === \"pipeDelimited\" ? \"|\" : \",\";\n return [[name, value.map(scalar).join(delimiter)]];\n }\n if (isRecord(value)) {\n const values = Object.entries(value).filter(([, v]) => v !== undefined);\n return explode\n ? values.map(([k, v]) => [k, scalar(v)])\n : [[name, values.flatMap(([k, v]) => [k, scalar(v)]).join(\",\")]];\n }\n return [[name, scalar(value)]];\n}\nfunction pathValue(value: unknown, explode = false): string {\n const enc = (v: unknown) => encodeURIComponent(scalar(v));\n if (Array.isArray(value)) return value.map(enc).join(\",\");\n if (isRecord(value))\n return Object.entries(value)\n .map(([k, v]) =>\n explode ? `${enc(k)}=${enc(v)}` : `${enc(k)},${enc(v)}`,\n )\n .join(\",\");\n return enc(value);\n}\nexport async function defaultRequest(\n ctx: RequestContext,\n): Promise<TransportResponse> {\n const headers = new Headers(ctx.headers);\n let body: BodyInit | undefined;\n if (ctx.body !== undefined) {\n if (ctx.method === \"GET\" || ctx.method === \"HEAD\")\n throw new Error(\n \"Fetch cannot send GET/HEAD bodies; configure a request adapter\",\n );\n if (ctx.mediaType?.includes(\"json\")) {\n body = JSON.stringify(ctx.body);\n if (!headers.has(\"Content-Type\"))\n headers.set(\"Content-Type\", ctx.mediaType);\n } else if (ctx.mediaType?.startsWith(\"multipart/form-data\")) {\n if (!isRecord(ctx.body))\n throw new Error(\"Multipart body must be an object\");\n const form = new FormData();\n for (const [name, value] of Object.entries(ctx.body)) {\n if (value === undefined) continue;\n for (const v of Array.isArray(value) ? value : [value])\n form.append(name, v instanceof Blob ? v : scalar(v));\n }\n body = form;\n headers.delete(\"Content-Type\");\n } else if (ctx.mediaType?.startsWith(\"application/x-www-form-urlencoded\")) {\n if (!isRecord(ctx.body)) throw new Error(\"Form body must be an object\");\n const form = new URLSearchParams();\n for (const [name, value] of Object.entries(ctx.body))\n for (const [k, v] of queryEntries(name, value)) form.append(k, v);\n body = form.toString();\n if (!headers.has(\"Content-Type\"))\n headers.set(\"Content-Type\", ctx.mediaType);\n } else if (\n typeof ctx.body === \"string\" ||\n ctx.body instanceof Blob ||\n ctx.body instanceof ArrayBuffer ||\n ArrayBuffer.isView(ctx.body)\n ) {\n body = ctx.body as BodyInit;\n if (ctx.mediaType && !headers.has(\"Content-Type\"))\n headers.set(\"Content-Type\", ctx.mediaType);\n } else\n throw new Error(\"Unsupported body format; configure a request adapter\");\n }\n const response = await fetch(ctx.url, {\n method: ctx.method,\n headers,\n body,\n signal: ctx.signal,\n });\n let data: unknown;\n const mode = ctx.operation.responseType;\n if (\n response.status === 204 ||\n response.status === 205 ||\n ctx.method === \"HEAD\"\n )\n data = undefined;\n else if (!response.ok) {\n const text = await response.text();\n try {\n data = JSON.parse(text);\n } catch {\n data = text;\n }\n } else if (mode === \"void\") data = undefined;\n else if (\n mode === \"blob\" ||\n (mode === \"auto\" &&\n !response.headers.get(\"Content-Type\")?.includes(\"json\") &&\n !response.headers.get(\"Content-Type\")?.startsWith(\"text/\"))\n )\n data = await response.blob();\n else {\n const text = await response.text();\n data =\n mode === \"json\" ||\n (mode === \"auto\" &&\n response.headers.get(\"Content-Type\")?.includes(\"json\"))\n ? text\n ? JSON.parse(text)\n : undefined\n : text;\n }\n return { data, status: response.status, headers: response.headers };\n}\nconst runtimeId = Math.random().toString(36).slice(2);\nlet nextClient = 0;\nexport function createClient(config: ClientConfig = {}) {\n const clientId = `chainspec-${runtimeId}-${++nextClient}`;\n const base = config.baseUrl ?? \"\";\n const before = [...(config.beforeRequest ?? [])],\n after = [...(config.afterResponse ?? [])],\n errors = [...(config.onError ?? [])];\n return {\n endpoint<P, B, R, H extends boolean, Required extends boolean>(\n definition: Definition,\n ): Endpoint<P, B, R, H, Required> {\n const def = Object.freeze({\n ...definition,\n parameters: definition.parameters.map((p) => Object.freeze({ ...p })),\n });\n async function execute(...args: unknown[]): Promise<R> {\n const { params: input, body, options } = splitArgs(def, args);\n let context: RequestContext | undefined;\n try {\n let path = def.path;\n for (const p of def.parameters.filter((p) => p.in === \"path\")) {\n const value = input.path?.[p.name];\n if (value === undefined || value === null)\n throw new Error(`Missing path parameter: ${p.name}`);\n path = path.split(`{${p.name}}`).join(pathValue(value, p.explode));\n }\n if (/\\{[^}]+\\}/.test(path))\n throw new Error(\"Unresolved path parameter\");\n const query = new URLSearchParams();\n const headers = new Headers(\n typeof config.headers === \"function\"\n ? await config.headers()\n : config.headers,\n );\n new Headers(options.headers).forEach((v, k) => headers.set(k, v));\n for (const p of def.parameters.filter(\n (p) => p.in === \"query\" || p.in === \"cookie\",\n )) {\n const bag = p.in === \"query\" ? input.query : input.cookies;\n const value = bag?.[p.name];\n if (p.required && (value === undefined || value === null))\n throw new Error(`Missing ${p.in} parameter: ${p.name}`);\n if (value === undefined) continue;\n if (p.in === \"query\")\n for (const [k, v] of queryEntries(\n p.name,\n value,\n p.style,\n p.explode ?? true,\n ))\n query.append(k, v);\n else if (config.request)\n headers.append(\n \"Cookie\",\n `${p.name}=${encodeURIComponent(scalar(value))}`,\n );\n else\n throw new Error(\n \"Cookie parameters require a custom Node request adapter; browser Fetch cannot set Cookie headers\",\n );\n }\n if (def.bodyRequired && body === undefined)\n throw new Error(\"Request body is required\");\n const url = `${base.replace(/\\/$/, \"\")}/${path.replace(/^\\//, \"\")}${query.size ? `?${query}` : \"\"}`;\n context = {\n url,\n method: def.method,\n headers,\n body,\n mediaType: def.mediaType,\n signal: options.signal,\n operation: def,\n };\n for (const fn of [...before, ...(options.beforeRequest ?? [])])\n context = (await fn(context)) ?? context;\n for (const p of def.parameters.filter(\n (p) => p.in === \"header\" && p.required,\n )) {\n if (!context.headers.has(p.name))\n throw new Error(`Missing header parameter: ${p.name}`);\n }\n let response = await (config.request ?? defaultRequest)(context);\n if (\n !Number.isInteger(response.status) ||\n response.status < 100 ||\n response.status > 599\n )\n throw new Error(\"Adapter must return {data,status,headers}\");\n if (response.status < 200 || response.status >= 300)\n throw new ApiError(\n response.status,\n response.data,\n response.headers,\n );\n for (const fn of [...after, ...(options.afterResponse ?? [])])\n response = (await fn(response, context)) ?? response;\n return response.data as R;\n } catch (error) {\n for (const fn of [...errors, ...(options.onError ?? [])])\n await fn(error, context);\n throw error;\n }\n }\n return {\n definition: def,\n request: execute as Endpoint<P, B, R, H, Required>[\"request\"],\n getKey(...args: RequestArgs<P, B, H, Required>) {\n const { params, body, options } = splitArgs(def, args);\n const scope =\n typeof config.cacheScope === \"function\"\n ? config.cacheScope()\n : (config.cacheScope ?? \"\");\n return [\n clientId,\n base,\n scope,\n def.id,\n params,\n body,\n Array.from(new Headers(options?.headers).entries()),\n ];\n },\n };\n },\n };\n}\n";
2
+ export declare const HOOKS_SOURCE = "import useSWR, { type SWRConfiguration, type SWRResponse } from \"swr\";\nimport useSWRMutation, {\n type SWRMutationConfiguration,\n type SWRMutationResponse,\n} from \"swr/mutation\";\nimport {\n splitArgs,\n type Endpoint,\n type RequestArgs,\n type RequestOptions,\n} from \"./runtime.js\";\nexport type QueryOptions<R> = RequestOptions & {\n enabled?: boolean;\n swr?: SWRConfiguration<R, Error>;\n};\nexport type QueryArgs<\n P,\n B,\n R,\n H extends boolean,\n Required extends boolean,\n> = H extends true\n ? Required extends true\n ? [params: P | null, body: B, options?: QueryOptions<R>]\n : [params: P | null, body?: B, options?: QueryOptions<R>]\n : {} extends P\n ? [params?: P | null, options?: QueryOptions<R>]\n : [params: P | null, options?: QueryOptions<R>];\nexport type MutationOptions<B, R> = RequestOptions & {\n swr?: SWRMutationConfiguration<R, Error, readonly unknown[], B> & {\n throwOnError?: boolean;\n };\n};\nexport type TriggerOptions<B, R> = SWRMutationConfiguration<\n R,\n Error,\n readonly unknown[],\n B\n> & { throwOnError?: boolean };\nexport type MutationResult<B, R, Bound extends boolean> = Omit<\n SWRMutationResponse<R, Error, readonly unknown[], B>,\n \"trigger\"\n> & {\n trigger(\n ...args: Bound extends true\n ? [body?: B, options?: TriggerOptions<B, R>]\n : [body: B, options?: TriggerOptions<B, R>]\n ): Promise<R | undefined>;\n};\nexport type MutationHook<\n P,\n B,\n R,\n H extends boolean,\n Required extends boolean,\n> = H extends true\n ? {\n (\n params: P,\n body: B,\n options?: MutationOptions<B, R>,\n ): MutationResult<B, R, true>;\n (\n params: P,\n body?: undefined,\n options?: MutationOptions<B, R>,\n ): MutationResult<B, R, Required extends true ? false : true>;\n }\n : (\n ...args: {} extends P\n ? [params?: P, options?: MutationOptions<never, R>]\n : [params: P, options?: MutationOptions<never, R>]\n ) => MutationResult<never, R, true>;\nexport type QueryEndpoint<\n P,\n B,\n R,\n H extends boolean,\n Required extends boolean,\n> = Endpoint<P, B, R, H, Required> & {\n useRequest(...args: QueryArgs<P, B, R, H, Required>): SWRResponse<R, Error>;\n};\nexport type MutationEndpoint<\n P,\n B,\n R,\n H extends boolean,\n Required extends boolean,\n> = Endpoint<P, B, R, H, Required> & {\n useRequest: MutationHook<P, B, R, H, Required>;\n useMutation: MutationHook<P, B, R, H, Required>;\n};\nexport function withQuery<P, B, R, H extends boolean, Required extends boolean>(\n endpoint: Endpoint<P, B, R, H, Required>,\n): QueryEndpoint<P, B, R, H, Required> {\n function useRequest(...args: QueryArgs<P, B, R, H, Required>) {\n const { options } = splitArgs(endpoint.definition, args);\n const config = options as QueryOptions<R>;\n const requestArgs = args as unknown as RequestArgs<P, B, H, Required>;\n return useSWR<R, Error>(\n args[0] !== null && config.enabled !== false\n ? endpoint.getKey(...requestArgs)\n : null,\n () => endpoint.request(...requestArgs),\n config.swr,\n );\n }\n return Object.assign(endpoint, { useRequest });\n}\nexport function withMutation<\n P,\n B,\n R,\n H extends boolean,\n Required extends boolean,\n>(\n endpoint: Endpoint<P, B, R, H, Required>,\n): MutationEndpoint<P, B, R, H, Required> {\n function useMutation(...args: unknown[]) {\n const {\n params,\n body: preset,\n options,\n } = splitArgs(endpoint.definition, args);\n const config = options as MutationOptions<B, R>;\n const boundArgs = (endpoint.definition.hasBody\n ? [params, preset, options]\n : [params, options]) as unknown as RequestArgs<P, B, H, Required>;\n const state = useSWRMutation<R, Error, readonly unknown[], B>(\n endpoint.getKey(...boundArgs),\n (_key, { arg }) => {\n const requestArgs = (endpoint.definition.hasBody\n ? [params, arg, options]\n : [params, options]) as unknown as RequestArgs<P, B, H, Required>;\n return endpoint.request(...requestArgs);\n },\n config.swr,\n );\n return {\n ...state,\n trigger: (...triggerArgs: unknown[]) => {\n const body = triggerArgs.length ? triggerArgs[0] : preset;\n const trigger = state.trigger as (\n body: B,\n options?: TriggerOptions<B, R>,\n ) => Promise<R | undefined>;\n return trigger(\n body as B,\n triggerArgs[1] as TriggerOptions<B, R> | undefined,\n );\n },\n };\n }\n return Object.assign(endpoint, {\n useRequest: useMutation,\n useMutation,\n }) as MutationEndpoint<P, B, R, H, Required>;\n}\n";
3
3
  export declare const RUNTIME_LICENSE = "MIT License\n\nCopyright (c) 2026 batype and SnapNest contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n";
@@ -1,4 +1,4 @@
1
1
  // Generated by scripts/embed-templates.mjs. Edit templates/ instead.
2
- export const RUNTIME_SOURCE = "/** ChainSpec runtime. Generated SDK types describe the spec, not runtime validation. */\nexport type Parameter = {\n name: string;\n in: \"path\" | \"query\" | \"header\" | \"cookie\";\n required?: boolean;\n style?: string;\n explode?: boolean;\n};\nexport type Definition = {\n id: string;\n method: string;\n path: string;\n controller: string;\n name: string;\n parameters: Parameter[];\n bodyRequired?: boolean;\n mediaType?: string;\n responseType: \"json\" | \"text\" | \"blob\" | \"void\" | \"auto\";\n};\nexport type Input = {\n path?: Record<string, unknown>;\n query?: Record<string, unknown>;\n headers?: Record<string, unknown>;\n cookies?: Record<string, unknown>;\n body?: unknown;\n};\nexport interface RequestContext {\n url: string;\n method: string;\n headers: Headers;\n body: unknown;\n mediaType?: string;\n signal?: AbortSignal;\n readonly operation: Definition;\n}\nexport interface TransportResponse {\n data: unknown;\n status: number;\n headers: Headers;\n}\nexport type RequestAdapter = (\n context: RequestContext,\n) => Promise<TransportResponse>;\nexport type BeforeRequest = (\n context: RequestContext,\n) => void | RequestContext | Promise<void | RequestContext>;\nexport type AfterResponse = (\n response: TransportResponse,\n context: RequestContext,\n) => void | TransportResponse | Promise<void | TransportResponse>;\nexport type OnError = (\n error: unknown,\n context: RequestContext | undefined,\n) => void | Promise<void>;\nexport interface RequestOptions {\n signal?: AbortSignal;\n headers?: HeadersInit;\n beforeRequest?: BeforeRequest[];\n afterResponse?: AfterResponse[];\n onError?: OnError[];\n}\nexport interface ClientConfig {\n baseUrl?: string;\n headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);\n request?: RequestAdapter;\n beforeRequest?: BeforeRequest[];\n afterResponse?: AfterResponse[];\n onError?: OnError[];\n /** Change on tenant/user/auth changes. Never put bearer tokens here. */ cacheScope?:\n | string\n | (() => string);\n}\nexport type RequestArgs<I> = {} extends I\n ? [input?: I, options?: RequestOptions]\n : [input: I, options?: RequestOptions];\nexport interface Endpoint<I, R> {\n readonly definition: Definition;\n request(...args: RequestArgs<I>): Promise<R>;\n getKey(input?: I, options?: RequestOptions): readonly unknown[];\n}\nexport class ApiError extends Error {\n constructor(\n public status: number,\n public data: unknown,\n public headers: Headers,\n ) {\n super(`HTTP ${status}`);\n this.name = \"ApiError\";\n }\n}\nconst scalar = (value: unknown): string => {\n if (value === null) return \"\";\n if ([\"string\", \"number\", \"boolean\"].includes(typeof value))\n return String(value);\n throw new Error(\"Parameter must be a scalar or a declared array/object\");\n};\nconst isRecord = (v: unknown): v is Record<string, unknown> =>\n typeof v === \"object\" && v !== null && !Array.isArray(v);\nfunction queryEntries(\n name: string,\n value: unknown,\n style = \"form\",\n explode = true,\n): [string, string][] {\n if (value === undefined) return [];\n if (style === \"deepObject\") {\n if (!isRecord(value)) throw new Error(\"deepObject needs an object\");\n return Object.entries(value)\n .filter(([, v]) => v !== undefined)\n .map(([k, v]) => [`${name}[${k}]`, scalar(v)]);\n }\n if (Array.isArray(value)) {\n if (style === \"form\" && explode) return value.map((v) => [name, scalar(v)]);\n const delimiter =\n style === \"spaceDelimited\" ? \" \" : style === \"pipeDelimited\" ? \"|\" : \",\";\n return [[name, value.map(scalar).join(delimiter)]];\n }\n if (isRecord(value)) {\n const values = Object.entries(value).filter(([, v]) => v !== undefined);\n return explode\n ? values.map(([k, v]) => [k, scalar(v)])\n : [[name, values.flatMap(([k, v]) => [k, scalar(v)]).join(\",\")]];\n }\n return [[name, scalar(value)]];\n}\nfunction pathValue(value: unknown, explode = false): string {\n const enc = (v: unknown) => encodeURIComponent(scalar(v));\n if (Array.isArray(value)) return value.map(enc).join(\",\");\n if (isRecord(value))\n return Object.entries(value)\n .map(([k, v]) =>\n explode ? `${enc(k)}=${enc(v)}` : `${enc(k)},${enc(v)}`,\n )\n .join(\",\");\n return enc(value);\n}\nexport async function defaultRequest(\n ctx: RequestContext,\n): Promise<TransportResponse> {\n const headers = new Headers(ctx.headers);\n let body: BodyInit | undefined;\n if (ctx.body !== undefined) {\n if (ctx.method === \"GET\" || ctx.method === \"HEAD\")\n throw new Error(\n \"Fetch cannot send GET/HEAD bodies; configure a request adapter\",\n );\n if (ctx.mediaType?.includes(\"json\")) {\n body = JSON.stringify(ctx.body);\n if (!headers.has(\"Content-Type\"))\n headers.set(\"Content-Type\", ctx.mediaType);\n } else if (ctx.mediaType?.startsWith(\"multipart/form-data\")) {\n if (!isRecord(ctx.body))\n throw new Error(\"Multipart body must be an object\");\n const form = new FormData();\n for (const [name, value] of Object.entries(ctx.body)) {\n if (value === undefined) continue;\n for (const v of Array.isArray(value) ? value : [value])\n form.append(name, v instanceof Blob ? v : scalar(v));\n }\n body = form;\n headers.delete(\"Content-Type\");\n } else if (ctx.mediaType?.startsWith(\"application/x-www-form-urlencoded\")) {\n if (!isRecord(ctx.body)) throw new Error(\"Form body must be an object\");\n const form = new URLSearchParams();\n for (const [name, value] of Object.entries(ctx.body))\n for (const [k, v] of queryEntries(name, value)) form.append(k, v);\n body = form.toString();\n if (!headers.has(\"Content-Type\"))\n headers.set(\"Content-Type\", ctx.mediaType);\n } else if (\n typeof ctx.body === \"string\" ||\n ctx.body instanceof Blob ||\n ctx.body instanceof ArrayBuffer ||\n ArrayBuffer.isView(ctx.body)\n ) {\n body = ctx.body as BodyInit;\n if (ctx.mediaType && !headers.has(\"Content-Type\"))\n headers.set(\"Content-Type\", ctx.mediaType);\n } else\n throw new Error(\"Unsupported body format; configure a request adapter\");\n }\n const response = await fetch(ctx.url, {\n method: ctx.method,\n headers,\n body,\n signal: ctx.signal,\n });\n let data: unknown;\n const mode = ctx.operation.responseType;\n if (\n response.status === 204 ||\n response.status === 205 ||\n ctx.method === \"HEAD\"\n )\n data = undefined;\n else if (!response.ok) {\n const text = await response.text();\n try {\n data = JSON.parse(text);\n } catch {\n data = text;\n }\n } else if (mode === \"void\") data = undefined;\n else if (\n mode === \"blob\" ||\n (mode === \"auto\" &&\n !response.headers.get(\"Content-Type\")?.includes(\"json\") &&\n !response.headers.get(\"Content-Type\")?.startsWith(\"text/\"))\n )\n data = await response.blob();\n else {\n const text = await response.text();\n data =\n mode === \"json\" ||\n (mode === \"auto\" &&\n response.headers.get(\"Content-Type\")?.includes(\"json\"))\n ? text\n ? JSON.parse(text)\n : undefined\n : text;\n }\n return { data, status: response.status, headers: response.headers };\n}\nconst runtimeId = Math.random().toString(36).slice(2);\nlet nextClient = 0;\nexport function createClient(config: ClientConfig = {}) {\n const clientId = `chainspec-${runtimeId}-${++nextClient}`;\n const base = config.baseUrl ?? \"\";\n const before = [...(config.beforeRequest ?? [])],\n after = [...(config.afterResponse ?? [])],\n errors = [...(config.onError ?? [])];\n return {\n endpoint<I, R>(definition: Definition): Endpoint<I, R> {\n const def = Object.freeze({\n ...definition,\n parameters: definition.parameters.map((p) => Object.freeze({ ...p })),\n });\n async function execute(\n input: Input = {},\n options: RequestOptions = {},\n ): Promise<R> {\n let context: RequestContext | undefined;\n try {\n let path = def.path;\n for (const p of def.parameters.filter((p) => p.in === \"path\")) {\n const value = input.path?.[p.name];\n if (value === undefined || value === null)\n throw new Error(`Missing path parameter: ${p.name}`);\n path = path.split(`{${p.name}}`).join(pathValue(value, p.explode));\n }\n if (/\\{[^}]+\\}/.test(path))\n throw new Error(\"Unresolved path parameter\");\n const query = new URLSearchParams();\n const headers = new Headers(\n typeof config.headers === \"function\"\n ? await config.headers()\n : config.headers,\n );\n new Headers(options.headers).forEach((v, k) => headers.set(k, v));\n for (const p of def.parameters.filter((p) => p.in !== \"path\")) {\n const bag =\n p.in === \"query\"\n ? input.query\n : p.in === \"header\"\n ? input.headers\n : input.cookies;\n const value = bag?.[p.name];\n if (p.required && (value === undefined || value === null))\n throw new Error(`Missing ${p.in} parameter: ${p.name}`);\n if (value === undefined) continue;\n if (p.in === \"query\")\n for (const [k, v] of queryEntries(\n p.name,\n value,\n p.style,\n p.explode ?? true,\n ))\n query.append(k, v);\n else if (p.in === \"header\")\n headers.set(\n p.name,\n Array.isArray(value)\n ? value.map(scalar).join(\",\")\n : scalar(value),\n );\n else if (config.request)\n headers.append(\n \"Cookie\",\n `${p.name}=${encodeURIComponent(scalar(value))}`,\n );\n else\n throw new Error(\n \"Cookie parameters require a custom Node request adapter; browser Fetch cannot set Cookie headers\",\n );\n }\n if (def.bodyRequired && input.body === undefined)\n throw new Error(\"Request body is required\");\n const url = `${base.replace(/\\/$/, \"\")}/${path.replace(/^\\//, \"\")}${query.size ? `?${query}` : \"\"}`;\n context = {\n url,\n method: def.method,\n headers,\n body: input.body,\n mediaType: def.mediaType,\n signal: options.signal,\n operation: def,\n };\n for (const fn of [...before, ...(options.beforeRequest ?? [])])\n context = (await fn(context)) ?? context;\n let response = await (config.request ?? defaultRequest)(context);\n if (\n !Number.isInteger(response.status) ||\n response.status < 100 ||\n response.status > 599\n )\n throw new Error(\"Adapter must return {data,status,headers}\");\n if (response.status < 200 || response.status >= 300)\n throw new ApiError(\n response.status,\n response.data,\n response.headers,\n );\n for (const fn of [...after, ...(options.afterResponse ?? [])])\n response = (await fn(response, context)) ?? response;\n return response.data as R;\n } catch (error) {\n for (const fn of [...errors, ...(options.onError ?? [])])\n await fn(error, context);\n throw error;\n }\n }\n return {\n definition: def,\n request: execute as Endpoint<I, R>[\"request\"],\n getKey(input?: I, options?: RequestOptions) {\n const scope =\n typeof config.cacheScope === \"function\"\n ? config.cacheScope()\n : (config.cacheScope ?? \"\");\n return [\n clientId,\n base,\n scope,\n def.id,\n input ?? {},\n Array.from(new Headers(options?.headers).entries()),\n ];\n },\n };\n },\n };\n}\n";
3
- export const HOOKS_SOURCE = "import useSWR, { type SWRConfiguration, type SWRResponse } from \"swr\";\nimport useSWRMutation, {\n type SWRMutationConfiguration,\n type SWRMutationResponse,\n} from \"swr/mutation\";\nimport type { Endpoint, RequestArgs, RequestOptions } from \"./runtime.js\";\nexport type QueryOptions<R> = {\n enabled?: boolean;\n request?: RequestOptions;\n swr?: SWRConfiguration<R, Error>;\n};\nexport type QueryArgs<I, R> = {} extends I\n ? [input?: I | null, options?: QueryOptions<R>]\n : [input: I | null, options?: QueryOptions<R>];\nexport type MutationOptions<I, R> = {\n request?: RequestOptions;\n swr?: SWRMutationConfiguration<R, Error, readonly unknown[], I>;\n};\nexport type QueryEndpoint<I, R> = Endpoint<I, R> & {\n useRequest(...args: QueryArgs<I, R>): SWRResponse<R, Error>;\n};\nexport type MutationEndpoint<I, R> = Endpoint<I, R> & {\n useRequest(\n options?: MutationOptions<I, R>,\n ): SWRMutationResponse<R, Error, readonly unknown[], I>;\n useMutation(\n options?: MutationOptions<I, R>,\n ): SWRMutationResponse<R, Error, readonly unknown[], I>;\n};\nexport function withQuery<I, R>(endpoint: Endpoint<I, R>): QueryEndpoint<I, R> {\n return Object.assign(endpoint, {\n useRequest(...[input, options]: QueryArgs<I, R>) {\n const enabled = input !== null && options?.enabled !== false;\n return useSWR<R, Error>(\n enabled ? endpoint.getKey(input ?? undefined, options?.request) : null,\n () =>\n endpoint.request(\n ...([input ?? undefined, options?.request] as RequestArgs<I>),\n ),\n options?.swr,\n );\n },\n });\n}\nexport function withMutation<I, R>(\n endpoint: Endpoint<I, R>,\n): MutationEndpoint<I, R> {\n function useMutation(options?: MutationOptions<I, R>) {\n return useSWRMutation<R, Error, readonly unknown[], I>(\n endpoint.getKey(undefined, options?.request),\n (_key, { arg }) =>\n endpoint.request(...([arg, options?.request] as RequestArgs<I>)),\n options?.swr,\n );\n }\n return Object.assign(endpoint, { useRequest: useMutation, useMutation });\n}\n";
2
+ export const RUNTIME_SOURCE = "/** ChainSpec runtime. Generated SDK types describe the spec, not runtime validation. */\nexport type Parameter = {\n name: string;\n in: \"path\" | \"query\" | \"header\" | \"cookie\";\n required?: boolean;\n style?: string;\n explode?: boolean;\n};\nexport type Definition = {\n id: string;\n method: string;\n path: string;\n controller: string;\n name: string;\n parameters: Parameter[];\n hasBody: boolean;\n bodyRequired?: boolean;\n mediaType?: string;\n responseType: \"json\" | \"text\" | \"blob\" | \"void\" | \"auto\";\n};\nexport type Input = {\n path?: Record<string, unknown>;\n query?: Record<string, unknown>;\n cookies?: Record<string, unknown>;\n};\nexport interface RequestContext {\n url: string;\n method: string;\n headers: Headers;\n body: unknown;\n mediaType?: string;\n signal?: AbortSignal;\n readonly operation: Definition;\n}\nexport interface TransportResponse {\n data: unknown;\n status: number;\n headers: Headers;\n}\nexport type RequestAdapter = (\n context: RequestContext,\n) => Promise<TransportResponse>;\nexport type BeforeRequest = (\n context: RequestContext,\n) => void | RequestContext | Promise<void | RequestContext>;\nexport type AfterResponse = (\n response: TransportResponse,\n context: RequestContext,\n) => void | TransportResponse | Promise<void | TransportResponse>;\nexport type OnError = (\n error: unknown,\n context: RequestContext | undefined,\n) => void | Promise<void>;\nexport interface RequestOptions {\n signal?: AbortSignal;\n headers?: HeadersInit;\n beforeRequest?: BeforeRequest[];\n afterResponse?: AfterResponse[];\n onError?: OnError[];\n}\nexport interface ClientConfig {\n baseUrl?: string;\n headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);\n request?: RequestAdapter;\n beforeRequest?: BeforeRequest[];\n afterResponse?: AfterResponse[];\n onError?: OnError[];\n /** Change on tenant/user/auth changes. Never put bearer tokens here. */ cacheScope?:\n | string\n | (() => string);\n}\nexport type RequestArgs<\n P,\n B,\n H extends boolean,\n Required extends boolean,\n> = H extends true\n ? Required extends true\n ? [params: P, body: B, options?: RequestOptions]\n : [params: P, body?: B, options?: RequestOptions]\n : {} extends P\n ? [params?: P, options?: RequestOptions]\n : [params: P, options?: RequestOptions];\nexport interface Endpoint<\n P,\n B,\n R,\n H extends boolean = false,\n Required extends boolean = false,\n> {\n readonly definition: Definition;\n request(...args: RequestArgs<P, B, H, Required>): Promise<R>;\n getKey(...args: RequestArgs<P, B, H, Required>): readonly unknown[];\n}\n/** Body position is fixed by the generated operation contract, never inferred from object keys. */\nexport function splitArgs(\n definition: Definition,\n args: readonly unknown[],\n): { params: Input; body: unknown; options: RequestOptions } {\n return {\n params: (args[0] ?? {}) as Input,\n body: definition.hasBody ? args[1] : undefined,\n options: (args[definition.hasBody ? 2 : 1] ?? {}) as RequestOptions,\n };\n}\nexport class ApiError extends Error {\n constructor(\n public status: number,\n public data: unknown,\n public headers: Headers,\n ) {\n super(`HTTP ${status}`);\n this.name = \"ApiError\";\n }\n}\nconst scalar = (value: unknown): string => {\n if (value === null) return \"\";\n if ([\"string\", \"number\", \"boolean\"].includes(typeof value))\n return String(value);\n throw new Error(\"Parameter must be a scalar or a declared array/object\");\n};\nconst isRecord = (v: unknown): v is Record<string, unknown> =>\n typeof v === \"object\" && v !== null && !Array.isArray(v);\nfunction queryEntries(\n name: string,\n value: unknown,\n style = \"form\",\n explode = true,\n): [string, string][] {\n if (value === undefined) return [];\n if (style === \"deepObject\") {\n if (!isRecord(value)) throw new Error(\"deepObject needs an object\");\n return Object.entries(value)\n .filter(([, v]) => v !== undefined)\n .map(([k, v]) => [`${name}[${k}]`, scalar(v)]);\n }\n if (Array.isArray(value)) {\n if (style === \"form\" && explode) return value.map((v) => [name, scalar(v)]);\n const delimiter =\n style === \"spaceDelimited\" ? \" \" : style === \"pipeDelimited\" ? \"|\" : \",\";\n return [[name, value.map(scalar).join(delimiter)]];\n }\n if (isRecord(value)) {\n const values = Object.entries(value).filter(([, v]) => v !== undefined);\n return explode\n ? values.map(([k, v]) => [k, scalar(v)])\n : [[name, values.flatMap(([k, v]) => [k, scalar(v)]).join(\",\")]];\n }\n return [[name, scalar(value)]];\n}\nfunction pathValue(value: unknown, explode = false): string {\n const enc = (v: unknown) => encodeURIComponent(scalar(v));\n if (Array.isArray(value)) return value.map(enc).join(\",\");\n if (isRecord(value))\n return Object.entries(value)\n .map(([k, v]) =>\n explode ? `${enc(k)}=${enc(v)}` : `${enc(k)},${enc(v)}`,\n )\n .join(\",\");\n return enc(value);\n}\nexport async function defaultRequest(\n ctx: RequestContext,\n): Promise<TransportResponse> {\n const headers = new Headers(ctx.headers);\n let body: BodyInit | undefined;\n if (ctx.body !== undefined) {\n if (ctx.method === \"GET\" || ctx.method === \"HEAD\")\n throw new Error(\n \"Fetch cannot send GET/HEAD bodies; configure a request adapter\",\n );\n if (ctx.mediaType?.includes(\"json\")) {\n body = JSON.stringify(ctx.body);\n if (!headers.has(\"Content-Type\"))\n headers.set(\"Content-Type\", ctx.mediaType);\n } else if (ctx.mediaType?.startsWith(\"multipart/form-data\")) {\n if (!isRecord(ctx.body))\n throw new Error(\"Multipart body must be an object\");\n const form = new FormData();\n for (const [name, value] of Object.entries(ctx.body)) {\n if (value === undefined) continue;\n for (const v of Array.isArray(value) ? value : [value])\n form.append(name, v instanceof Blob ? v : scalar(v));\n }\n body = form;\n headers.delete(\"Content-Type\");\n } else if (ctx.mediaType?.startsWith(\"application/x-www-form-urlencoded\")) {\n if (!isRecord(ctx.body)) throw new Error(\"Form body must be an object\");\n const form = new URLSearchParams();\n for (const [name, value] of Object.entries(ctx.body))\n for (const [k, v] of queryEntries(name, value)) form.append(k, v);\n body = form.toString();\n if (!headers.has(\"Content-Type\"))\n headers.set(\"Content-Type\", ctx.mediaType);\n } else if (\n typeof ctx.body === \"string\" ||\n ctx.body instanceof Blob ||\n ctx.body instanceof ArrayBuffer ||\n ArrayBuffer.isView(ctx.body)\n ) {\n body = ctx.body as BodyInit;\n if (ctx.mediaType && !headers.has(\"Content-Type\"))\n headers.set(\"Content-Type\", ctx.mediaType);\n } else\n throw new Error(\"Unsupported body format; configure a request adapter\");\n }\n const response = await fetch(ctx.url, {\n method: ctx.method,\n headers,\n body,\n signal: ctx.signal,\n });\n let data: unknown;\n const mode = ctx.operation.responseType;\n if (\n response.status === 204 ||\n response.status === 205 ||\n ctx.method === \"HEAD\"\n )\n data = undefined;\n else if (!response.ok) {\n const text = await response.text();\n try {\n data = JSON.parse(text);\n } catch {\n data = text;\n }\n } else if (mode === \"void\") data = undefined;\n else if (\n mode === \"blob\" ||\n (mode === \"auto\" &&\n !response.headers.get(\"Content-Type\")?.includes(\"json\") &&\n !response.headers.get(\"Content-Type\")?.startsWith(\"text/\"))\n )\n data = await response.blob();\n else {\n const text = await response.text();\n data =\n mode === \"json\" ||\n (mode === \"auto\" &&\n response.headers.get(\"Content-Type\")?.includes(\"json\"))\n ? text\n ? JSON.parse(text)\n : undefined\n : text;\n }\n return { data, status: response.status, headers: response.headers };\n}\nconst runtimeId = Math.random().toString(36).slice(2);\nlet nextClient = 0;\nexport function createClient(config: ClientConfig = {}) {\n const clientId = `chainspec-${runtimeId}-${++nextClient}`;\n const base = config.baseUrl ?? \"\";\n const before = [...(config.beforeRequest ?? [])],\n after = [...(config.afterResponse ?? [])],\n errors = [...(config.onError ?? [])];\n return {\n endpoint<P, B, R, H extends boolean, Required extends boolean>(\n definition: Definition,\n ): Endpoint<P, B, R, H, Required> {\n const def = Object.freeze({\n ...definition,\n parameters: definition.parameters.map((p) => Object.freeze({ ...p })),\n });\n async function execute(...args: unknown[]): Promise<R> {\n const { params: input, body, options } = splitArgs(def, args);\n let context: RequestContext | undefined;\n try {\n let path = def.path;\n for (const p of def.parameters.filter((p) => p.in === \"path\")) {\n const value = input.path?.[p.name];\n if (value === undefined || value === null)\n throw new Error(`Missing path parameter: ${p.name}`);\n path = path.split(`{${p.name}}`).join(pathValue(value, p.explode));\n }\n if (/\\{[^}]+\\}/.test(path))\n throw new Error(\"Unresolved path parameter\");\n const query = new URLSearchParams();\n const headers = new Headers(\n typeof config.headers === \"function\"\n ? await config.headers()\n : config.headers,\n );\n new Headers(options.headers).forEach((v, k) => headers.set(k, v));\n for (const p of def.parameters.filter(\n (p) => p.in === \"query\" || p.in === \"cookie\",\n )) {\n const bag = p.in === \"query\" ? input.query : input.cookies;\n const value = bag?.[p.name];\n if (p.required && (value === undefined || value === null))\n throw new Error(`Missing ${p.in} parameter: ${p.name}`);\n if (value === undefined) continue;\n if (p.in === \"query\")\n for (const [k, v] of queryEntries(\n p.name,\n value,\n p.style,\n p.explode ?? true,\n ))\n query.append(k, v);\n else if (config.request)\n headers.append(\n \"Cookie\",\n `${p.name}=${encodeURIComponent(scalar(value))}`,\n );\n else\n throw new Error(\n \"Cookie parameters require a custom Node request adapter; browser Fetch cannot set Cookie headers\",\n );\n }\n if (def.bodyRequired && body === undefined)\n throw new Error(\"Request body is required\");\n const url = `${base.replace(/\\/$/, \"\")}/${path.replace(/^\\//, \"\")}${query.size ? `?${query}` : \"\"}`;\n context = {\n url,\n method: def.method,\n headers,\n body,\n mediaType: def.mediaType,\n signal: options.signal,\n operation: def,\n };\n for (const fn of [...before, ...(options.beforeRequest ?? [])])\n context = (await fn(context)) ?? context;\n for (const p of def.parameters.filter(\n (p) => p.in === \"header\" && p.required,\n )) {\n if (!context.headers.has(p.name))\n throw new Error(`Missing header parameter: ${p.name}`);\n }\n let response = await (config.request ?? defaultRequest)(context);\n if (\n !Number.isInteger(response.status) ||\n response.status < 100 ||\n response.status > 599\n )\n throw new Error(\"Adapter must return {data,status,headers}\");\n if (response.status < 200 || response.status >= 300)\n throw new ApiError(\n response.status,\n response.data,\n response.headers,\n );\n for (const fn of [...after, ...(options.afterResponse ?? [])])\n response = (await fn(response, context)) ?? response;\n return response.data as R;\n } catch (error) {\n for (const fn of [...errors, ...(options.onError ?? [])])\n await fn(error, context);\n throw error;\n }\n }\n return {\n definition: def,\n request: execute as Endpoint<P, B, R, H, Required>[\"request\"],\n getKey(...args: RequestArgs<P, B, H, Required>) {\n const { params, body, options } = splitArgs(def, args);\n const scope =\n typeof config.cacheScope === \"function\"\n ? config.cacheScope()\n : (config.cacheScope ?? \"\");\n return [\n clientId,\n base,\n scope,\n def.id,\n params,\n body,\n Array.from(new Headers(options?.headers).entries()),\n ];\n },\n };\n },\n };\n}\n";
3
+ export const HOOKS_SOURCE = "import useSWR, { type SWRConfiguration, type SWRResponse } from \"swr\";\nimport useSWRMutation, {\n type SWRMutationConfiguration,\n type SWRMutationResponse,\n} from \"swr/mutation\";\nimport {\n splitArgs,\n type Endpoint,\n type RequestArgs,\n type RequestOptions,\n} from \"./runtime.js\";\nexport type QueryOptions<R> = RequestOptions & {\n enabled?: boolean;\n swr?: SWRConfiguration<R, Error>;\n};\nexport type QueryArgs<\n P,\n B,\n R,\n H extends boolean,\n Required extends boolean,\n> = H extends true\n ? Required extends true\n ? [params: P | null, body: B, options?: QueryOptions<R>]\n : [params: P | null, body?: B, options?: QueryOptions<R>]\n : {} extends P\n ? [params?: P | null, options?: QueryOptions<R>]\n : [params: P | null, options?: QueryOptions<R>];\nexport type MutationOptions<B, R> = RequestOptions & {\n swr?: SWRMutationConfiguration<R, Error, readonly unknown[], B> & {\n throwOnError?: boolean;\n };\n};\nexport type TriggerOptions<B, R> = SWRMutationConfiguration<\n R,\n Error,\n readonly unknown[],\n B\n> & { throwOnError?: boolean };\nexport type MutationResult<B, R, Bound extends boolean> = Omit<\n SWRMutationResponse<R, Error, readonly unknown[], B>,\n \"trigger\"\n> & {\n trigger(\n ...args: Bound extends true\n ? [body?: B, options?: TriggerOptions<B, R>]\n : [body: B, options?: TriggerOptions<B, R>]\n ): Promise<R | undefined>;\n};\nexport type MutationHook<\n P,\n B,\n R,\n H extends boolean,\n Required extends boolean,\n> = H extends true\n ? {\n (\n params: P,\n body: B,\n options?: MutationOptions<B, R>,\n ): MutationResult<B, R, true>;\n (\n params: P,\n body?: undefined,\n options?: MutationOptions<B, R>,\n ): MutationResult<B, R, Required extends true ? false : true>;\n }\n : (\n ...args: {} extends P\n ? [params?: P, options?: MutationOptions<never, R>]\n : [params: P, options?: MutationOptions<never, R>]\n ) => MutationResult<never, R, true>;\nexport type QueryEndpoint<\n P,\n B,\n R,\n H extends boolean,\n Required extends boolean,\n> = Endpoint<P, B, R, H, Required> & {\n useRequest(...args: QueryArgs<P, B, R, H, Required>): SWRResponse<R, Error>;\n};\nexport type MutationEndpoint<\n P,\n B,\n R,\n H extends boolean,\n Required extends boolean,\n> = Endpoint<P, B, R, H, Required> & {\n useRequest: MutationHook<P, B, R, H, Required>;\n useMutation: MutationHook<P, B, R, H, Required>;\n};\nexport function withQuery<P, B, R, H extends boolean, Required extends boolean>(\n endpoint: Endpoint<P, B, R, H, Required>,\n): QueryEndpoint<P, B, R, H, Required> {\n function useRequest(...args: QueryArgs<P, B, R, H, Required>) {\n const { options } = splitArgs(endpoint.definition, args);\n const config = options as QueryOptions<R>;\n const requestArgs = args as unknown as RequestArgs<P, B, H, Required>;\n return useSWR<R, Error>(\n args[0] !== null && config.enabled !== false\n ? endpoint.getKey(...requestArgs)\n : null,\n () => endpoint.request(...requestArgs),\n config.swr,\n );\n }\n return Object.assign(endpoint, { useRequest });\n}\nexport function withMutation<\n P,\n B,\n R,\n H extends boolean,\n Required extends boolean,\n>(\n endpoint: Endpoint<P, B, R, H, Required>,\n): MutationEndpoint<P, B, R, H, Required> {\n function useMutation(...args: unknown[]) {\n const {\n params,\n body: preset,\n options,\n } = splitArgs(endpoint.definition, args);\n const config = options as MutationOptions<B, R>;\n const boundArgs = (endpoint.definition.hasBody\n ? [params, preset, options]\n : [params, options]) as unknown as RequestArgs<P, B, H, Required>;\n const state = useSWRMutation<R, Error, readonly unknown[], B>(\n endpoint.getKey(...boundArgs),\n (_key, { arg }) => {\n const requestArgs = (endpoint.definition.hasBody\n ? [params, arg, options]\n : [params, options]) as unknown as RequestArgs<P, B, H, Required>;\n return endpoint.request(...requestArgs);\n },\n config.swr,\n );\n return {\n ...state,\n trigger: (...triggerArgs: unknown[]) => {\n const body = triggerArgs.length ? triggerArgs[0] : preset;\n const trigger = state.trigger as (\n body: B,\n options?: TriggerOptions<B, R>,\n ) => Promise<R | undefined>;\n return trigger(\n body as B,\n triggerArgs[1] as TriggerOptions<B, R> | undefined,\n );\n },\n };\n }\n return Object.assign(endpoint, {\n useRequest: useMutation,\n useMutation,\n }) as MutationEndpoint<P, B, R, H, Required>;\n}\n";
4
4
  export const RUNTIME_LICENSE = "MIT License\n\nCopyright (c) 2026 batype and SnapNest contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@batype/chainspec",
3
- "version": "1.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",