rspress-plugin-api-extractor 0.8.8 → 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.
Files changed (53) hide show
  1. package/README.md +11 -11
  2. package/api-extracted-package.js +1 -1
  3. package/build-program.js +3 -3
  4. package/build-stages.js +30 -31
  5. package/config-helpers.js +61 -21
  6. package/errors.js +1 -7
  7. package/frontmatter.js +149 -0
  8. package/index.d.ts +11 -2
  9. package/layers/ConfigServiceLive.js +53 -39
  10. package/layers/TypeRegistryServiceLive.js +11 -6
  11. package/markdown/helpers.js +40 -69
  12. package/markdown/index.js +2 -2
  13. package/markdown/page-generators/class-page.js +40 -42
  14. package/markdown/page-generators/enum-page.js +15 -15
  15. package/markdown/page-generators/function-page.js +18 -20
  16. package/markdown/page-generators/interface-page.js +41 -43
  17. package/markdown/page-generators/namespace-page.js +22 -24
  18. package/markdown/page-generators/type-alias-page.js +14 -16
  19. package/markdown/page-generators/variable-page.js +14 -16
  20. package/markdown/prose-linker.js +22 -0
  21. package/model-loader.js +59 -113
  22. package/package.json +14 -8
  23. package/plugin.js +1 -5
  24. package/runtime/components/EnumMembersTable/index.css +18 -18
  25. package/runtime/components/EnumMembersTable/index.module.js +3 -3
  26. package/runtime/components/ExampleBlock/index.css +2 -2
  27. package/runtime/components/ExampleBlock/index.module.js +2 -2
  28. package/runtime/components/MemberSignature/index.css +6 -6
  29. package/runtime/components/MemberSignature/index.module.js +4 -4
  30. package/runtime/components/ParametersTable/index.css +19 -19
  31. package/runtime/components/ParametersTable/index.module.js +3 -3
  32. package/runtime/components/SignatureBlock/index.css +6 -6
  33. package/runtime/components/SignatureBlock/index.module.js +4 -4
  34. package/runtime/components/SignatureCode/index.css +9 -9
  35. package/runtime/components/SignatureCode/index.module.js +3 -3
  36. package/runtime/components/SignatureToolbar/index.css +18 -18
  37. package/runtime/components/SignatureToolbar/index.module.js +7 -7
  38. package/runtime/components/buttons/index.css +5 -5
  39. package/runtime/components/buttons/index.module.js +2 -2
  40. package/shiki-transformer.js +3 -3
  41. package/sync-node-fs.js +80 -0
  42. package/tsdoc-metadata.json +1 -1
  43. package/twoslash-transformer.js +1 -1
  44. package/content-hash.js +0 -79
  45. package/formatter.js +0 -69
  46. package/layers/SnapshotServiceLive.js +0 -92
  47. package/loader.js +0 -200
  48. package/markdown/cross-linker.js +0 -157
  49. package/migrations/001_create_snapshots.js +0 -25
  50. package/multi-entry-resolver.js +0 -70
  51. package/route-collisions.js +0 -44
  52. package/services/SnapshotService.js +0 -7
  53. 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 "api-extractor-llms";
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 "api-extractor-llms";
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 };
@@ -1,157 +0,0 @@
1
- import { CrossLinker } from "api-extractor-llms";
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 };
@@ -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 };