@tsdoctor/model 0.1.0 → 0.2.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.
@@ -0,0 +1,11 @@
1
+ //#region src/internal/text.ts
2
+ /**
3
+ * Shared private text helpers.
4
+ *
5
+ * @internal
6
+ */
7
+ /** Escape a literal string for embedding in a RegExp pattern. */
8
+ const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9
+
10
+ //#endregion
11
+ export { escapeRegExp };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsdoctor/model",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "private": false,
5
5
  "description": "Render Microsoft API Extractor models into LLM-lean markdown. Pure model loading, TSDoc extraction, type-signature formatting, and per-item markdown rendering.",
6
6
  "keywords": [
@@ -41,6 +41,16 @@
41
41
  "@microsoft/api-extractor-model": "^7.33.10",
42
42
  "@microsoft/tsdoc": "^0.16.0"
43
43
  },
44
+ "peerDependencies": {
45
+ "@effected/markdown": "^0.7.0",
46
+ "@effected/package-json": "^0.12.0",
47
+ "effect": "4.0.0-rc.109"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "@effected/package-json": {
51
+ "optional": true
52
+ }
53
+ },
44
54
  "engines": {
45
55
  "node": ">=24.11.0"
46
56
  }
package/cross-linker.js DELETED
@@ -1,37 +0,0 @@
1
- //#region src/cross-linker.ts
2
- const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3
- /**
4
- * Links known API item names in prose to their docs, using an injected
5
- * {@link RouteFormatter} so each consumer supplies its own URL scheme.
6
- *
7
- * @public
8
- */
9
- var CrossLinker = class {
10
- byName;
11
- routeFor;
12
- constructor(refs, routeFor) {
13
- this.byName = new Map(refs.map((r) => [r.name, r]));
14
- this.routeFor = routeFor;
15
- }
16
- /** Wrap known item names in markdown links, skipping code spans + existing links. */
17
- addLinks(text) {
18
- let result = text;
19
- const names = [...this.byName.keys()].sort((a, b) => b.length - a.length);
20
- for (const name of names) {
21
- const ref = this.byName.get(name);
22
- if (!ref) continue;
23
- const route = this.routeFor(ref);
24
- const regex = new RegExp(`\\b${escapeRegExp(name)}\\b`, "g");
25
- result = result.replace(regex, (match, offset) => {
26
- const before = result.slice(0, offset);
27
- if (before.endsWith("](") || before.endsWith("[")) return match;
28
- if ((before.match(/`/g) || []).length % 2 === 1) return match;
29
- return `[${match}](${route})`;
30
- });
31
- }
32
- return result;
33
- }
34
- };
35
-
36
- //#endregion
37
- export { CrossLinker };
package/formatter.js DELETED
@@ -1,70 +0,0 @@
1
- //#region src/formatter.ts
2
- /**
3
- * Formats an API Extractor `Excerpt` into a clean, line-wrapped type signature string.
4
- *
5
- * @public
6
- */
7
- var TypeSignatureFormatter = class {
8
- maxLineLength;
9
- indent;
10
- constructor(opts = {}) {
11
- this.maxLineLength = opts.maxLineLength ?? 80;
12
- this.indent = opts.indent ?? " ";
13
- }
14
- format(excerpt) {
15
- if (!excerpt.spannedTokens || excerpt.spannedTokens.length === 0) return this.stripExportDeclare(excerpt.text);
16
- const tokens = excerpt.spannedTokens;
17
- let currentLine = "";
18
- const lines = [];
19
- let bracketDepth = 0;
20
- let lastTokenText = "";
21
- for (let i = 0; i < tokens.length; i++) {
22
- let tokenText = tokens[i].text;
23
- if (i === 0) tokenText = this.stripExportDeclare(tokenText);
24
- if (tokenText.trim() === "") continue;
25
- if (tokenText === "{" || tokenText === "[" || tokenText === "(") bracketDepth++;
26
- else if (tokenText === "}" || tokenText === "]" || tokenText === ")") bracketDepth--;
27
- const isOperator = tokenText.trim() === "|" || tokenText.trim() === "&";
28
- if (lastTokenText && this.needsSpaceBefore(lastTokenText, tokenText)) currentLine += " ";
29
- currentLine += tokenText;
30
- lastTokenText = tokenText;
31
- if (isOperator && bracketDepth === 0 && currentLine.length > this.maxLineLength && i < tokens.length - 1) {
32
- lines.push(currentLine.trimEnd());
33
- currentLine = this.indent;
34
- }
35
- }
36
- if (currentLine.trim()) lines.push(currentLine.trimEnd());
37
- if (lines.length <= 1) return lines.length === 1 ? lines[0].trimStart() : "";
38
- return this.stripExportDeclare(lines.join("\n"));
39
- }
40
- stripExportDeclare(text) {
41
- let result = text.trim().replace(/^export\s+declare\s+/i, "").replace(/^export\s+/i, "").replace(/^declare\s+/i, "");
42
- result = result.replace(/\bexport\s+declare\s+/gi, "").replace(/\bexport\s+/gi, "").replace(/\bdeclare\s+/gi, "");
43
- return result;
44
- }
45
- needsSpaceBefore(prevText, currentText) {
46
- if (/\s$/.test(prevText)) return false;
47
- if (/^\s/.test(currentText)) return false;
48
- if (currentText.trim().startsWith("<")) return false;
49
- if (currentText.trim().match(/^[,;]/)) return false;
50
- if (prevText.trim().endsWith(",")) return true;
51
- if (currentText.trim() === "=" || currentText.trim().startsWith("=")) return true;
52
- if (prevText.trim().endsWith("=")) return true;
53
- if (currentText.trim() === "|" || currentText.trim() === "&") return true;
54
- if (prevText.trim() === "|" || prevText.trim() === "&") return true;
55
- if (prevText.trim() === "{" && currentText.trim() !== "}") return true;
56
- if (currentText.trim() === "}" && prevText.trim() !== "{") return true;
57
- if (prevText.trim().match(/^[[(]$/)) return false;
58
- if (currentText.trim().match(/^[\])]$/)) return false;
59
- if (prevText.trim().endsWith(":")) return true;
60
- if (prevText.trim().endsWith("?:")) return true;
61
- if (currentText.trim().startsWith(":") && !prevText.trim().match(/[,;:?]$/)) return false;
62
- if (currentText.trim().startsWith("{") && /[a-zA-Z0-9_>]$/.test(prevText.trim())) return true;
63
- const prevEndsAlnum = /[a-zA-Z0-9_>]$/.test(prevText.trim());
64
- const currStartsAlnum = /^[a-zA-Z0-9_<]/.test(currentText.trim());
65
- return prevEndsAlnum && currStartsAlnum;
66
- }
67
- };
68
-
69
- //#endregion
70
- export { TypeSignatureFormatter };
package/model-loader.js DELETED
@@ -1,24 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- import { ApiModel } from "@microsoft/api-extractor-model";
4
-
5
- //#region src/model-loader.ts
6
- /**
7
- * Load a Microsoft API Extractor `.api.json` model from disk and return its
8
- * single `ApiPackage`. Ported from rspress-plugin-api-extractor's ApiModelLoader.
9
- *
10
- * @packageDocumentation
11
- */
12
- /**
13
- * Load a `.api.json` model file and return its first (only) package.
14
- *
15
- * @public
16
- */
17
- async function loadApiModel(modelPath) {
18
- const resolved = resolve(modelPath);
19
- if (!existsSync(resolved)) throw new Error(`API model file not found: ${resolved}`);
20
- return new ApiModel().loadPackage(resolved);
21
- }
22
-
23
- //#endregion
24
- export { loadApiModel };
package/render.js DELETED
@@ -1,118 +0,0 @@
1
- import { CrossLinker } from "./cross-linker.js";
2
- import { TypeSignatureFormatter } from "./formatter.js";
3
- import { getDeprecation, getExamples, getParams, getReturns, getSummary } from "./tsdoc.js";
4
- import { ApiItemContainerMixin } from "@microsoft/api-extractor-model";
5
-
6
- //#region src/render.ts
7
- const KIND_SLUG = {
8
- Class: "class",
9
- Interface: "interface",
10
- Function: "function",
11
- TypeAlias: "type",
12
- Variable: "variable",
13
- Enum: "enum",
14
- Namespace: "namespace"
15
- };
16
- /**
17
- * The default emit rule for {@link renderPackage}: drop compiler-synthetic
18
- * forgotten exports — items the model retains only because API Extractor ran with
19
- * `includeForgottenExports: true` (e.g. the `*_base` classes TypeScript hoists for
20
- * Effect class mixins). Those carry `isExported === false` on `ApiExportedMixin`.
21
- * Every other item, including any lacking the flag, is kept.
22
- *
23
- * @public
24
- */
25
- const isEmittable = (item) => item.isExported !== false;
26
- const formatter = new TypeSignatureFormatter();
27
- const signatureOf = (item) => {
28
- const declared = item;
29
- return declared.excerpt?.text ? formatter.format(declared.excerpt).trim() : "";
30
- };
31
- /**
32
- * Render one API item to a markdown body (no frontmatter).
33
- *
34
- * @public
35
- */
36
- function renderItem(item, opts) {
37
- const link = (text) => opts.crossLinker ? opts.crossLinker.addLinks(text) : text;
38
- const lines = [`# ${item.displayName}`, ""];
39
- const deprecation = getDeprecation(item);
40
- if (deprecation) lines.push(`> **Deprecated:** ${link(deprecation.message)}`, "");
41
- const summary = getSummary(item);
42
- if (summary) lines.push(link(summary), "");
43
- const signature = signatureOf(item);
44
- if (signature) lines.push("```ts", signature, "```", "");
45
- const params = getParams(item);
46
- if (params.length > 0) {
47
- lines.push("## Parameters", "");
48
- for (const p of params) {
49
- const type = p.type ? ` \`${p.type}\`` : "";
50
- const desc = p.description ? ` — ${link(p.description)}` : "";
51
- lines.push(`- \`${p.name}\`${type}${desc}`);
52
- }
53
- lines.push("");
54
- }
55
- const returns = getReturns(item);
56
- if (returns) lines.push("## Returns", "", link(returns.description), "");
57
- const members = item.members;
58
- if (Array.isArray(members) && members.length > 0 && ApiItemContainerMixin.isBaseClassOf(item)) {
59
- lines.push("## Members", "");
60
- for (const m of members) {
61
- const sig = signatureOf(m);
62
- const mSummary = getSummary(m);
63
- lines.push(`### ${m.displayName}`, "");
64
- if (sig) lines.push("```ts", sig, "```", "");
65
- if (mSummary) lines.push(link(mSummary), "");
66
- }
67
- }
68
- const examples = getExamples(item);
69
- if (examples.length > 0) {
70
- lines.push("## Examples", "");
71
- for (const ex of examples) lines.push(`\`\`\`${ex.language}`, ex.code, "```", "");
72
- }
73
- return `${lines.join("\n").trim()}\n`;
74
- }
75
- /**
76
- * Walk a package's first entry point and assemble one RenderedDoc per top-level member.
77
- *
78
- * @public
79
- */
80
- function renderPackage(apiPackage, opts) {
81
- const entryPoint = apiPackage.entryPoints[0];
82
- if (!entryPoint) return [];
83
- const keep = opts.filter ?? isEmittable;
84
- const pairs = [];
85
- for (const member of entryPoint.members) {
86
- const kind = KIND_SLUG[member.kind];
87
- if (kind === void 0) continue;
88
- if (!keep(member)) continue;
89
- pairs.push({
90
- item: member,
91
- ref: {
92
- name: member.displayName,
93
- kind,
94
- slug: member.displayName.toLowerCase()
95
- }
96
- });
97
- }
98
- const crossLinker = opts.routeFor ? new CrossLinker(pairs.map((p) => p.ref), opts.routeFor) : void 0;
99
- return pairs.map(({ item, ref }) => {
100
- const body = renderItem(item, {
101
- packageName: opts.packageName,
102
- ...crossLinker ? { crossLinker } : {}
103
- });
104
- const meta = {
105
- ...ref,
106
- summary: getSummary(item),
107
- packageName: opts.packageName
108
- };
109
- const frontmatter = opts.frontmatter ? opts.frontmatter(meta) : "";
110
- return {
111
- ...meta,
112
- markdown: frontmatter + body
113
- };
114
- });
115
- }
116
-
117
- //#endregion
118
- export { isEmittable, renderItem, renderPackage };
package/tsdoc.js DELETED
@@ -1,158 +0,0 @@
1
- import { ApiDocumentedItem, ApiReleaseTagMixin, ReleaseTag } from "@microsoft/api-extractor-model";
2
-
3
- //#region src/tsdoc.ts
4
- /**
5
- * Recursively flatten a TSDoc DocNode tree to plain text (code spans → backticks).
6
- *
7
- * @public
8
- */
9
- function extractPlainText(node) {
10
- const nodeAny = node;
11
- if (node.kind === "PlainText") return nodeAny.text || "";
12
- if (node.kind === "SoftBreak") return " ";
13
- if (node.kind === "CodeSpan") return `\`${nodeAny.code || ""}\``;
14
- if (node.kind === "LinkTag") {
15
- if (nodeAny.linkText) return extractPlainText(nodeAny.linkText);
16
- return (nodeAny.codeDestination?.memberReferences?.[0]?.memberIdentifier)?.identifier || "";
17
- }
18
- const parts = [];
19
- if (typeof nodeAny.getChildNodes === "function") for (const child of nodeAny.getChildNodes()) {
20
- const childText = extractPlainText(child);
21
- if (childText) parts.push(childText);
22
- }
23
- return parts.join("");
24
- }
25
- /**
26
- * The TSDoc `@summary` section as a single cleaned line.
27
- *
28
- * @public
29
- */
30
- function getSummary(item) {
31
- if (item instanceof ApiDocumentedItem) {
32
- const tsdoc = item.tsdocComment;
33
- if (tsdoc?.summarySection) return extractPlainText(tsdoc.summarySection).replace(/\s+/g, " ").trim();
34
- }
35
- return "";
36
- }
37
- /**
38
- * `@param` blocks merged with parameter types from the declaration excerpt.
39
- *
40
- * @public
41
- */
42
- function getParams(item) {
43
- const out = [];
44
- const paramTypes = /* @__PURE__ */ new Map();
45
- const parameters = item.parameters;
46
- if (Array.isArray(parameters)) for (const param of parameters) {
47
- const excerpt = param.parameterTypeExcerpt;
48
- const name = param.name || "";
49
- if (excerpt?.text) paramTypes.set(name, String(excerpt.text).trim());
50
- }
51
- if (item instanceof ApiDocumentedItem) {
52
- const tsdoc = item.tsdocComment;
53
- if (tsdoc?.params) {
54
- for (const block of tsdoc.params.blocks) {
55
- const blockAny = block;
56
- const name = blockAny.parameterName || "";
57
- const description = extractPlainText(blockAny.content).replace(/\s+/g, " ").trim();
58
- const type = paramTypes.get(name);
59
- out.push({
60
- name,
61
- ...type != null ? { type } : {},
62
- description
63
- });
64
- }
65
- return out;
66
- }
67
- }
68
- for (const [name, type] of paramTypes.entries()) out.push({
69
- name,
70
- type,
71
- description: ""
72
- });
73
- return out;
74
- }
75
- /**
76
- * The `@returns` block description, if present.
77
- *
78
- * @public
79
- */
80
- function getReturns(item) {
81
- if (item instanceof ApiDocumentedItem) {
82
- const tsdoc = item.tsdocComment;
83
- if (tsdoc?.returnsBlock) {
84
- const description = extractPlainText(tsdoc.returnsBlock.content).replace(/\s+/g, " ").trim();
85
- return description.length > 0 ? { description } : null;
86
- }
87
- }
88
- return null;
89
- }
90
- /**
91
- * All `@example` fenced-code blocks (falls back to plain text).
92
- *
93
- * @public
94
- */
95
- function getExamples(item) {
96
- const examples = [];
97
- if (!(item instanceof ApiDocumentedItem)) return examples;
98
- const tsdoc = item.tsdocComment;
99
- for (const block of tsdoc?.customBlocks || []) {
100
- if (block.blockTag?.tagNameWithUpperCase !== "@EXAMPLE") continue;
101
- const content = block.content;
102
- let found = false;
103
- for (const node of content?.nodes || []) if (node.kind === "FencedCode") {
104
- examples.push({
105
- language: node.language || "typescript",
106
- code: node.code || ""
107
- });
108
- found = true;
109
- }
110
- if (!found) {
111
- const text = extractPlainText(content).trim();
112
- if (text) examples.push({
113
- language: "typescript",
114
- code: text
115
- });
116
- }
117
- }
118
- return examples;
119
- }
120
- /**
121
- * Reads the deprecation-block message from an ApiItem, if one is present.
122
- *
123
- * @public
124
- */
125
- function getDeprecation(item) {
126
- if (item instanceof ApiDocumentedItem) {
127
- const tsdoc = item.tsdocComment;
128
- if (tsdoc?.deprecatedBlock) return { message: extractPlainText(tsdoc.deprecatedBlock.content).replace(/\s+/g, " ").trim() };
129
- }
130
- return null;
131
- }
132
- /**
133
- * The release tag (Public/Beta/Alpha/Internal) or "Public" when absent.
134
- *
135
- * @public
136
- */
137
- function getReleaseTag(item) {
138
- if (ApiReleaseTagMixin.isBaseClassOf(item)) switch (item.releaseTag) {
139
- case ReleaseTag.Public: return "Public";
140
- case ReleaseTag.Beta: return "Beta";
141
- case ReleaseTag.Alpha: return "Alpha";
142
- case ReleaseTag.Internal: return "Internal";
143
- default: return "Public";
144
- }
145
- return "Public";
146
- }
147
- /**
148
- * True when the item carries the given TSDoc modifier tag (without the `@`).
149
- *
150
- * @public
151
- */
152
- function hasModifierTag(item, tagName) {
153
- if (item instanceof ApiDocumentedItem) return ((item.tsdocComment?.modifierTagSet)?.nodes || []).some((t) => t.tagName === `@${tagName}`);
154
- return false;
155
- }
156
-
157
- //#endregion
158
- export { extractPlainText, getDeprecation, getExamples, getParams, getReleaseTag, getReturns, getSummary, hasModifierTag };