sourcey 3.6.0 → 3.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/adapters/index.d.ts +3 -2
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +35 -3
- package/dist/adapters/types.d.ts +8 -2
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/cli.js +28 -34
- package/dist/client/search.js +181 -119
- package/dist/client/tabs.js +23 -0
- package/dist/components/layout/Head.js +2 -2
- package/dist/components/openapi/Introduction.d.ts.map +1 -1
- package/dist/components/openapi/Introduction.js +16 -1
- package/dist/components/openapi/Security.d.ts.map +1 -1
- package/dist/components/openapi/Security.js +18 -2
- package/dist/config.d.ts +63 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -7
- package/dist/core/api-rendering.d.ts +47 -0
- package/dist/core/api-rendering.d.ts.map +1 -0
- package/dist/core/api-rendering.js +76 -0
- package/dist/core/doxygen-loader.d.ts.map +1 -1
- package/dist/core/doxygen-loader.js +35 -18
- package/dist/core/godoc-loader.d.ts.map +1 -1
- package/dist/core/godoc-loader.js +17 -6
- package/dist/core/markdown-loader.d.ts.map +1 -1
- package/dist/core/markdown-loader.js +8 -5
- package/dist/core/mcp-normalizer.d.ts.map +1 -1
- package/dist/core/mcp-normalizer.js +48 -34
- package/dist/core/parser.d.ts.map +1 -1
- package/dist/core/parser.js +75 -4
- package/dist/core/rustdoc-introspector.d.ts +29 -0
- package/dist/core/rustdoc-introspector.d.ts.map +1 -0
- package/dist/core/rustdoc-introspector.js +236 -0
- package/dist/core/rustdoc-loader.d.ts +25 -0
- package/dist/core/rustdoc-loader.d.ts.map +1 -0
- package/dist/core/rustdoc-loader.js +469 -0
- package/dist/core/rustdoc-render.d.ts +76 -0
- package/dist/core/rustdoc-render.d.ts.map +1 -0
- package/dist/core/rustdoc-render.js +785 -0
- package/dist/core/rustdoc-types.d.ts +314 -0
- package/dist/core/rustdoc-types.d.ts.map +1 -0
- package/dist/core/rustdoc-types.js +18 -0
- package/dist/core/search-indexer.js +1 -1
- package/dist/core/sourcey-rustdoc/Cargo.lock +705 -0
- package/dist/core/sourcey-rustdoc/Cargo.toml +38 -0
- package/dist/core/sourcey-rustdoc/README.md +59 -0
- package/dist/core/sourcey-rustdoc/src/diagnostics.rs +42 -0
- package/dist/core/sourcey-rustdoc/src/doctest.rs +268 -0
- package/dist/core/sourcey-rustdoc/src/extract.rs +492 -0
- package/dist/core/sourcey-rustdoc/src/lib.rs +58 -0
- package/dist/core/sourcey-rustdoc/src/links.rs +126 -0
- package/dist/core/sourcey-rustdoc/src/main.rs +256 -0
- package/dist/core/sourcey-rustdoc/src/signature.rs +415 -0
- package/dist/core/sourcey-rustdoc/src/spec.rs +390 -0
- package/dist/core/sourcey-rustdoc/tests/format_version.rs +45 -0
- package/dist/dev-server.d.ts.map +1 -1
- package/dist/dev-server.js +49 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -13
- package/dist/init.js +2 -2
- package/dist/renderer/changelog-feed.d.ts.map +1 -1
- package/dist/renderer/changelog-feed.js +27 -19
- package/dist/renderer/html-builder.d.ts.map +1 -1
- package/dist/renderer/html-builder.js +4 -3
- package/dist/renderer/llms.js +57 -2
- package/dist/site-assembly.d.ts +5 -0
- package/dist/site-assembly.d.ts.map +1 -1
- package/dist/site-assembly.js +55 -1
- package/dist/themes/default/sourcey.css +300 -0
- package/dist/utils/code-samples.d.ts +1 -0
- package/dist/utils/code-samples.d.ts.map +1 -1
- package/dist/utils/code-samples.js +30 -22
- package/dist/utils/html.d.ts +10 -0
- package/dist/utils/html.d.ts.map +1 -0
- package/dist/utils/html.js +152 -0
- package/dist/utils/http.d.ts +0 -2
- package/dist/utils/http.d.ts.map +1 -1
- package/dist/utils/http.js +19 -34
- package/dist/utils/markdown.d.ts.map +1 -1
- package/dist/utils/markdown.js +102 -37
- package/package.json +9 -4
|
@@ -9,12 +9,14 @@ import { generateExample } from "../utils/example-generator.js";
|
|
|
9
9
|
// ── Public API ────────────────────────────────────────────────────────
|
|
10
10
|
export function normalizeMcpSpec(spec) {
|
|
11
11
|
const connectionInfo = {
|
|
12
|
-
transport: spec.transport
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
transport: spec.transport
|
|
13
|
+
? {
|
|
14
|
+
type: spec.transport.type,
|
|
15
|
+
command: spec.transport.command,
|
|
16
|
+
args: spec.transport.args,
|
|
17
|
+
url: spec.transport.url,
|
|
18
|
+
}
|
|
19
|
+
: undefined,
|
|
18
20
|
serverName: spec.server.name,
|
|
19
21
|
mcpVersion: spec.mcpVersion,
|
|
20
22
|
capabilities: spec.capabilities,
|
|
@@ -58,13 +60,14 @@ function toolToOperation(tool, connection) {
|
|
|
58
60
|
const group = tool["x-sourcey-group"];
|
|
59
61
|
const inputSchema = convertSchema(tool.inputSchema);
|
|
60
62
|
const outputSchema = tool.outputSchema ? convertSchema(tool.outputSchema) : undefined;
|
|
61
|
-
|
|
62
|
-
const parameters = flattenInputSchema(tool.inputSchema);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
const emitsRequestBody = hasNestedProperties(tool.inputSchema);
|
|
64
|
+
const parameters = emitsRequestBody ? [] : flattenInputSchema(tool.inputSchema);
|
|
65
|
+
const requestBody = emitsRequestBody
|
|
66
|
+
? {
|
|
67
|
+
required: true,
|
|
68
|
+
content: { "application/json": { schema: inputSchema } },
|
|
69
|
+
}
|
|
70
|
+
: undefined;
|
|
68
71
|
return {
|
|
69
72
|
operationId: tool.name,
|
|
70
73
|
summary: tool.title ?? tool.name,
|
|
@@ -80,12 +83,14 @@ function toolToOperation(tool, connection) {
|
|
|
80
83
|
codeSamples: generateToolSamples(tool, inputSchema),
|
|
81
84
|
mcpExtras: {
|
|
82
85
|
type: "tool",
|
|
83
|
-
annotations: tool.annotations
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
annotations: tool.annotations
|
|
87
|
+
? {
|
|
88
|
+
readOnlyHint: tool.annotations.readOnlyHint,
|
|
89
|
+
destructiveHint: tool.annotations.destructiveHint,
|
|
90
|
+
idempotentHint: tool.annotations.idempotentHint,
|
|
91
|
+
openWorldHint: tool.annotations.openWorldHint,
|
|
92
|
+
}
|
|
93
|
+
: undefined,
|
|
89
94
|
outputSchema,
|
|
90
95
|
connection,
|
|
91
96
|
},
|
|
@@ -136,7 +141,7 @@ function resourceTemplateToOperation(template, connection) {
|
|
|
136
141
|
// ── Prompt → Operation ────────────────────────────────────────────────
|
|
137
142
|
function promptToOperation(prompt, connection) {
|
|
138
143
|
const group = prompt["x-sourcey-group"];
|
|
139
|
-
const parameters = (prompt.arguments ?? []).map(arg => ({
|
|
144
|
+
const parameters = (prompt.arguments ?? []).map((arg) => ({
|
|
140
145
|
name: arg.name,
|
|
141
146
|
in: "argument",
|
|
142
147
|
description: arg.description,
|
|
@@ -189,18 +194,20 @@ function convertSchema(schema, name) {
|
|
|
189
194
|
}
|
|
190
195
|
}
|
|
191
196
|
if (schema.additionalProperties !== undefined) {
|
|
192
|
-
result.additionalProperties =
|
|
193
|
-
|
|
194
|
-
|
|
197
|
+
result.additionalProperties =
|
|
198
|
+
typeof schema.additionalProperties === "boolean"
|
|
199
|
+
? schema.additionalProperties
|
|
200
|
+
: convertSchema(schema.additionalProperties);
|
|
195
201
|
}
|
|
196
202
|
if (schema.required)
|
|
197
203
|
result.required = schema.required;
|
|
198
204
|
// Array — handle JsonSchema.items being JsonSchema | JsonSchema[]
|
|
199
205
|
if (schema.items) {
|
|
200
206
|
if (Array.isArray(schema.items)) {
|
|
201
|
-
result.items =
|
|
202
|
-
|
|
203
|
-
|
|
207
|
+
result.items =
|
|
208
|
+
schema.items.length === 1
|
|
209
|
+
? convertSchema(schema.items[0])
|
|
210
|
+
: { oneOf: schema.items.map((s) => convertSchema(s)) };
|
|
204
211
|
}
|
|
205
212
|
else {
|
|
206
213
|
result.items = convertSchema(schema.items);
|
|
@@ -265,7 +272,10 @@ function hasNestedProperties(schema) {
|
|
|
265
272
|
return Object.values(schema.properties).some((prop) => {
|
|
266
273
|
if (prop.type === "object" && prop.properties)
|
|
267
274
|
return true;
|
|
268
|
-
if (prop.type === "array" &&
|
|
275
|
+
if (prop.type === "array" &&
|
|
276
|
+
prop.items &&
|
|
277
|
+
!Array.isArray(prop.items) &&
|
|
278
|
+
prop.items.type === "object")
|
|
269
279
|
return true;
|
|
270
280
|
if (prop.allOf || prop.anyOf || prop.oneOf)
|
|
271
281
|
return true;
|
|
@@ -276,7 +286,7 @@ function extractUriTemplateParams(uriTemplate) {
|
|
|
276
286
|
const matches = uriTemplate.match(/\{([^}]+)\}/g);
|
|
277
287
|
if (!matches)
|
|
278
288
|
return [];
|
|
279
|
-
return matches.map(m => ({
|
|
289
|
+
return matches.map((m) => ({
|
|
280
290
|
name: m.slice(1, -1),
|
|
281
291
|
in: "path",
|
|
282
292
|
required: true,
|
|
@@ -289,11 +299,12 @@ function slugify(name) {
|
|
|
289
299
|
}
|
|
290
300
|
// ── Code sample generation ────────────────────────────────────────────
|
|
291
301
|
function generateToolSamples(tool, inputSchema) {
|
|
292
|
-
const example = inputSchema.properties
|
|
293
|
-
? generateExample(inputSchema)
|
|
294
|
-
: {};
|
|
302
|
+
const example = inputSchema.properties ? generateExample(inputSchema) : {};
|
|
295
303
|
const exampleJson = JSON.stringify(example, null, 2);
|
|
296
|
-
const indented = exampleJson
|
|
304
|
+
const indented = exampleJson
|
|
305
|
+
.split("\n")
|
|
306
|
+
.map((line, i) => (i === 0 ? line : " " + line))
|
|
307
|
+
.join("\n");
|
|
297
308
|
return [
|
|
298
309
|
{
|
|
299
310
|
lang: "json",
|
|
@@ -345,10 +356,13 @@ function generateResourceSamples(resource) {
|
|
|
345
356
|
];
|
|
346
357
|
}
|
|
347
358
|
function generatePromptSamples(prompt) {
|
|
348
|
-
const args =
|
|
359
|
+
const args = prompt.arguments ?? [];
|
|
349
360
|
const exampleArgs = Object.fromEntries(args.map((a) => [a.name, `<${a.name}>`]));
|
|
350
361
|
const argsJson = JSON.stringify(exampleArgs, null, 2);
|
|
351
|
-
const indented = argsJson
|
|
362
|
+
const indented = argsJson
|
|
363
|
+
.split("\n")
|
|
364
|
+
.map((line, i) => (i === 0 ? line : " " + line))
|
|
365
|
+
.join("\n");
|
|
352
366
|
const tsArgs = args.map((a) => ` ${a.name}: "<${a.name}>",`).join("\n");
|
|
353
367
|
const pyArgs = args.map((a) => ` "${a.name}": "<${a.name}>",`).join("\n");
|
|
354
368
|
return [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/core/parser.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/core/parser.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAmB,UAAU,EAAE,MAAM,YAAY,CAAC;AAE1E;;;;;;GAMG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CA+BvE"}
|
package/dist/core/parser.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { $RefParser } from "@apidevtools/json-schema-ref-parser";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
2
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
5
|
/**
|
|
4
6
|
* Dereference a loaded spec document.
|
|
@@ -10,7 +12,21 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
10
12
|
export async function parseSpec(loaded) {
|
|
11
13
|
try {
|
|
12
14
|
const parser = new $RefParser();
|
|
13
|
-
const dereferenced = await parser.dereference(resolveDocumentUri(loaded), structuredClone(loaded.raw), {
|
|
15
|
+
const dereferenced = await parser.dereference(resolveDocumentUri(loaded), structuredClone(loaded.raw), {
|
|
16
|
+
dereference: {
|
|
17
|
+
circular: "ignore",
|
|
18
|
+
onDereference(ref, value) {
|
|
19
|
+
const refName = refNameFromRef(ref);
|
|
20
|
+
if (refName && isRecord(value) && typeof value["x-ref-name"] !== "string") {
|
|
21
|
+
value["x-ref-name"] = refName;
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
resolve: {
|
|
26
|
+
http: false,
|
|
27
|
+
file: createConfinedFileResolver(loaded),
|
|
28
|
+
},
|
|
29
|
+
});
|
|
14
30
|
return {
|
|
15
31
|
document: dereferenced,
|
|
16
32
|
source: loaded.source,
|
|
@@ -21,6 +37,63 @@ export async function parseSpec(loaded) {
|
|
|
21
37
|
throw new Error(`Failed to parse OpenAPI spec: ${message}`);
|
|
22
38
|
}
|
|
23
39
|
}
|
|
40
|
+
function createConfinedFileResolver(loaded) {
|
|
41
|
+
const root = localSpecRoot(loaded.source);
|
|
42
|
+
if (!root)
|
|
43
|
+
return false;
|
|
44
|
+
return {
|
|
45
|
+
order: 1,
|
|
46
|
+
canRead(file) {
|
|
47
|
+
return filePathFromRef(file.url) !== null;
|
|
48
|
+
},
|
|
49
|
+
async read(file) {
|
|
50
|
+
const filePath = filePathFromRef(file.url);
|
|
51
|
+
if (!filePath)
|
|
52
|
+
throw new Error(`Unsupported external $ref URI: ${file.url}`);
|
|
53
|
+
const resolved = resolve(filePath);
|
|
54
|
+
if (!isWithinRoot(root, resolved)) {
|
|
55
|
+
throw new Error(`External $ref resolves outside the spec directory: ${file.url}`);
|
|
56
|
+
}
|
|
57
|
+
return readFile(resolved);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function localSpecRoot(source) {
|
|
62
|
+
if (source.startsWith("http://") || source.startsWith("https://"))
|
|
63
|
+
return null;
|
|
64
|
+
return dirname(resolve(source));
|
|
65
|
+
}
|
|
66
|
+
function filePathFromRef(refUrl) {
|
|
67
|
+
const withoutHash = refUrl.split("#", 1)[0];
|
|
68
|
+
if (!withoutHash)
|
|
69
|
+
return null;
|
|
70
|
+
if (/^file:/i.test(withoutHash)) {
|
|
71
|
+
try {
|
|
72
|
+
return fileURLToPath(withoutHash);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(withoutHash))
|
|
79
|
+
return null;
|
|
80
|
+
return resolve(withoutHash);
|
|
81
|
+
}
|
|
82
|
+
function isWithinRoot(root, target) {
|
|
83
|
+
const rel = relative(root, target);
|
|
84
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
85
|
+
}
|
|
86
|
+
function refNameFromRef(ref) {
|
|
87
|
+
const pointer = ref.includes("#") ? ref.slice(ref.indexOf("#") + 1) : ref;
|
|
88
|
+
const segments = pointer.split("/").filter(Boolean);
|
|
89
|
+
const raw = segments.at(-1);
|
|
90
|
+
if (!raw)
|
|
91
|
+
return null;
|
|
92
|
+
return raw.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
93
|
+
}
|
|
94
|
+
function isRecord(value) {
|
|
95
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
96
|
+
}
|
|
24
97
|
function resolveDocumentUri(loaded) {
|
|
25
98
|
const self = typeof loaded.raw["$self"] === "string" ? loaded.raw["$self"] : undefined;
|
|
26
99
|
if (!self)
|
|
@@ -30,9 +103,7 @@ function resolveDocumentUri(loaded) {
|
|
|
30
103
|
return new URL(self, loaded.source).toString();
|
|
31
104
|
}
|
|
32
105
|
const resolved = new URL(self, pathToFileURL(loaded.source));
|
|
33
|
-
return resolved.protocol === "file:"
|
|
34
|
-
? fileURLToPath(resolved)
|
|
35
|
-
: resolved.toString();
|
|
106
|
+
return resolved.protocol === "file:" ? fileURLToPath(resolved) : resolved.toString();
|
|
36
107
|
}
|
|
37
108
|
catch {
|
|
38
109
|
return loaded.source;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ResolvedRustdocConfig } from "../config.js";
|
|
2
|
+
import type { RustdocDiagnostic, RustdocSpec } from "./rustdoc-types.js";
|
|
3
|
+
export declare class RustdocIntrospectorError extends Error {
|
|
4
|
+
readonly code: string;
|
|
5
|
+
readonly cause?: Error | undefined;
|
|
6
|
+
constructor(code: string, message: string, cause?: Error);
|
|
7
|
+
}
|
|
8
|
+
export interface IntrospectOptions {
|
|
9
|
+
config: ResolvedRustdocConfig;
|
|
10
|
+
/** Override cargo binary; defaults to "cargo" on PATH. */
|
|
11
|
+
cargoBinary?: string;
|
|
12
|
+
/** Override rustup binary; defaults to "rustup" on PATH. */
|
|
13
|
+
rustupBinary?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface IntrospectResult {
|
|
16
|
+
spec: RustdocSpec;
|
|
17
|
+
diagnostics: RustdocDiagnostic[];
|
|
18
|
+
/** Mode actually used (after auto resolution). */
|
|
19
|
+
modeUsed: "live" | "snapshot";
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Run the bundled Rust helper or load a committed snapshot, return a
|
|
23
|
+
* parsed RustdocSpec plus any diagnostics. Mode is resolved per the
|
|
24
|
+
* spec's auto fallback policy.
|
|
25
|
+
*/
|
|
26
|
+
export declare function runIntrospector(opts: IntrospectOptions): Promise<IntrospectResult>;
|
|
27
|
+
/** Resolved helper entry paths, exported for tests that pin runtime. */
|
|
28
|
+
export declare const __rustdocHelperCandidatesForTests: string[];
|
|
29
|
+
//# sourceMappingURL=rustdoc-introspector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rustdoc-introspector.d.ts","sourceRoot":"","sources":["../../src/core/rustdoc-introspector.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAE1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAQzE,qBAAa,wBAAyB,SAAQ,KAAK;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;gBACvB,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK;CAMzD;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,qBAAqB,CAAC;IAC9B,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,WAAW,CAAC;IAClB,WAAW,EAAE,iBAAiB,EAAE,CAAC;IACjC,kDAAkD;IAClD,QAAQ,EAAE,MAAM,GAAG,UAAU,CAAC;CAC/B;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAexF;AAwPD,wEAAwE;AACxE,eAAO,MAAM,iCAAiC,UAG7C,CAAC"}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { access, readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { gunzipSync } from "node:zlib";
|
|
6
|
+
import { RUSTDOC_DIAGNOSTIC_CODES, SPEC_VERSION } from "./rustdoc-types.js";
|
|
7
|
+
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const PACKAGED_HELPER_DIR = join(MODULE_DIR, "sourcey-rustdoc");
|
|
9
|
+
const DEV_HELPER_DIR = resolve(MODULE_DIR, "../../rust/sourcey-rustdoc");
|
|
10
|
+
const HELPER_MANIFEST = "Cargo.toml";
|
|
11
|
+
const HELPER_ENTRY = "src/main.rs";
|
|
12
|
+
export class RustdocIntrospectorError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
cause;
|
|
15
|
+
constructor(code, message, cause) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "RustdocIntrospectorError";
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.cause = cause;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Run the bundled Rust helper or load a committed snapshot, return a
|
|
24
|
+
* parsed RustdocSpec plus any diagnostics. Mode is resolved per the
|
|
25
|
+
* spec's auto fallback policy.
|
|
26
|
+
*/
|
|
27
|
+
export async function runIntrospector(opts) {
|
|
28
|
+
const cfg = opts.config;
|
|
29
|
+
const mode = await resolveMode(cfg, opts);
|
|
30
|
+
if (mode === "snapshot") {
|
|
31
|
+
const result = await loadSnapshot(cfg);
|
|
32
|
+
if (cfg.mode === "auto") {
|
|
33
|
+
result.diagnostics.unshift({
|
|
34
|
+
severity: "info",
|
|
35
|
+
code: RUSTDOC_DIAGNOSTIC_CODES.AUTO_FELL_BACK_TO_SNAPSHOT,
|
|
36
|
+
message: `Rustdoc auto mode did not find "${cfg.toolchain}" and used the configured snapshot.`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
return runLive(cfg, opts);
|
|
42
|
+
}
|
|
43
|
+
async function resolveMode(cfg, opts) {
|
|
44
|
+
if (cfg.mode === "live")
|
|
45
|
+
return "live";
|
|
46
|
+
if (cfg.mode === "snapshot") {
|
|
47
|
+
if (!cfg.snapshot) {
|
|
48
|
+
throw new RustdocIntrospectorError(RUSTDOC_DIAGNOSTIC_CODES.NO_VIABLE_MODE, "Rustdoc adapter mode is 'snapshot' but no snapshot path was configured.");
|
|
49
|
+
}
|
|
50
|
+
return "snapshot";
|
|
51
|
+
}
|
|
52
|
+
// auto
|
|
53
|
+
const nightlyAvailable = await probeNightly(opts.rustupBinary ?? "rustup", cfg.toolchain);
|
|
54
|
+
if (nightlyAvailable)
|
|
55
|
+
return "live";
|
|
56
|
+
if (cfg.snapshot)
|
|
57
|
+
return "snapshot";
|
|
58
|
+
throw new RustdocIntrospectorError(RUSTDOC_DIAGNOSTIC_CODES.NO_VIABLE_MODE, `Rustdoc adapter "auto" mode found neither the "${cfg.toolchain}" toolchain nor a configured snapshot. ` +
|
|
59
|
+
`Either run \`rustup toolchain install ${cfg.toolchain}\` or set { mode: "snapshot", snapshot: "./snapshots/rustdoc.json" }.`);
|
|
60
|
+
}
|
|
61
|
+
async function probeNightly(rustupBinary, toolchain) {
|
|
62
|
+
try {
|
|
63
|
+
const result = spawnSync(rustupBinary, ["toolchain", "list"], {
|
|
64
|
+
encoding: "utf8",
|
|
65
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
66
|
+
});
|
|
67
|
+
if (result.status !== 0 || !result.stdout)
|
|
68
|
+
return false;
|
|
69
|
+
return result.stdout.split("\n").some((line) => line.includes(toolchain));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function loadSnapshot(cfg) {
|
|
76
|
+
const snapshotPath = cfg.snapshot;
|
|
77
|
+
if (!snapshotPath) {
|
|
78
|
+
throw new RustdocIntrospectorError(RUSTDOC_DIAGNOSTIC_CODES.NO_VIABLE_MODE, "Snapshot mode requires `snapshot` to point at a committed rustdoc.json.");
|
|
79
|
+
}
|
|
80
|
+
let raw;
|
|
81
|
+
try {
|
|
82
|
+
if (/\.gz$/i.test(snapshotPath)) {
|
|
83
|
+
const buf = await readFile(snapshotPath);
|
|
84
|
+
raw = gunzipSync(buf).toString("utf8");
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
raw = await readFile(snapshotPath, "utf8");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
throw new RustdocIntrospectorError(RUSTDOC_DIAGNOSTIC_CODES.INVALID_SNAPSHOT_SCHEMA, `Could not read snapshot at ${snapshotPath}: ${err.message}`, err);
|
|
92
|
+
}
|
|
93
|
+
let spec;
|
|
94
|
+
try {
|
|
95
|
+
spec = JSON.parse(raw);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
throw new RustdocIntrospectorError(RUSTDOC_DIAGNOSTIC_CODES.INVALID_SNAPSHOT_SCHEMA, `Snapshot at ${snapshotPath} is not valid JSON: ${err.message}`, err);
|
|
99
|
+
}
|
|
100
|
+
validateSpec(spec, snapshotPath);
|
|
101
|
+
return {
|
|
102
|
+
spec,
|
|
103
|
+
diagnostics: collectDiagnostics(spec),
|
|
104
|
+
modeUsed: "snapshot",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function runLive(cfg, opts) {
|
|
108
|
+
const helperDir = await resolveHelperDir();
|
|
109
|
+
const cargoBinary = opts.cargoBinary ?? "cargo";
|
|
110
|
+
const args = buildArgs(cfg, helperDir);
|
|
111
|
+
const { stdout, stderr, code } = await runCargo(cargoBinary, args, helperDir);
|
|
112
|
+
if (code !== 0) {
|
|
113
|
+
throw new RustdocIntrospectorError(RUSTDOC_DIAGNOSTIC_CODES.HELPER_FAILED, `sourcey-rustdoc helper exited with code ${code}:\n${stderr.trim()}`);
|
|
114
|
+
}
|
|
115
|
+
let spec;
|
|
116
|
+
try {
|
|
117
|
+
spec = JSON.parse(stdout);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
throw new RustdocIntrospectorError(RUSTDOC_DIAGNOSTIC_CODES.HELPER_FAILED, `Could not parse helper output as JSON: ${err.message}\n` +
|
|
121
|
+
`stdout (first 200 chars): ${stdout.slice(0, 200)}`, err);
|
|
122
|
+
}
|
|
123
|
+
validateSpec(spec, "live invocation");
|
|
124
|
+
return {
|
|
125
|
+
spec,
|
|
126
|
+
diagnostics: collectDiagnostics(spec),
|
|
127
|
+
modeUsed: "live",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function buildArgs(cfg, helperDir) {
|
|
131
|
+
void helperDir;
|
|
132
|
+
const args = [
|
|
133
|
+
"run",
|
|
134
|
+
"--release",
|
|
135
|
+
"--quiet",
|
|
136
|
+
"--",
|
|
137
|
+
"--manifest",
|
|
138
|
+
cfg.manifest,
|
|
139
|
+
"--toolchain",
|
|
140
|
+
cfg.toolchain,
|
|
141
|
+
"--output",
|
|
142
|
+
"-",
|
|
143
|
+
];
|
|
144
|
+
for (const crateName of cfg.crates) {
|
|
145
|
+
args.push("--crate", crateName);
|
|
146
|
+
}
|
|
147
|
+
if (cfg.features.all) {
|
|
148
|
+
args.push("--all-features");
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
if (cfg.features.default === false) {
|
|
152
|
+
args.push("--no-default-features");
|
|
153
|
+
}
|
|
154
|
+
for (const feature of cfg.features.list) {
|
|
155
|
+
args.push("--features", feature);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (cfg.includePrivate)
|
|
159
|
+
args.push("--include-private");
|
|
160
|
+
if (cfg.includeHidden)
|
|
161
|
+
args.push("--include-hidden");
|
|
162
|
+
if (cfg.target)
|
|
163
|
+
args.push("--target", cfg.target);
|
|
164
|
+
return args;
|
|
165
|
+
}
|
|
166
|
+
async function resolveHelperDir() {
|
|
167
|
+
const candidates = [PACKAGED_HELPER_DIR, DEV_HELPER_DIR];
|
|
168
|
+
for (const candidate of candidates) {
|
|
169
|
+
try {
|
|
170
|
+
await access(join(candidate, HELPER_MANIFEST));
|
|
171
|
+
await access(join(candidate, HELPER_ENTRY));
|
|
172
|
+
return candidate;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// try next
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
throw new RustdocIntrospectorError(RUSTDOC_DIAGNOSTIC_CODES.HELPER_MISSING, "Sourcey's Rust documentation helper is missing. Checked:\n" +
|
|
179
|
+
candidates.map((c) => `- ${join(c, HELPER_ENTRY)}`).join("\n") +
|
|
180
|
+
"\nReinstall sourcey or run `npm run build`.");
|
|
181
|
+
}
|
|
182
|
+
function runCargo(binary, args, cwd) {
|
|
183
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
184
|
+
let child;
|
|
185
|
+
try {
|
|
186
|
+
child = spawn(binary, args, { cwd, env: process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
rejectPromise(new RustdocIntrospectorError(RUSTDOC_DIAGNOSTIC_CODES.HELPER_FAILED, `Could not launch cargo (${binary}). Install Rust from https://rustup.rs, or use mode: "snapshot" with a committed rustdoc.json.`, err));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const stdoutChunks = [];
|
|
193
|
+
const stderrChunks = [];
|
|
194
|
+
child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
195
|
+
child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
|
196
|
+
child.on("error", (err) => {
|
|
197
|
+
if (err.code === "ENOENT") {
|
|
198
|
+
rejectPromise(new RustdocIntrospectorError(RUSTDOC_DIAGNOSTIC_CODES.HELPER_FAILED, `Could not launch cargo (${binary}). Install Rust from https://rustup.rs, or use mode: "snapshot" with a committed rustdoc.json.`, err));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
rejectPromise(new RustdocIntrospectorError(RUSTDOC_DIAGNOSTIC_CODES.HELPER_FAILED, err.message, err));
|
|
202
|
+
});
|
|
203
|
+
child.on("close", (code) => {
|
|
204
|
+
resolvePromise({
|
|
205
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
206
|
+
stderr: Buffer.concat(stderrChunks).toString("utf8"),
|
|
207
|
+
code: code ?? -1,
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
function validateSpec(spec, source) {
|
|
213
|
+
if (spec.version !== SPEC_VERSION) {
|
|
214
|
+
throw new RustdocIntrospectorError(RUSTDOC_DIAGNOSTIC_CODES.INVALID_SNAPSHOT_SCHEMA, `Rustdoc snapshot from ${source} reports version ${spec.version}; this sourcey expects ${SPEC_VERSION}.`);
|
|
215
|
+
}
|
|
216
|
+
if (!Array.isArray(spec.crates)) {
|
|
217
|
+
throw new RustdocIntrospectorError(RUSTDOC_DIAGNOSTIC_CODES.INVALID_SNAPSHOT_SCHEMA, `Rustdoc snapshot from ${source} is missing the \`crates\` array.`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function collectDiagnostics(spec) {
|
|
221
|
+
const out = [];
|
|
222
|
+
for (const d of spec.diagnostics ?? []) {
|
|
223
|
+
out.push(d);
|
|
224
|
+
}
|
|
225
|
+
for (const c of spec.crates) {
|
|
226
|
+
for (const d of c.diagnostics) {
|
|
227
|
+
out.push({ ...d, crate_name: d.crate_name ?? c.name });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
/** Resolved helper entry paths, exported for tests that pin runtime. */
|
|
233
|
+
export const __rustdocHelperCandidatesForTests = [
|
|
234
|
+
join(PACKAGED_HELPER_DIR, HELPER_ENTRY),
|
|
235
|
+
join(DEV_HELPER_DIR, HELPER_ENTRY),
|
|
236
|
+
];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ResolvedRustdocConfig } from "../config.js";
|
|
2
|
+
import type { MarkdownPage } from "./markdown-loader.js";
|
|
3
|
+
import type { SiteTab } from "./navigation.js";
|
|
4
|
+
export interface RustdocLoaderDiagnostic {
|
|
5
|
+
severity: "error" | "warning" | "info";
|
|
6
|
+
code: string;
|
|
7
|
+
message: string;
|
|
8
|
+
crate?: string;
|
|
9
|
+
file?: string;
|
|
10
|
+
line?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface RustdocLoaderResult {
|
|
13
|
+
pages: Map<string, MarkdownPage>;
|
|
14
|
+
navTab: SiteTab;
|
|
15
|
+
diagnostics: RustdocLoaderDiagnostic[];
|
|
16
|
+
}
|
|
17
|
+
export interface RustdocSourceLinkOptions {
|
|
18
|
+
repo?: string;
|
|
19
|
+
editBranch?: string;
|
|
20
|
+
editBasePath?: string;
|
|
21
|
+
}
|
|
22
|
+
/** Load a rustdoc tab from the configured manifest or snapshot. */
|
|
23
|
+
export declare function loadRustdocTab(config: ResolvedRustdocConfig, tabSlug: string, tabLabel: string, sourceLinks?: RustdocSourceLinkOptions): Promise<RustdocLoaderResult>;
|
|
24
|
+
export declare const RUST_SEARCH_CATEGORIES: readonly ["rust function", "rust method", "rust struct", "rust enum", "rust trait", "rust trait alias", "rust type alias", "rust constant", "rust static", "rust macro", "rust proc-macro", "rust associated type", "rust associated constant", "rust module", "rust doctest", "rust re-export", "rust variant", "rust struct field", "rust union", "rust impl", "rust primitive", "rust extern type"];
|
|
25
|
+
//# sourceMappingURL=rustdoc-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rustdoc-loader.d.ts","sourceRoot":"","sources":["../../src/core/rustdoc-loader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAgC,MAAM,sBAAsB,CAAC;AACvF,OAAO,KAAK,EAAE,OAAO,EAA6B,MAAM,iBAAiB,CAAC;AAa1E,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACjC,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,uBAAuB,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,mEAAmE;AACnE,wBAAsB,cAAc,CAClC,MAAM,EAAE,qBAAqB,EAC7B,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,WAAW,GAAE,wBAA6B,GACzC,OAAO,CAAC,mBAAmB,CAAC,CA0F9B;AA2ZD,eAAO,MAAM,sBAAsB,wYAuBzB,CAAC"}
|