specleaf 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +4 -1
  2. package/lib/cli.js +49 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -75,6 +75,8 @@ management/
75
75
 
76
76
  每个模块独立解析,保留各自的 OpenAPI 版本、服务地址、鉴权、tags 和引用作用域,支持共享引用、同名模型及同路径接口。缺少 tags 的接口归入“未分组”。目录搜索和方法筛选覆盖**全部模块**,可按模块路径、标题或接口关键词搜索。跨模块打开接口时保留搜索、筛选和分组展开状态;正文概览的统计和导出作用于当前模块,字段树、响应、媒体类型和示例等详情状态会重置。
77
77
 
78
+ 路径文件可以反向引用主入口中的参数、响应、请求体和模型,例如 `paths/items.yaml` 中的 `../openapi.yaml#/components/parameters/Page`。生成时会按引用原文件的路径归并为内部引用,保留递归模型;示例、默认值和扩展数据中的字面 `$ref` 不会改写。此行为也适用于单文件入口。
79
+
78
80
  模块概览链接为 `#/module/<编码后的模块ID>/overview`,接口链接为 `#/module/<编码后的模块ID>/operation/<编码后的接口ID>`;每个 ID 整体通过 `encodeURIComponent` 编码,例如 `merchant/orders` 变成 `merchant%2Forders`。链接可复制、刷新和前进后退,未知模块或接口显示无效链接状态。单文件输入继续使用原来的 `#/overview` 和 `#/operation/<编码后的接口ID>`。
79
81
 
80
82
  导出得到当前模块合并后的 OpenAPI JSON,文件名为 `openapi-<编码后的模块ID>.json`;根模块为 `openapi-..json`。单文件仍导出 `openapi.json`。模块集合和所需资源全部内嵌,断网后也能浏览全部模块、搜索和导出。
@@ -131,7 +133,7 @@ npm pack
131
133
  mkdir ../specleaf-consumer
132
134
  cd ../specleaf-consumer
133
135
  npm init -y
134
- npm install --omit=dev --ignore-scripts ../specleaf/specleaf-1.1.0.tgz
136
+ npm install --omit=dev --ignore-scripts ../specleaf/specleaf-1.1.1.tgz
135
137
  npm exec -- specleaf ../specleaf/examples/minimal.json -o ./api.html
136
138
  npx specleaf --version
137
139
  ```
@@ -214,6 +216,7 @@ CI 在 Linux、macOS 和 Windows 上分别使用 Node.js 22.19.0 与 24 运行
214
216
 
215
217
  ```text
216
218
  build/contract.ts 本地契约解析、模块发现、统一加载与错误上下文
219
+ build/references.ts 引用来源追踪、入口返回引用归并与离线引用检查
217
220
  build/openapi-plugin.ts 开发模式虚拟模块和引用监听
218
221
  build/serialize.ts HTML JSON 数据块的安全序列化
219
222
  build/package.ts 预编译 ESM CLI、通用 IIFE、CSS 和图标
package/lib/cli.js CHANGED
@@ -1,11 +1,55 @@
1
1
  #!/usr/bin/env node
2
2
  import { mkdir, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import $RefParser from "@apidevtools/json-schema-ref-parser";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import $RefParser, { getJsonSchemaRefParserDefaultOptions } from "@apidevtools/json-schema-ref-parser";
6
6
  import { randomUUID } from "node:crypto";
7
7
  // src/lib/openapi.ts
8
8
  var pointerEscape = (value) => value.replace(/~/g, "~0").replace(/\//g, "~1");
9
+ // build/references.ts
10
+ function rootReferenceTracker(entry, excluded) {
11
+ const root = pathToFileURL(entry);
12
+ const origins = /* @__PURE__ */ new WeakMap();
13
+ function remember(value, source, seen = /* @__PURE__ */ new WeakSet(), namedResource = false) {
14
+ if (!value || typeof value !== "object" || seen.has(value)) return;
15
+ seen.add(value);
16
+ namedResource ||= typeof value.$id === "string";
17
+ if (!namedResource) origins.set(value, source);
18
+ for (const child of Object.values(value)) remember(child, source, seen, namedResource);
19
+ }
20
+ const defaults = getJsonSchemaRefParserDefaultOptions().parse;
21
+ const parsers = Object.fromEntries(["json", "yaml"].map((name) => {
22
+ const plugin = defaults[name];
23
+ const parse = plugin.parse;
24
+ return [name, {
25
+ ...plugin,
26
+ async parse(file) {
27
+ const value = typeof parse === "function" ? await parse.call(this, file) : parse;
28
+ remember(value, new URL(file.url.replace(/^([a-z]):\//i, "file:///$1:/"), root));
29
+ return value;
30
+ }
31
+ }];
32
+ }));
33
+ function internalize(document) {
34
+ const seen = /* @__PURE__ */ new WeakSet();
35
+ function visit(value, path = "#") {
36
+ if (!value || typeof value !== "object" || excluded(path, value) || seen.has(value)) return;
37
+ seen.add(value);
38
+ if (typeof value.$ref === "string" && !value.$ref.startsWith("#")) {
39
+ const source = origins.get(value);
40
+ const target = source ? new URL(value.$ref, source) : void 0;
41
+ if (target && target.href.split("#")[0] === root.href) value.$ref = target.hash || "#";
42
+ if (!value.$ref.startsWith("#")) throw new Error(`契约合并后仍有外部引用:${path}/$ref → ${value.$ref}`);
43
+ }
44
+ for (const [key, child] of Object.entries(value)) visit(child, `${path}/${pointerEscape(key)}`);
45
+ }
46
+ visit(document);
47
+ }
48
+ return {
49
+ parsers,
50
+ internalize
51
+ };
52
+ }
9
53
  // build/contract.ts
10
54
  var DocumentationLoadError = class extends Error {
11
55
  files;
@@ -66,9 +110,11 @@ async function loadContract(entry) {
66
110
  entry = contractEntry(process.cwd(), entry);
67
111
  const parser = new $RefParser();
68
112
  try {
113
+ const references = rootReferenceTracker(entry, literalPayload);
69
114
  const document = await parser.bundle(entry, {
70
115
  resolve: { http: false },
71
116
  parse: {
117
+ ...references.parsers,
72
118
  binary: false,
73
119
  text: false
74
120
  },
@@ -77,6 +123,7 @@ async function loadContract(entry) {
77
123
  });
78
124
  if (!/^3\.(0|1)\.\d+$/.test(document.openapi || "")) throw new Error(`仅支持 OpenAPI 3.0 / 3.1,当前版本:${document.openapi ?? "缺失"}`);
79
125
  if (!document.info?.title || !document.paths || typeof document.paths !== "object" || Array.isArray(document.paths)) throw new Error("契约必须包含 info.title 和 paths 对象。");
126
+ references.internalize(document);
80
127
  return {
81
128
  document,
82
129
  files: parser.$refs.paths(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specleaf",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "将本地 OpenAPI 3.0 / 3.1 YAML / JSON 文件或模块目录转为按模块分组、离线阅读的单文件 HTML 文档。",
5
5
  "license": "MIT",
6
6
  "repository": {