rspress-plugin-api-extractor 0.8.9 → 0.9.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 +11 -11
- package/build-program.js +3 -3
- package/build-stages.js +30 -31
- package/config-helpers.js +61 -21
- package/errors.js +1 -7
- package/frontmatter.js +149 -0
- package/index.d.ts +11 -2
- package/layers/ConfigServiceLive.js +53 -39
- package/layers/TypeRegistryServiceLive.js +10 -5
- package/markdown/helpers.js +40 -69
- package/markdown/index.js +2 -2
- package/markdown/page-generators/class-page.js +40 -42
- package/markdown/page-generators/enum-page.js +15 -15
- package/markdown/page-generators/function-page.js +18 -20
- package/markdown/page-generators/interface-page.js +41 -43
- package/markdown/page-generators/namespace-page.js +22 -24
- package/markdown/page-generators/type-alias-page.js +14 -16
- package/markdown/page-generators/variable-page.js +14 -16
- package/markdown/prose-linker.js +22 -0
- package/model-loader.js +59 -113
- package/package.json +11 -5
- package/plugin.js +1 -5
- package/shiki-transformer.js +3 -3
- package/sync-node-fs.js +80 -0
- package/twoslash-transformer.js +1 -1
- package/content-hash.js +0 -79
- package/formatter.js +0 -69
- package/layers/SnapshotServiceLive.js +0 -92
- package/loader.js +0 -200
- package/markdown/cross-linker.js +0 -157
- package/migrations/001_create_snapshots.js +0 -25
- package/multi-entry-resolver.js +0 -70
- package/route-collisions.js +0 -44
- package/services/SnapshotService.js +0 -7
- package/synthetic-bases.js +0 -74
package/content-hash.js
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
//#region src/content-hash.ts
|
|
4
|
-
/**
|
|
5
|
-
* Normalizes content string for consistent hashing.
|
|
6
|
-
*
|
|
7
|
-
* Applies the following transformations:
|
|
8
|
-
* - Converts all line endings to Unix-style (`\n`)
|
|
9
|
-
* - Trims leading and trailing whitespace
|
|
10
|
-
* - Collapses multiple consecutive blank lines to a single blank line
|
|
11
|
-
*
|
|
12
|
-
* @param content - The content string to normalize
|
|
13
|
-
* @returns Normalized content string
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```typescript
|
|
17
|
-
* const normalized = normalizeContent("line1\r\n\r\n\r\nline2 ");
|
|
18
|
-
* // Returns: "line1\n\nline2"
|
|
19
|
-
* ```
|
|
20
|
-
*/
|
|
21
|
-
function normalizeContent(content) {
|
|
22
|
-
return content.replaceAll("\r\n", "\n").replaceAll("\r", "\n").trim().replaceAll(/\n{3,}/g, "\n\n");
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Generates a SHA-256 hash of normalized markdown content.
|
|
26
|
-
*
|
|
27
|
-
* The content is normalized before hashing to ensure consistent results
|
|
28
|
-
* regardless of line ending differences or trailing whitespace.
|
|
29
|
-
*
|
|
30
|
-
* @param content - The markdown content to hash (excluding frontmatter)
|
|
31
|
-
* @returns Hexadecimal SHA-256 hash string
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```typescript
|
|
35
|
-
* const hash = hashContent("# My Title\n\nContent here");
|
|
36
|
-
* ```
|
|
37
|
-
*/
|
|
38
|
-
function hashContent(content) {
|
|
39
|
-
const normalized = normalizeContent(content);
|
|
40
|
-
return createHash("sha256").update(normalized).digest("hex");
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Generates a SHA-256 hash of frontmatter fields.
|
|
44
|
-
*
|
|
45
|
-
* Excludes timestamp-related fields (`publishedTime`, `modifiedTime`, `head`,
|
|
46
|
-
* `article:published_time`, `article:modified_time`) to prevent circular
|
|
47
|
-
* dependencies in change detection.
|
|
48
|
-
*
|
|
49
|
-
* @param frontmatter - The frontmatter object to hash
|
|
50
|
-
* @returns Hexadecimal SHA-256 hash string
|
|
51
|
-
*
|
|
52
|
-
* @remarks
|
|
53
|
-
* Keys are sorted alphabetically before hashing to ensure consistent
|
|
54
|
-
* results regardless of object key order.
|
|
55
|
-
*
|
|
56
|
-
* @example
|
|
57
|
-
* ```typescript
|
|
58
|
-
* const hash = hashFrontmatter({
|
|
59
|
-
* title: "My Page",
|
|
60
|
-
* description: "Page description"
|
|
61
|
-
* });
|
|
62
|
-
* ```
|
|
63
|
-
*/
|
|
64
|
-
function hashFrontmatter(frontmatter) {
|
|
65
|
-
const filtered = {};
|
|
66
|
-
for (const [key, value] of Object.entries(frontmatter)) {
|
|
67
|
-
if (key === "publishedTime" || key === "modifiedTime" || key === "head" || key === "article:published_time" || key === "article:modified_time") continue;
|
|
68
|
-
filtered[key] = value;
|
|
69
|
-
}
|
|
70
|
-
const sorted = Object.keys(filtered).sort().reduce((acc, key) => {
|
|
71
|
-
acc[key] = filtered[key];
|
|
72
|
-
return acc;
|
|
73
|
-
}, {});
|
|
74
|
-
const json = JSON.stringify(sorted);
|
|
75
|
-
return createHash("sha256").update(json).digest("hex");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
//#endregion
|
|
79
|
-
export { hashContent, hashFrontmatter, normalizeContent };
|
package/formatter.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { TypeSignatureFormatter } from "@tsdoctor/model";
|
|
2
|
-
|
|
3
|
-
//#region src/formatter.ts
|
|
4
|
-
/**
|
|
5
|
-
* Formats TypeScript type signatures for display in documentation.
|
|
6
|
-
*
|
|
7
|
-
* Transforms raw API Extractor excerpt text into clean, readable signatures.
|
|
8
|
-
* The core {@link TypeSignatureFormatter.format | format} algorithm is
|
|
9
|
-
* inherited from the `api-extractor-llms` `TypeSignatureFormatter`; this
|
|
10
|
-
* subclass adds the positional constructor and the test-only
|
|
11
|
-
* {@link TypeSignatureFormatter.addLinks | addLinks} cross-link injection.
|
|
12
|
-
*
|
|
13
|
-
* **Relationships:**
|
|
14
|
-
* - Used by all page generators ({@link ClassPageGenerator}, etc.)
|
|
15
|
-
* - Works with API Extractor's `Excerpt` model
|
|
16
|
-
* - Can integrate with {@link MarkdownCrossLinker} for type linking
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* ```ts
|
|
20
|
-
* const formatter = new TypeSignatureFormatter();
|
|
21
|
-
*
|
|
22
|
-
* // Format a simple signature
|
|
23
|
-
* const signature = formatter.format(apiFunction.excerpt);
|
|
24
|
-
* // "function myFunc(arg: string): Promise<void>"
|
|
25
|
-
*
|
|
26
|
-
* // With cross-linking
|
|
27
|
-
* const linked = formatter.addLinks(signature, excerpt);
|
|
28
|
-
* // "function myFunc(arg: string): Promise<[MyType](/api/types/mytype)>"
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
var TypeSignatureFormatter$1 = class extends TypeSignatureFormatter {
|
|
32
|
-
apiItemRoutes;
|
|
33
|
-
constructor(maxLineLength = 80, indent = " ", apiItemRoutes) {
|
|
34
|
-
super({
|
|
35
|
-
maxLineLength,
|
|
36
|
-
indent
|
|
37
|
-
});
|
|
38
|
-
this.apiItemRoutes = apiItemRoutes;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Inject markdown cross-links into already-formatted signature text.
|
|
42
|
-
*
|
|
43
|
-
* A lower-level escape hatch kept for callers that want link injection at
|
|
44
|
-
* the formatter level; the build pipeline normally cross-links prose via
|
|
45
|
-
* MarkdownCrossLinker instead. The shared library's formatter has no
|
|
46
|
-
* equivalent, so this stays plugin-local. Covered by formatter.test.ts.
|
|
47
|
-
*/
|
|
48
|
-
addLinks(text, excerpt) {
|
|
49
|
-
if (!excerpt.spannedTokens || !this.apiItemRoutes) return text;
|
|
50
|
-
const typeReferences = /* @__PURE__ */ new Map();
|
|
51
|
-
for (const token of excerpt.spannedTokens) if (token.kind === "Reference" && token.canonicalReference) {
|
|
52
|
-
const canonicalRef = token.canonicalReference.toString();
|
|
53
|
-
const route = this.apiItemRoutes.get(canonicalRef);
|
|
54
|
-
if (route && token.text) typeReferences.set(token.text.trim(), route);
|
|
55
|
-
}
|
|
56
|
-
let result = text;
|
|
57
|
-
for (const [typeName, route] of typeReferences.entries()) {
|
|
58
|
-
const regex = new RegExp(`\\b${this.escapeRegExp(typeName)}\\b`, "g");
|
|
59
|
-
result = result.replace(regex, `[${typeName}](${route})`);
|
|
60
|
-
}
|
|
61
|
-
return result;
|
|
62
|
-
}
|
|
63
|
-
escapeRegExp(string) {
|
|
64
|
-
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
//#endregion
|
|
69
|
-
export { TypeSignatureFormatter$1 as TypeSignatureFormatter };
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { hashContent } from "../content-hash.js";
|
|
2
|
-
import { SnapshotService } from "../services/SnapshotService.js";
|
|
3
|
-
import { SnapshotDbError } from "../errors.js";
|
|
4
|
-
import migration from "../migrations/001_create_snapshots.js";
|
|
5
|
-
import { Effect, Layer, Option } from "effect";
|
|
6
|
-
import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-node";
|
|
7
|
-
import { Migrator, SqlClient } from "effect/unstable/sql";
|
|
8
|
-
|
|
9
|
-
//#region src/layers/SnapshotServiceLive.ts
|
|
10
|
-
function toFileSnapshot(row) {
|
|
11
|
-
return {
|
|
12
|
-
outputDir: row.output_dir,
|
|
13
|
-
filePath: row.file_path,
|
|
14
|
-
publishedTime: row.published_time,
|
|
15
|
-
modifiedTime: row.modified_time,
|
|
16
|
-
contentHash: row.content_hash,
|
|
17
|
-
frontmatterHash: row.frontmatter_hash,
|
|
18
|
-
buildTime: row.build_time
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
function toSnapshotDbError(error) {
|
|
22
|
-
return new SnapshotDbError({
|
|
23
|
-
operation: "query",
|
|
24
|
-
dbPath: "snapshot-db",
|
|
25
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
const SnapshotServiceLive = (dbPath) => {
|
|
29
|
-
const SqlLive = SqliteClient.layer({ filename: dbPath });
|
|
30
|
-
const MigratorLive = SqliteMigrator.layer({ loader: Migrator.fromRecord({ "001_create_snapshots": migration }) }).pipe(Layer.provide(SqlLive));
|
|
31
|
-
const ServiceImpl = Layer.effect(SnapshotService, Effect.gen(function* () {
|
|
32
|
-
const sql = yield* SqlClient.SqlClient;
|
|
33
|
-
yield* Effect.addFinalizer(() => sql`PRAGMA wal_checkpoint(TRUNCATE)`.pipe(Effect.ignore));
|
|
34
|
-
return {
|
|
35
|
-
hashContent,
|
|
36
|
-
getSnapshot: (outputDir, filePath) => sql`SELECT * FROM file_snapshots WHERE output_dir = ${outputDir} AND file_path = ${filePath}`.pipe(Effect.map((rows) => rows.length > 0 ? Option.some(toFileSnapshot(rows[0])) : Option.none()), Effect.mapError(toSnapshotDbError)),
|
|
37
|
-
getAllForDirectory: (outputDir) => sql`SELECT * FROM file_snapshots WHERE output_dir = ${outputDir}`.pipe(Effect.map((rows) => rows.map(toFileSnapshot)), Effect.mapError(toSnapshotDbError)),
|
|
38
|
-
getFilePaths: (outputDir) => sql`SELECT file_path FROM file_snapshots WHERE output_dir = ${outputDir}`.pipe(Effect.map((rows) => rows.map((r) => r.file_path)), Effect.mapError(toSnapshotDbError)),
|
|
39
|
-
upsert: (snapshot) => sql`INSERT INTO file_snapshots
|
|
40
|
-
(output_dir, file_path, published_time, modified_time,
|
|
41
|
-
content_hash, frontmatter_hash, build_time)
|
|
42
|
-
VALUES (${snapshot.outputDir}, ${snapshot.filePath},
|
|
43
|
-
${snapshot.publishedTime}, ${snapshot.modifiedTime},
|
|
44
|
-
${snapshot.contentHash}, ${snapshot.frontmatterHash},
|
|
45
|
-
${snapshot.buildTime})
|
|
46
|
-
ON CONFLICT(output_dir, file_path) DO UPDATE SET
|
|
47
|
-
published_time = ${snapshot.publishedTime},
|
|
48
|
-
modified_time = ${snapshot.modifiedTime},
|
|
49
|
-
content_hash = ${snapshot.contentHash},
|
|
50
|
-
frontmatter_hash = ${snapshot.frontmatterHash},
|
|
51
|
-
build_time = ${snapshot.buildTime}
|
|
52
|
-
WHERE published_time != ${snapshot.publishedTime}
|
|
53
|
-
OR modified_time != ${snapshot.modifiedTime}
|
|
54
|
-
OR content_hash != ${snapshot.contentHash}
|
|
55
|
-
OR frontmatter_hash != ${snapshot.frontmatterHash}`.pipe(Effect.as(true), Effect.mapError(toSnapshotDbError)),
|
|
56
|
-
batchUpsert: (snapshots) => (snapshots.length === 0 ? Effect.succeed(0) : sql.withTransaction(Effect.forEach(snapshots, (s) => sql`INSERT INTO file_snapshots
|
|
57
|
-
(output_dir, file_path, published_time, modified_time,
|
|
58
|
-
content_hash, frontmatter_hash, build_time)
|
|
59
|
-
VALUES (${s.outputDir}, ${s.filePath},
|
|
60
|
-
${s.publishedTime}, ${s.modifiedTime},
|
|
61
|
-
${s.contentHash}, ${s.frontmatterHash},
|
|
62
|
-
${s.buildTime})
|
|
63
|
-
ON CONFLICT(output_dir, file_path) DO UPDATE SET
|
|
64
|
-
published_time = ${s.publishedTime},
|
|
65
|
-
modified_time = ${s.modifiedTime},
|
|
66
|
-
content_hash = ${s.contentHash},
|
|
67
|
-
frontmatter_hash = ${s.frontmatterHash},
|
|
68
|
-
build_time = ${s.buildTime}
|
|
69
|
-
WHERE published_time != ${s.publishedTime}
|
|
70
|
-
OR modified_time != ${s.modifiedTime}
|
|
71
|
-
OR content_hash != ${s.contentHash}
|
|
72
|
-
OR frontmatter_hash != ${s.frontmatterHash}`, { concurrency: 1 })).pipe(Effect.map(() => snapshots.length))).pipe(Effect.mapError(toSnapshotDbError)),
|
|
73
|
-
deleteSnapshot: (outputDir, filePath) => sql`DELETE FROM file_snapshots WHERE output_dir = ${outputDir} AND file_path = ${filePath}`.pipe(Effect.asVoid, Effect.mapError(toSnapshotDbError)),
|
|
74
|
-
cleanupStale: (outputDir, currentFiles) => Effect.gen(function* () {
|
|
75
|
-
const rows = yield* sql`SELECT file_path FROM file_snapshots WHERE output_dir = ${outputDir}`;
|
|
76
|
-
const staleFiles = [];
|
|
77
|
-
for (const row of rows) {
|
|
78
|
-
const fp = row.file_path;
|
|
79
|
-
if (!currentFiles.has(fp)) {
|
|
80
|
-
yield* sql`DELETE FROM file_snapshots WHERE output_dir = ${outputDir} AND file_path = ${fp}`;
|
|
81
|
-
staleFiles.push(fp);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return staleFiles;
|
|
85
|
-
}).pipe(Effect.mapError(toSnapshotDbError))
|
|
86
|
-
};
|
|
87
|
-
}));
|
|
88
|
-
return Layer.provide(ServiceImpl, Layer.merge(SqlLive, MigratorLive));
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
//#endregion
|
|
92
|
-
export { SnapshotServiceLive };
|
package/loader.js
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import { PluginEvent } from "./observability/events.js";
|
|
2
|
-
import { ApiDocumentedItem, ApiItemKind } from "@microsoft/api-extractor-model";
|
|
3
|
-
import { extractPlainText, getDeprecation, getExamples, getParams, getReleaseTag, getReturns, getSummary, hasModifierTag } from "@tsdoctor/model";
|
|
4
|
-
|
|
5
|
-
//#region src/loader.ts
|
|
6
|
-
/** Module-level emitter injected by plugin.ts at startup. */
|
|
7
|
-
let emitEvent = () => {};
|
|
8
|
-
let currentBuildId = "";
|
|
9
|
-
function setLoaderEventEmitter(fn, buildId = "") {
|
|
10
|
-
emitEvent = fn;
|
|
11
|
-
currentBuildId = buildId;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Parser for extracting and analyzing information from API Extractor models and TSDoc comments
|
|
15
|
-
*/
|
|
16
|
-
var ApiParser = class ApiParser {
|
|
17
|
-
/**
|
|
18
|
-
* Private constructor to prevent instantiation
|
|
19
|
-
*/
|
|
20
|
-
constructor() {}
|
|
21
|
-
/**
|
|
22
|
-
* Check if an API item has a custom modifier tag
|
|
23
|
-
*/
|
|
24
|
-
static hasModifierTag(item, tagName) {
|
|
25
|
-
return hasModifierTag(item, tagName);
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Extract all API items from a package (or resolved entry items) and categorize them based on configuration.
|
|
29
|
-
*
|
|
30
|
-
* When passed a `ResolvedEntryItem[]`, uses the items directly (multi-entry support).
|
|
31
|
-
* When passed an `ApiPackage`, reads from `entryPoints[0]` (legacy single-entry behavior).
|
|
32
|
-
*/
|
|
33
|
-
static categorizeApiItems(source, categories) {
|
|
34
|
-
const items = {};
|
|
35
|
-
for (const categoryKey of Object.keys(categories)) items[categoryKey] = [];
|
|
36
|
-
let members;
|
|
37
|
-
if (Array.isArray(source)) members = source.map((r) => r.item);
|
|
38
|
-
else {
|
|
39
|
-
const entryPoint = source.entryPoints[0];
|
|
40
|
-
if (!entryPoint) return items;
|
|
41
|
-
members = entryPoint.members;
|
|
42
|
-
}
|
|
43
|
-
const sortedCategories = Object.entries(categories).sort((a, b) => {
|
|
44
|
-
const [, configA] = a;
|
|
45
|
-
const [, configB] = b;
|
|
46
|
-
if (configA.tsdocModifier && !configB.tsdocModifier) return -1;
|
|
47
|
-
if (!configA.tsdocModifier && configB.tsdocModifier) return 1;
|
|
48
|
-
return 0;
|
|
49
|
-
});
|
|
50
|
-
for (const member of members) {
|
|
51
|
-
let categorized = false;
|
|
52
|
-
for (const [categoryKey, config] of sortedCategories) {
|
|
53
|
-
if (config.tsdocModifier && ApiParser.hasModifierTag(member, config.tsdocModifier)) {
|
|
54
|
-
items[categoryKey].push(member);
|
|
55
|
-
categorized = true;
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
if (config.itemKinds?.includes(member.kind)) {
|
|
59
|
-
items[categoryKey].push(member);
|
|
60
|
-
categorized = true;
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (!categorized && typeof process !== "undefined" && !process.env.VITEST) emitEvent(PluginEvent.ItemSkipped({
|
|
65
|
-
ctx: { buildId: currentBuildId },
|
|
66
|
-
item: member.displayName,
|
|
67
|
-
kind: String(member.kind),
|
|
68
|
-
reason: "uncategorized",
|
|
69
|
-
level: "warn"
|
|
70
|
-
}));
|
|
71
|
-
}
|
|
72
|
-
return items;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Extract all members from namespaces in a package (or resolved entry items).
|
|
76
|
-
* Returns a flat list of namespace members with their qualified names.
|
|
77
|
-
*
|
|
78
|
-
* When passed a `ResolvedEntryItem[]`, scans those items for namespaces (multi-entry support).
|
|
79
|
-
* When passed an `ApiPackage`, reads from `entryPoints[0]` (legacy single-entry behavior).
|
|
80
|
-
*
|
|
81
|
-
* @param source - The API package or resolved entry items to extract from
|
|
82
|
-
* @returns Array of namespace members with qualified names
|
|
83
|
-
*/
|
|
84
|
-
static extractNamespaceMembers(source) {
|
|
85
|
-
const members = [];
|
|
86
|
-
let topLevelItems;
|
|
87
|
-
if (Array.isArray(source)) topLevelItems = source.map((r) => r.item);
|
|
88
|
-
else {
|
|
89
|
-
const entryPoint = source.entryPoints[0];
|
|
90
|
-
if (!entryPoint) return members;
|
|
91
|
-
topLevelItems = entryPoint.members;
|
|
92
|
-
}
|
|
93
|
-
for (const item of topLevelItems) if (item.kind === ApiItemKind.Namespace) {
|
|
94
|
-
const namespace = item;
|
|
95
|
-
for (const member of namespace.members) members.push({
|
|
96
|
-
item: member,
|
|
97
|
-
namespace,
|
|
98
|
-
qualifiedName: `${namespace.displayName}.${member.displayName}`
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
return members;
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Extract plain text from a TSDoc DocNode tree (prose form).
|
|
105
|
-
*
|
|
106
|
-
* Delegates to api-extractor-llms `extractPlainText`. Used internally for
|
|
107
|
-
* `@see` reference text, where `{@link}` targets are flattened to display text.
|
|
108
|
-
*/
|
|
109
|
-
static extractPlainText(node) {
|
|
110
|
-
return extractPlainText(node);
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Get the summary text from an API item's TSDoc comment
|
|
114
|
-
*/
|
|
115
|
-
static getSummary(item) {
|
|
116
|
-
return getSummary(item);
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Get the release tag (public, beta, alpha, internal) from an API item
|
|
120
|
-
*/
|
|
121
|
-
static getReleaseTag(item) {
|
|
122
|
-
return getReleaseTag(item);
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Get parameter documentation from an API item's TSDoc comment
|
|
126
|
-
*/
|
|
127
|
-
static getParams(item) {
|
|
128
|
-
return getParams(item);
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Get return value documentation from an API item's TSDoc comment
|
|
132
|
-
*/
|
|
133
|
-
static getReturns(item) {
|
|
134
|
-
return getReturns(item);
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Get code examples from an API item's TSDoc comment
|
|
138
|
-
*/
|
|
139
|
-
static getExamples(item) {
|
|
140
|
-
return getExamples(item);
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Get deprecation message from an API item's TSDoc comment
|
|
144
|
-
*/
|
|
145
|
-
static getDeprecation(item) {
|
|
146
|
-
return getDeprecation(item);
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Get inheritance information from a class or interface
|
|
150
|
-
*/
|
|
151
|
-
static getInheritance(item) {
|
|
152
|
-
const result = {};
|
|
153
|
-
if (item.kind === ApiItemKind.Class) {
|
|
154
|
-
const apiClass = item;
|
|
155
|
-
if (apiClass.extendsType) result.extends = [apiClass.extendsType.excerpt.text];
|
|
156
|
-
const implementsTypes = apiClass.implementsTypes || [];
|
|
157
|
-
if (implementsTypes.length > 0) result.implements = implementsTypes.map((type) => type.excerpt.text);
|
|
158
|
-
} else if (item.kind === ApiItemKind.Interface) {
|
|
159
|
-
const extendsTypes = item.extendsTypes || [];
|
|
160
|
-
if (extendsTypes.length > 0) result.extends = extendsTypes.map((type) => type.excerpt.text);
|
|
161
|
-
}
|
|
162
|
-
return result;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Get see also references from an API item's TSDoc comment
|
|
166
|
-
*/
|
|
167
|
-
static getSeeReferences(item) {
|
|
168
|
-
if (item instanceof ApiDocumentedItem) {
|
|
169
|
-
const tsdoc = item.tsdocComment;
|
|
170
|
-
const references = [];
|
|
171
|
-
for (const seeBlock of tsdoc?.seeBlocks || []) {
|
|
172
|
-
const content = seeBlock.content;
|
|
173
|
-
const text = ApiParser.extractPlainText(content);
|
|
174
|
-
if (text.trim()) references.push({ text: text.replace(/\s+/g, " ").trim() });
|
|
175
|
-
}
|
|
176
|
-
return references;
|
|
177
|
-
}
|
|
178
|
-
return [];
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Get source code link for an API item
|
|
182
|
-
* @param item - The API item
|
|
183
|
-
* @param sourceConfig - Source configuration with repository URL and ref
|
|
184
|
-
* @returns Source code URL with line number, or null if not available
|
|
185
|
-
*/
|
|
186
|
-
static getSourceLink(item, sourceConfig) {
|
|
187
|
-
if (!sourceConfig) return null;
|
|
188
|
-
const itemAny = item;
|
|
189
|
-
const filePath = itemAny.fileUrlPath || itemAny.filePath;
|
|
190
|
-
if (!filePath) return null;
|
|
191
|
-
const lineNumber = itemAny.fileLineNumber || itemAny.line;
|
|
192
|
-
const ref = sourceConfig.ref || "blob/main";
|
|
193
|
-
const baseUrl = `${sourceConfig.url}/${ref}`;
|
|
194
|
-
if (lineNumber) return `${baseUrl}/${filePath}#L${lineNumber}`;
|
|
195
|
-
return `${baseUrl}/${filePath}`;
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
//#endregion
|
|
200
|
-
export { ApiParser, setLoaderEventEmitter };
|
package/markdown/cross-linker.js
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { CrossLinker } from "@tsdoctor/model";
|
|
2
|
-
|
|
3
|
-
//#region src/markdown/cross-linker.ts
|
|
4
|
-
/**
|
|
5
|
-
* A cross-linking utility for markdown API documentation.
|
|
6
|
-
*
|
|
7
|
-
* This class maintains a mapping of API item names to their documentation routes,
|
|
8
|
-
* enabling automatic cross-linking of type references in markdown content. It supports
|
|
9
|
-
* both top-level exports and class/interface members.
|
|
10
|
-
*
|
|
11
|
-
* **How it works:**
|
|
12
|
-
* 1. During initialization, it builds a route map from all API items in a package
|
|
13
|
-
* 2. For classes and interfaces, it also maps their members (e.g., `ClassName.methodName`)
|
|
14
|
-
* 3. When processing text, it replaces type names with markdown or HTML links
|
|
15
|
-
*
|
|
16
|
-
* **Relationships:**
|
|
17
|
-
* - Initialized by {@link ApiExtractorPlugin} with categorized API items
|
|
18
|
-
* - Used by page generators to add cross-links in documentation text
|
|
19
|
-
* - Provides route/kind data to {@link ShikiCrossLinker} for code block linking
|
|
20
|
-
*
|
|
21
|
-
* **Link Formats:**
|
|
22
|
-
* - Markdown: `[TypeName](/path/to/type)` - for use in .mdx content
|
|
23
|
-
* - HTML: anchor tags with href - for use in JSX/components
|
|
24
|
-
*
|
|
25
|
-
* @example Initialization
|
|
26
|
-
* ```ts
|
|
27
|
-
* const crossLinker = new MarkdownCrossLinker();
|
|
28
|
-
* const { routes, kinds } = crossLinker.initialize(
|
|
29
|
-
* categorizedItems,
|
|
30
|
-
* "/api/my-package",
|
|
31
|
-
* categories
|
|
32
|
-
* );
|
|
33
|
-
* ```
|
|
34
|
-
*
|
|
35
|
-
* @example Adding cross-links
|
|
36
|
-
* ```ts
|
|
37
|
-
* // Markdown format
|
|
38
|
-
* const text = crossLinker.addCrossLinks("Returns a MyClass instance");
|
|
39
|
-
* // Result: "Returns a [MyClass](/api/my-package/class/myclass) instance"
|
|
40
|
-
*
|
|
41
|
-
* // HTML format (for JSX)
|
|
42
|
-
* const html = crossLinker.addCrossLinksHtml("Returns a MyClass instance");
|
|
43
|
-
* // Result: "Returns a anchor-linked MyClass instance"
|
|
44
|
-
* ```
|
|
45
|
-
*
|
|
46
|
-
* @see {@link ShikiCrossLinker} for code block cross-linking
|
|
47
|
-
*/
|
|
48
|
-
var MarkdownCrossLinker = class {
|
|
49
|
-
/**
|
|
50
|
-
* Map of API item names to their route paths for cross-linking
|
|
51
|
-
*/
|
|
52
|
-
apiItemRoutes = /* @__PURE__ */ new Map();
|
|
53
|
-
/**
|
|
54
|
-
* Clear all accumulated routes. Call at the start of each build.
|
|
55
|
-
*/
|
|
56
|
-
clear() {
|
|
57
|
-
this.apiItemRoutes.clear();
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Add routes for API items. Accumulates across multiple calls.
|
|
61
|
-
* Call clear() first if starting a fresh build.
|
|
62
|
-
* @returns Object with routes map and kinds map for semantic highlighting
|
|
63
|
-
*/
|
|
64
|
-
addRoutes(items, baseRoute, categories) {
|
|
65
|
-
const apiItemKinds = /* @__PURE__ */ new Map();
|
|
66
|
-
for (const [categoryKey, categoryConfig] of Object.entries(categories)) {
|
|
67
|
-
const categoryItems = items[categoryKey] || [];
|
|
68
|
-
for (const item of categoryItems) {
|
|
69
|
-
const itemRoute = `${baseRoute}/${categoryConfig.folderName}/${item.displayName.toLowerCase()}`;
|
|
70
|
-
this.apiItemRoutes.set(item.displayName, itemRoute);
|
|
71
|
-
apiItemKinds.set(item.displayName, item.kind);
|
|
72
|
-
if ((item.kind === "Class" || item.kind === "Interface") && item.members) for (const member of item.members) {
|
|
73
|
-
const memberName = member.displayName;
|
|
74
|
-
const memberId = this.sanitizeId(memberName);
|
|
75
|
-
const fullMemberName = `${item.displayName}.${memberName}`;
|
|
76
|
-
const memberRoute = `${itemRoute}#${memberId}`;
|
|
77
|
-
this.apiItemRoutes.set(fullMemberName, memberRoute);
|
|
78
|
-
apiItemKinds.set(fullMemberName, member.kind);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return {
|
|
83
|
-
routes: this.apiItemRoutes,
|
|
84
|
-
kinds: apiItemKinds
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Initialize the cross-link map with all API items.
|
|
89
|
-
* @deprecated Use clear() + addRoutes() instead.
|
|
90
|
-
* @returns Object with routes map and kinds map for semantic highlighting
|
|
91
|
-
*/
|
|
92
|
-
initialize(items, baseRoute, categories) {
|
|
93
|
-
this.clear();
|
|
94
|
-
return this.addRoutes(items, baseRoute, categories);
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Set routes directly from pre-built route maps (e.g., from prepareWorkItems).
|
|
98
|
-
* Replaces all existing routes.
|
|
99
|
-
*/
|
|
100
|
-
setRoutes(routes) {
|
|
101
|
-
this.apiItemRoutes.clear();
|
|
102
|
-
for (const [name, route] of routes) this.apiItemRoutes.set(name, route);
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Add cross-links to type references in code (markdown format).
|
|
106
|
-
*
|
|
107
|
-
* Skips matches inside backtick code spans and existing markdown links.
|
|
108
|
-
*/
|
|
109
|
-
addCrossLinks(text) {
|
|
110
|
-
if (this.apiItemRoutes.size === 0) return text;
|
|
111
|
-
const refs = Array.from(this.apiItemRoutes.keys()).map((name) => ({
|
|
112
|
-
name,
|
|
113
|
-
kind: "type",
|
|
114
|
-
slug: name.toLowerCase()
|
|
115
|
-
}));
|
|
116
|
-
return new CrossLinker(refs, (ref) => this.apiItemRoutes.get(ref.name) ?? "").addLinks(text);
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Add cross-links to type references in code (HTML format)
|
|
120
|
-
* Use this when the text will be rendered as HTML (e.g., in React components)
|
|
121
|
-
*
|
|
122
|
-
* Note: intentionally hand-rolled and test-only — the upstream library's
|
|
123
|
-
* CrossLinker emits markdown links only, so the HTML path has no library
|
|
124
|
-
* equivalent.
|
|
125
|
-
*/
|
|
126
|
-
addCrossLinksHtml(text) {
|
|
127
|
-
let result = text;
|
|
128
|
-
const sortedNames = Array.from(this.apiItemRoutes.keys()).sort((a, b) => b.length - a.length);
|
|
129
|
-
for (const name of sortedNames) {
|
|
130
|
-
const route = this.apiItemRoutes.get(name);
|
|
131
|
-
if (!route) continue;
|
|
132
|
-
const regex = new RegExp(`\\b${name}\\b(?![a-zA-Z])`, "g");
|
|
133
|
-
result = result.replace(regex, (match, offset) => {
|
|
134
|
-
const beforeMatch = result.substring(0, offset);
|
|
135
|
-
if (beforeMatch.includes("<a") && !beforeMatch.includes("</a>")) return match;
|
|
136
|
-
return `<a href="${route}">${match}</a>`;
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
return result;
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Sanitize a display name to create a valid HTML ID
|
|
143
|
-
* Converts to lowercase, replaces spaces/special chars with hyphens
|
|
144
|
-
*/
|
|
145
|
-
sanitizeId(displayName, prefix = "") {
|
|
146
|
-
const sanitized = displayName.toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/^-+|-+$/g, "");
|
|
147
|
-
return prefix ? `${prefix}-${sanitized}` : sanitized;
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
/**
|
|
151
|
-
* Module-level instance used by internal generator functions.
|
|
152
|
-
* External callers should create their own instance or use this one.
|
|
153
|
-
*/
|
|
154
|
-
const markdownCrossLinker = new MarkdownCrossLinker();
|
|
155
|
-
|
|
156
|
-
//#endregion
|
|
157
|
-
export { MarkdownCrossLinker, markdownCrossLinker };
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
|
-
import { SqlClient } from "effect/unstable/sql";
|
|
3
|
-
|
|
4
|
-
//#region src/migrations/001_create_snapshots.ts
|
|
5
|
-
const migration = Effect.gen(function* () {
|
|
6
|
-
const sql = yield* SqlClient.SqlClient;
|
|
7
|
-
yield* sql`
|
|
8
|
-
CREATE TABLE IF NOT EXISTS file_snapshots (
|
|
9
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10
|
-
output_dir TEXT NOT NULL,
|
|
11
|
-
file_path TEXT NOT NULL,
|
|
12
|
-
published_time TEXT NOT NULL,
|
|
13
|
-
modified_time TEXT NOT NULL,
|
|
14
|
-
content_hash TEXT NOT NULL,
|
|
15
|
-
frontmatter_hash TEXT NOT NULL,
|
|
16
|
-
build_time TEXT NOT NULL,
|
|
17
|
-
UNIQUE(output_dir, file_path)
|
|
18
|
-
)
|
|
19
|
-
`;
|
|
20
|
-
yield* sql`CREATE INDEX IF NOT EXISTS idx_output_dir ON file_snapshots(output_dir)`;
|
|
21
|
-
yield* sql`CREATE INDEX IF NOT EXISTS idx_file_path ON file_snapshots(file_path)`;
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
//#endregion
|
|
25
|
-
export { migration as default };
|
package/multi-entry-resolver.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
//#region src/multi-entry-resolver.ts
|
|
2
|
-
/**
|
|
3
|
-
* Derive an entry point name from its display name in the API model.
|
|
4
|
-
*
|
|
5
|
-
* - Empty string (main entry "." in package.json) maps to "default"
|
|
6
|
-
* - Named entries (e.g., "testing") keep their name
|
|
7
|
-
*/
|
|
8
|
-
function getEntryPointName(displayName) {
|
|
9
|
-
return displayName === "" ? "default" : displayName;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Create a stable identity key for an API item based on its display name and kind.
|
|
13
|
-
* Used to detect re-exports across entry points.
|
|
14
|
-
*/
|
|
15
|
-
function itemKey(item) {
|
|
16
|
-
return `${item.displayName}::${item.kind}`;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Resolve all entry points from an API package into a flat list of
|
|
20
|
-
* deduplicated items.
|
|
21
|
-
*
|
|
22
|
-
* - Re-exported items (same displayName + kind across entries) are
|
|
23
|
-
* deduplicated to a single entry with availableFrom listing all
|
|
24
|
-
* entry points. The defining entry point prefers "default".
|
|
25
|
-
* - Items with different kinds but the same displayName (e.g. the
|
|
26
|
-
* Effect const + type companion pattern) remain as separate entries.
|
|
27
|
-
*
|
|
28
|
-
* @param apiPackage - The merged API package with 1+ entry points
|
|
29
|
-
* @returns Flat array of resolved items
|
|
30
|
-
*/
|
|
31
|
-
function resolveEntryPoints(apiPackage) {
|
|
32
|
-
const itemsByKey = /* @__PURE__ */ new Map();
|
|
33
|
-
for (const entryPoint of apiPackage.entryPoints) {
|
|
34
|
-
const epName = getEntryPointName(entryPoint.displayName);
|
|
35
|
-
for (const member of entryPoint.members) {
|
|
36
|
-
const key = itemKey(member);
|
|
37
|
-
const existing = itemsByKey.get(key) || [];
|
|
38
|
-
existing.push({
|
|
39
|
-
item: member,
|
|
40
|
-
entryPointName: epName
|
|
41
|
-
});
|
|
42
|
-
itemsByKey.set(key, existing);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
const intermediate = [];
|
|
46
|
-
for (const [, entries] of itemsByKey) if (entries.length === 1) {
|
|
47
|
-
const { item, entryPointName } = entries[0];
|
|
48
|
-
intermediate.push({
|
|
49
|
-
item,
|
|
50
|
-
definingEntryPoint: entryPointName,
|
|
51
|
-
availableFrom: [entryPointName]
|
|
52
|
-
});
|
|
53
|
-
} else {
|
|
54
|
-
const definingEntry = entries.find((e) => e.entryPointName === "default") || entries[0];
|
|
55
|
-
const allEntryPoints = [...new Set(entries.map((e) => e.entryPointName))];
|
|
56
|
-
intermediate.push({
|
|
57
|
-
item: definingEntry.item,
|
|
58
|
-
definingEntryPoint: definingEntry.entryPointName,
|
|
59
|
-
availableFrom: allEntryPoints
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
return intermediate.map((r) => ({
|
|
63
|
-
item: r.item,
|
|
64
|
-
definingEntryPoint: r.definingEntryPoint,
|
|
65
|
-
availableFrom: r.availableFrom
|
|
66
|
-
}));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
//#endregion
|
|
70
|
-
export { resolveEntryPoints };
|