rspress-plugin-api-extractor 0.1.2 → 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.
Files changed (109) hide show
  1. package/README.md +2 -2
  2. package/api-extracted-package.js +426 -0
  3. package/build-program.js +120 -0
  4. package/build-stages.js +664 -0
  5. package/category-resolver.js +50 -0
  6. package/code-post-processor.js +38 -0
  7. package/config-helpers.js +106 -0
  8. package/config-utils.js +258 -0
  9. package/content-hash.js +79 -0
  10. package/errors.js +29 -0
  11. package/formatter.js +69 -0
  12. package/hide-cut-transformer.js +96 -0
  13. package/index.d.ts +901 -831
  14. package/index.js +4 -6128
  15. package/layers/ConfigServiceLive.js +377 -0
  16. package/layers/ObservabilityLive.js +136 -0
  17. package/layers/PathDerivationServiceLive.js +16 -0
  18. package/layers/SnapshotServiceLive.js +94 -0
  19. package/layers/TypeRegistryServiceLive.js +46 -0
  20. package/llms-processing.js +270 -0
  21. package/llms-program.js +262 -0
  22. package/loader.js +186 -0
  23. package/markdown/cross-linker.js +156 -0
  24. package/markdown/helpers.js +364 -0
  25. package/markdown/index.js +11 -0
  26. package/markdown/page-generators/class-page.js +357 -0
  27. package/markdown/page-generators/enum-page.js +152 -0
  28. package/markdown/page-generators/function-page.js +127 -0
  29. package/markdown/page-generators/index-pages.js +27 -0
  30. package/markdown/page-generators/interface-page.js +307 -0
  31. package/markdown/page-generators/namespace-page.js +280 -0
  32. package/markdown/page-generators/type-alias-page.js +110 -0
  33. package/markdown/page-generators/variable-page.js +110 -0
  34. package/markdown/shiki-utils.js +48 -0
  35. package/migrations/001_create_snapshots.js +25 -0
  36. package/model-loader.js +95 -0
  37. package/multi-entry-resolver.js +70 -0
  38. package/og-resolver.js +271 -0
  39. package/package.json +64 -73
  40. package/path-derivation.js +48 -0
  41. package/plugin.js +218 -0
  42. package/prettier-formatter.js +73 -0
  43. package/public/tsconfig/rspress.json +44 -0
  44. package/remark-api-codeblocks.js +130 -0
  45. package/remark-with-api.js +172 -0
  46. package/route-collisions.js +52 -0
  47. package/runtime/components/ApiExample/index.js +29 -25
  48. package/runtime/components/ApiLlmsPackageActions/index.js +252 -323
  49. package/runtime/components/ApiLlmsViewOptions/index.js +269 -340
  50. package/runtime/components/ApiMember/index.js +49 -47
  51. package/runtime/components/ApiSignature/index.js +32 -28
  52. package/runtime/components/EnumMembersTable/{index_module.css → index.css} +18 -19
  53. package/runtime/components/EnumMembersTable/index.js +36 -67
  54. package/runtime/components/EnumMembersTable/index.module.js +9 -6
  55. package/runtime/components/ExampleBlock/{index_module.css → index.css} +2 -3
  56. package/runtime/components/ExampleBlock/index.js +23 -28
  57. package/runtime/components/ExampleBlock/index.module.js +8 -5
  58. package/runtime/components/MarkdownContent/index.js +26 -18
  59. package/runtime/components/MarkdownText/index.js +28 -22
  60. package/runtime/components/MemberSignature/{index_module.css → index.css} +5 -6
  61. package/runtime/components/MemberSignature/index.js +46 -46
  62. package/runtime/components/MemberSignature/index.module.js +9 -6
  63. package/runtime/components/ParametersTable/{index_module.css → index.css} +19 -20
  64. package/runtime/components/ParametersTable/index.js +36 -67
  65. package/runtime/components/ParametersTable/index.module.js +9 -6
  66. package/runtime/components/SignatureBlock/{index_module.css → index.css} +5 -6
  67. package/runtime/components/SignatureBlock/index.js +30 -29
  68. package/runtime/components/SignatureBlock/index.module.js +9 -6
  69. package/runtime/components/SignatureCode/{index_module.css → index.css} +9 -10
  70. package/runtime/components/SignatureCode/index.js +40 -32
  71. package/runtime/components/SignatureCode/index.module.js +9 -6
  72. package/runtime/components/SignatureToolbar/{index_module.css → index.css} +18 -20
  73. package/runtime/components/SignatureToolbar/index.js +48 -51
  74. package/runtime/components/SignatureToolbar/index.module.js +13 -10
  75. package/runtime/components/buttons/ButtonGroup.js +13 -6
  76. package/runtime/components/buttons/CopyCodeButton.js +40 -38
  77. package/runtime/components/buttons/WrapSignatureButton.js +20 -16
  78. package/runtime/components/buttons/{index_module.css → index.css} +5 -6
  79. package/runtime/components/buttons/index.module.js +8 -5
  80. package/runtime/components/icons/CheckIcon/index.js +20 -17
  81. package/runtime/components/icons/CopyIcon/index.js +20 -17
  82. package/runtime/components/icons/UnwrapIcon/index.js +22 -18
  83. package/runtime/components/icons/WrapIcon/index.js +20 -17
  84. package/runtime/components/shared/_twoslash.css +3 -10
  85. package/runtime/components/shared/variables.css +0 -3
  86. package/runtime/hooks/useWrapToggle.js +32 -9
  87. package/runtime/index.d.ts +513 -173
  88. package/runtime/index.js +11 -9
  89. package/runtime/utils/decode-hast.js +31 -16
  90. package/runtime/utils/hast-renderer.js +21 -7
  91. package/schemas/config.js +199 -0
  92. package/schemas/index.js +5 -0
  93. package/schemas/opengraph.js +26 -0
  94. package/schemas/performance.js +19 -0
  95. package/serve.js +133 -0
  96. package/services/ConfigService.js +7 -0
  97. package/services/PathDerivationService.js +7 -0
  98. package/services/SnapshotService.js +7 -0
  99. package/services/TypeRegistryService.js +7 -0
  100. package/shiki-transformer.js +758 -0
  101. package/tsconfig-parser.js +127 -0
  102. package/tsdoc-metadata.json +11 -11
  103. package/twoslash-patterns.js +87 -0
  104. package/twoslash-transformer.js +316 -0
  105. package/type-reference-extractor.js +201 -0
  106. package/typescript-config.js +168 -0
  107. package/vfs-registry.js +121 -0
  108. package/0~llms-program.js +0 -344
  109. package/runtime/components/ApiLlmsPackageActions/index.module.js +0 -2
@@ -0,0 +1,73 @@
1
+ import { BuildMetrics } from "./layers/ObservabilityLive.js";
2
+ import { addLogicalBlankLines } from "./code-post-processor.js";
3
+ import { Effect, Metric } from "effect";
4
+ import { format } from "prettier";
5
+
6
+ //#region src/prettier-formatter.ts
7
+ /* v8 ignore start -- Prettier integration wrapper, tested via page generator integration tests */
8
+ /**
9
+ * Map code fence languages to Prettier parsers
10
+ */
11
+ const LANGUAGE_TO_PARSER = {
12
+ typescript: "typescript",
13
+ ts: "typescript",
14
+ tsx: "typescript",
15
+ javascript: "babel",
16
+ js: "babel",
17
+ jsx: "babel",
18
+ node: "babel"
19
+ };
20
+ /**
21
+ * Default Prettier options for consistent formatting
22
+ */
23
+ const PRETTIER_OPTIONS = {
24
+ printWidth: 80,
25
+ tabWidth: 2,
26
+ useTabs: false,
27
+ semi: true,
28
+ singleQuote: false,
29
+ trailingComma: "es5",
30
+ bracketSpacing: true,
31
+ arrowParens: "always"
32
+ };
33
+ /**
34
+ * Format code using Prettier
35
+ *
36
+ * @param code - The code to format
37
+ * @param language - The code fence language (e.g., "typescript", "ts", "js")
38
+ * @returns FormatResult with formatted code and metadata
39
+ */
40
+ async function formatCode(code, language) {
41
+ const start = performance.now();
42
+ const parser = LANGUAGE_TO_PARSER[language.toLowerCase()];
43
+ if (!parser) return {
44
+ code,
45
+ success: true,
46
+ formatTime: performance.now() - start
47
+ };
48
+ try {
49
+ const formatted = await format(code, {
50
+ ...PRETTIER_OPTIONS,
51
+ parser
52
+ });
53
+ const formatTime = performance.now() - start;
54
+ return {
55
+ code: addLogicalBlankLines(formatted.trim()),
56
+ success: true,
57
+ formatTime
58
+ };
59
+ } catch (error) {
60
+ const formatTime = performance.now() - start;
61
+ const errorMsg = error instanceof Error ? error.message : String(error);
62
+ Effect.runSync(Metric.increment(BuildMetrics.prettierErrors));
63
+ return {
64
+ code,
65
+ success: false,
66
+ error: errorMsg,
67
+ formatTime
68
+ };
69
+ }
70
+ }
71
+
72
+ //#endregion
73
+ export { formatCode };
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig.json",
3
+ "compilerOptions": {
4
+ "allowImportingTsExtensions": true,
5
+ "isolatedModules": true,
6
+ "jsx": "react-jsx",
7
+ "lib": ["dom", "es2023"],
8
+ "module": "esnext",
9
+ "moduleDetection": "force",
10
+ "moduleResolution": "bundler",
11
+ "noEmit": true,
12
+ "noUncheckedSideEffectImports": true,
13
+ "noUnusedLocals": true,
14
+ "noUnusedParameters": true,
15
+ "resolveJsonModule": true,
16
+ "skipLibCheck": true,
17
+ "strict": true,
18
+ "target": "es2023",
19
+ "types": ["node", "react", "react-dom"],
20
+ "useDefineForClassFields": true,
21
+ "verbatimModuleSyntax": true
22
+ },
23
+ "exclude": ["${configDir}/node_modules", "${configDir}/dist", "${configDir}/.rspress"],
24
+ "include": [
25
+ "${configDir}/types/*.ts",
26
+ "${configDir}/package.json",
27
+ "${configDir}/*.ts",
28
+ "${configDir}/*.cts",
29
+ "${configDir}/*.mts",
30
+ "${configDir}/lib/**/*.ts",
31
+ "${configDir}/lib/**/*.tsx",
32
+ "${configDir}/lib/**/*.cts",
33
+ "${configDir}/lib/**/*.mts",
34
+ "${configDir}/docs/**/*.ts",
35
+ "${configDir}/docs/**/*.tsx",
36
+ "${configDir}/docs/**/*.mdx",
37
+ "${configDir}/theme/**/*.ts",
38
+ "${configDir}/theme/**/*.tsx",
39
+ "${configDir}/theme/**/*.mdx"
40
+ ],
41
+ "mdx": {
42
+ "checkMdx": true
43
+ }
44
+ }
@@ -0,0 +1,130 @@
1
+ import { VfsRegistry } from "./vfs-registry.js";
2
+ import { generateShikiHast } from "./markdown/shiki-utils.js";
3
+ import { visit } from "unist-util-visit";
4
+
5
+ //#region src/remark-api-codeblocks.ts
6
+ /**
7
+ * Create an MDX JSX attribute value expression with proper estree AST.
8
+ * This ensures the value is properly serialized as a JavaScript string literal.
9
+ *
10
+ * @param value - The string value to wrap
11
+ * @returns An MdxJsxAttributeValueExpression node
12
+ */
13
+ function createExpressionValue(value) {
14
+ const escaped = JSON.stringify(value);
15
+ return {
16
+ type: "mdxJsxAttributeValueExpression",
17
+ value: escaped,
18
+ data: { estree: {
19
+ type: "Program",
20
+ sourceType: "module",
21
+ body: [{
22
+ type: "ExpressionStatement",
23
+ expression: {
24
+ type: "Literal",
25
+ value,
26
+ raw: escaped
27
+ }
28
+ }]
29
+ } }
30
+ };
31
+ }
32
+ /**
33
+ * Extract a string value from a JSX attribute on an MdxJsxFlowElement.
34
+ * Handles both plain string attributes and expression attributes with estree literals.
35
+ *
36
+ * @param node - The JSX element node
37
+ * @param attrName - The attribute name to extract
38
+ * @returns The string value, or undefined if not found
39
+ */
40
+ function getJsxAttrStringValue(node, attrName) {
41
+ for (const attr of node.attributes) {
42
+ if (attr.type !== "mdxJsxAttribute" || attr.name !== attrName) continue;
43
+ if (typeof attr.value === "string") return attr.value;
44
+ if (attr.value?.type === "mdxJsxAttributeValueExpression" && attr.value.data?.estree) {
45
+ const body = attr.value.data.estree.body;
46
+ if (body?.length > 0 && body[0].type === "ExpressionStatement") {
47
+ const expr = body[0].expression;
48
+ if (expr.type === "Literal" && typeof expr.value === "string") return expr.value;
49
+ }
50
+ }
51
+ }
52
+ }
53
+ /**
54
+ * Remove named attributes from a JSX element node.
55
+ *
56
+ * @param node - The JSX element node
57
+ * @param attrNames - Array of attribute names to remove
58
+ */
59
+ function removeJsxAttrs(node, attrNames) {
60
+ const removeSet = new Set(attrNames);
61
+ node.attributes = node.attributes.filter((attr) => {
62
+ if (attr.type !== "mdxJsxAttribute") return true;
63
+ return !removeSet.has(attr.name);
64
+ });
65
+ }
66
+ /**
67
+ * Remark plugin that processes API JSX components for browser rendering.
68
+ *
69
+ * This plugin:
70
+ * 1. Visits JSX component nodes (ApiSignature, ApiMember, ApiExample) in the MDX tree
71
+ * 2. Reads `source` and `apiScope` props emitted by page generators
72
+ * 3. Looks up VFS configuration and runs Shiki with appropriate transformers
73
+ * 4. Injects the resulting HAST as a base64-encoded `hast` prop
74
+ * 5. Removes build-time `source` and `apiScope` props
75
+ *
76
+ * In SSG-MD mode, the plugin simply removes build-time props and lets the
77
+ * components render their own clean HTML for markdown conversion.
78
+ */
79
+ const remarkApiCodeblocks = () => {
80
+ return async function remarkTransformer(tree, file) {
81
+ if (!VfsRegistry.hasConfigs()) return;
82
+ const promises = [];
83
+ const isSsgMd = import.meta.env?.SSG_MD || process.env.RSBUILD_ENVIRONMENT === "node_md" || process.env.BUILD_TARGET === "node_md";
84
+ const currentFilePath = file.path || "unknown";
85
+ const jsxComponentNames = new Set([
86
+ "ApiSignature",
87
+ "ApiMember",
88
+ "ApiExample"
89
+ ]);
90
+ visit(tree, "mdxJsxFlowElement", (node) => {
91
+ if (!node.name || !jsxComponentNames.has(node.name)) return;
92
+ const source = getJsxAttrStringValue(node, "source");
93
+ const apiScopeValue = getJsxAttrStringValue(node, "apiScope");
94
+ if (!source || !apiScopeValue) return;
95
+ const promise = (async () => {
96
+ if (isSsgMd) {
97
+ removeJsxAttrs(node, ["source", "apiScope"]);
98
+ return;
99
+ }
100
+ const vfsConfig = VfsRegistry.get(apiScopeValue);
101
+ if (!vfsConfig) {
102
+ console.warn(`[remark-api-codeblocks] No VFS config found for scope "${apiScopeValue}" in ${currentFilePath}`);
103
+ removeJsxAttrs(node, ["source", "apiScope"]);
104
+ return;
105
+ }
106
+ const transformers = [];
107
+ if (node.name === "ApiExample" && vfsConfig.twoslashTransformer) transformers.push(vfsConfig.twoslashTransformer);
108
+ else if (node.name === "ApiMember" && vfsConfig.hideCutTransformer) transformers.push(vfsConfig.hideCutTransformer);
109
+ else if (node.name === "ApiSignature") {
110
+ if (vfsConfig.hideCutLinesTransformer) transformers.push(vfsConfig.hideCutLinesTransformer);
111
+ }
112
+ const isExample = node.name === "ApiExample" && !!vfsConfig.twoslashTransformer;
113
+ let hast = await generateShikiHast(source, vfsConfig.highlighter, transformers.length > 0 ? transformers : void 0, isExample, vfsConfig.theme);
114
+ if (hast && vfsConfig.crossLinker) hast = vfsConfig.crossLinker.transformHast(hast, apiScopeValue);
115
+ const hastBase64 = hast ? Buffer.from(JSON.stringify(hast), "utf-8").toString("base64") : "";
116
+ node.attributes.push({
117
+ type: "mdxJsxAttribute",
118
+ name: "hast",
119
+ value: createExpressionValue(hastBase64)
120
+ });
121
+ removeJsxAttrs(node, ["source", "apiScope"]);
122
+ })();
123
+ promises.push(promise);
124
+ });
125
+ await Promise.all(promises);
126
+ };
127
+ };
128
+
129
+ //#endregion
130
+ export { remarkApiCodeblocks };
@@ -0,0 +1,172 @@
1
+ import { BuildMetrics } from "./layers/ObservabilityLive.js";
2
+ import { formatCode } from "./prettier-formatter.js";
3
+ import { stripTwoslashDirectives } from "./markdown/helpers.js";
4
+ import { DEFAULT_SHIKI_THEMES } from "./markdown/shiki-utils.js";
5
+ import { Effect, Metric } from "effect";
6
+ import { codeToHast, hastToHtml } from "shiki";
7
+ import { visit } from "unist-util-visit";
8
+
9
+ //#region src/remark-with-api.ts
10
+ /* v8 ignore start -- remark plugin, requires MDX compilation context */
11
+ /**
12
+ * Supported languages for with-api code blocks
13
+ * Based on GitHub Linguist standard aliases:
14
+ * - TypeScript: typescript, ts
15
+ * - JavaScript: javascript, js, node
16
+ * - TSX/JSX: tsx, jsx (Shiki-supported)
17
+ */
18
+ const SUPPORTED_LANGUAGES = new Set([
19
+ "typescript",
20
+ "ts",
21
+ "javascript",
22
+ "js",
23
+ "node",
24
+ "tsx",
25
+ "jsx"
26
+ ]);
27
+ /**
28
+ * Infer API scope from file path
29
+ * Path structure: docs/en/api-name/rest-of-path.mdx
30
+ */
31
+ function inferApiScope(filePath) {
32
+ const match = filePath.replace(/\\/g, "/").match(/(?:^|\/)(docs\/en|website\/docs\/en)\/([^/]+)(?:\/|$)/);
33
+ if (!match) return;
34
+ return match[2];
35
+ }
36
+ /**
37
+ * Remark plugin that transforms `with-api` code blocks into ApiExample components
38
+ *
39
+ * Usage in markdown:
40
+ * ```typescript with-api
41
+ * import { ClaudeBinaryPlugin } from "claude-binary-plugin";
42
+ * // Full twoslash support: @noErrors, @errors, ^?, etc.
43
+ * ```
44
+ *
45
+ * The plugin:
46
+ * 1. Detects code blocks with `with-api` in the meta string
47
+ * 2. Supports typescript, ts, tsx, javascript, js, jsx languages
48
+ * 3. Processes with Twoslash for type information
49
+ * 4. Applies API docs cross-linker for type reference links
50
+ * 5. Renders to ApiExample component with pre-rendered Shiki HAST
51
+ */
52
+ const remarkWithApi = (options) => {
53
+ const { shikiCrossLinker, getTransformer, theme } = options;
54
+ const resolvedTheme = theme ?? DEFAULT_SHIKI_THEMES;
55
+ return async function remarkTransformer(tree, file) {
56
+ const fileStart = performance.now();
57
+ const promises = [];
58
+ let blockCount = 0;
59
+ let needsApiExampleImport = false;
60
+ const isSsgMd = import.meta.env?.SSG_MD || process.env.RSBUILD_ENVIRONMENT === "node_md" || process.env.BUILD_TARGET === "node_md";
61
+ const currentFilePath = file.path;
62
+ if (currentFilePath) {
63
+ const apiScope = inferApiScope(currentFilePath);
64
+ if (apiScope) shikiCrossLinker.setApiScope(apiScope);
65
+ }
66
+ visit(tree, "code", (node, index, parent) => {
67
+ const hasWithApi = node.meta?.includes("with-api");
68
+ const lang = node.lang || "typescript";
69
+ const isSupported = SUPPORTED_LANGUAGES.has(lang);
70
+ if (!hasWithApi || !isSupported) return;
71
+ blockCount++;
72
+ const promise = (async () => {
73
+ const blockStart = performance.now();
74
+ const rawCode = node.value;
75
+ const code = (await formatCode(rawCode, lang)).code;
76
+ const twoslashTransformer = getTransformer();
77
+ const transformers = twoslashTransformer ? [twoslashTransformer] : [];
78
+ const shikiStart = performance.now();
79
+ let hast = await codeToHast(code, {
80
+ lang,
81
+ meta: { __raw: "twoslash" },
82
+ themes: {
83
+ light: resolvedTheme.light,
84
+ dark: resolvedTheme.dark
85
+ },
86
+ defaultColor: false,
87
+ cssVariablePrefix: "--api-shiki-",
88
+ transformers
89
+ });
90
+ const apiScope = currentFilePath ? inferApiScope(currentFilePath) : void 0;
91
+ if (apiScope) hast = shikiCrossLinker.transformHast(hast, apiScope);
92
+ const shikiTime = performance.now() - shikiStart;
93
+ const totalBlockTime = performance.now() - blockStart;
94
+ Effect.runSync(Metric.increment(BuildMetrics.codeblockTotal));
95
+ Effect.runSync(Metric.update(BuildMetrics.codeblockDuration, totalBlockTime));
96
+ if (shikiTime > 0) Effect.runSync(Metric.update(BuildMetrics.codeblockShikiDuration, shikiTime));
97
+ if (totalBlockTime > 100) Effect.runSync(Metric.increment(BuildMetrics.codeblockSlow));
98
+ if (parent && typeof index === "number") if (isSsgMd) {
99
+ const cleanCode = hastToHtml(hast).replace(/<[^>]*>/g, "").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, "\"").replace(/&#39;/g, "'").trim();
100
+ node.lang = "typescript";
101
+ node.meta = void 0;
102
+ node.value = cleanCode;
103
+ } else {
104
+ const displayCode = stripTwoslashDirectives(code);
105
+ const hastJson = JSON.stringify(hast);
106
+ const mdxNode = {
107
+ type: "mdxJsxFlowElement",
108
+ name: "ApiExample",
109
+ attributes: [{
110
+ type: "mdxJsxAttribute",
111
+ name: "code",
112
+ value: displayCode
113
+ }, {
114
+ type: "mdxJsxAttribute",
115
+ name: "hast",
116
+ value: hastJson
117
+ }],
118
+ children: []
119
+ };
120
+ parent.children[index] = mdxNode;
121
+ needsApiExampleImport = true;
122
+ }
123
+ })();
124
+ promises.push(promise);
125
+ });
126
+ await Promise.all(promises);
127
+ if (needsApiExampleImport) {
128
+ if (!tree.children.some((node) => {
129
+ const n = node;
130
+ return n.type === "mdxjsEsm" && typeof n.value === "string" && n.value.includes("ApiExample");
131
+ })) {
132
+ const importNode = {
133
+ type: "mdxjsEsm",
134
+ value: "import { ApiExample } from \"rspress-plugin-api-extractor/runtime\";",
135
+ data: { estree: {
136
+ type: "Program",
137
+ body: [{
138
+ type: "ImportDeclaration",
139
+ specifiers: [{
140
+ type: "ImportSpecifier",
141
+ imported: {
142
+ type: "Identifier",
143
+ name: "ApiExample"
144
+ },
145
+ local: {
146
+ type: "Identifier",
147
+ name: "ApiExample"
148
+ }
149
+ }],
150
+ source: {
151
+ type: "Literal",
152
+ value: "rspress-plugin-api-extractor/runtime"
153
+ }
154
+ }],
155
+ sourceType: "module"
156
+ } }
157
+ };
158
+ let insertIndex = 0;
159
+ for (let i = 0; i < tree.children.length; i++) if (tree.children[i].type === "yaml") {
160
+ insertIndex = i + 1;
161
+ break;
162
+ }
163
+ tree.children.splice(insertIndex, 0, importNode);
164
+ }
165
+ }
166
+ const fileTime = performance.now() - fileStart;
167
+ if (blockCount > 0) console.log(`⏱️ [remark-with-api] Processed ${blockCount} blocks in ${fileTime.toFixed(0)}ms (avg: ${(fileTime / blockCount).toFixed(0)}ms per block)`);
168
+ };
169
+ };
170
+
171
+ //#endregion
172
+ export { remarkWithApi };
@@ -0,0 +1,52 @@
1
+ //#region src/route-collisions.ts
2
+ /**
3
+ * Group candidates by their final route (`${folder}/${baseName}`) and return the
4
+ * groups with more than one distinct item. The route key is the lowercased path
5
+ * the file is written to, so detection matches generation (and what a
6
+ * case-insensitive filesystem would merge). Companion pairs (same name, different
7
+ * folders) land under different keys and are never collisions.
8
+ *
9
+ * Output is deterministic: collisions ordered by route, items within a collision
10
+ * ordered by canonicalReference.
11
+ */
12
+ function detectRouteCollisions(candidates) {
13
+ const byKey = /* @__PURE__ */ new Map();
14
+ for (const candidate of candidates) {
15
+ const key = `${candidate.folder}/${candidate.baseName}`;
16
+ const group = byKey.get(key) ?? [];
17
+ group.push(candidate);
18
+ byKey.set(key, group);
19
+ }
20
+ const collisions = [];
21
+ for (const [route, group] of byKey) if (group.length > 1) {
22
+ const items = [...group].sort((a, b) => a.canonicalRef < b.canonicalRef ? -1 : a.canonicalRef > b.canonicalRef ? 1 : 0);
23
+ collisions.push({
24
+ route,
25
+ items
26
+ });
27
+ }
28
+ collisions.sort((a, b) => a.route < b.route ? -1 : a.route > b.route ? 1 : 0);
29
+ return collisions;
30
+ }
31
+ /** Build an actionable build-time error message for one or more route collisions. */
32
+ function formatRouteCollisionError(collisions, baseRoute) {
33
+ const lines = [];
34
+ for (const collision of collisions) {
35
+ lines.push(`Route collision: ${collision.items.length} API items resolve to the same documentation path "${baseRoute}/${collision.route}":`);
36
+ for (const item of collision.items) lines.push(` - ${item.displayName} (${item.kind}) [${item.canonicalRef}]`);
37
+ }
38
+ lines.push("");
39
+ lines.push("Item names must be unique per category folder. Paths are lowercased, so names differing only in case collide. Rename one of the items, or configure categories so they map to different folders.");
40
+ return `[rspress-plugin-api-extractor] ${lines.join("\n")}`;
41
+ }
42
+ /**
43
+ * Throw a descriptive error if any distinct items resolve to the same route.
44
+ * Called at build time before pages are generated, so collisions fail fast.
45
+ */
46
+ function assertNoRouteCollisions(candidates, baseRoute) {
47
+ const collisions = detectRouteCollisions(candidates);
48
+ if (collisions.length > 0) throw new Error(formatRouteCollisionError(collisions, baseRoute));
49
+ }
50
+
51
+ //#endregion
52
+ export { assertNoRouteCollisions };
@@ -1,29 +1,33 @@
1
- import { Fragment, jsx } from "react/jsx-runtime";
2
- import { createElement, useMemo } from "react";
3
1
  import { decodeHast } from "../../utils/decode-hast.js";
4
2
  import { ExampleBlock } from "../ExampleBlock/index.js";
3
+ import { createElement, useMemo } from "react";
4
+ import { Fragment, jsx } from "react/jsx-runtime";
5
+
6
+ //#region src/runtime/components/ApiExample/index.tsx
7
+ /**
8
+ * Renders an example code block.
9
+ *
10
+ * Replaces ExampleBlockWrapper with a simpler component that takes a plain
11
+ * code string (no base64, no HAST for code). The code should already have
12
+ * Twoslash directives stripped.
13
+ *
14
+ * In SSG-MD mode, renders a plain code block with the example code.
15
+ */
5
16
  function ApiExample({ code, hast }) {
6
- const parsedHast = useMemo(()=>decodeHast(hast, "ApiExample"), [
7
- hast
8
- ]);
9
- if (import.meta.env.SSG_MD) {
10
- const header = "```typescript\n";
11
- const footer = "\n```\n";
12
- return /*#__PURE__*/ jsx(Fragment, {
13
- children: `${header}${code.trim()}${footer}`
14
- });
15
- }
16
- if (parsedHast) return /*#__PURE__*/ createElement(ExampleBlock, {
17
- hast: parsedHast,
18
- code: code.trim()
19
- });
20
- return /*#__PURE__*/ jsx("pre", {
21
- children: /*#__PURE__*/ jsx("code", {
22
- className: "language-typescript",
23
- children: code.trim()
24
- })
25
- });
17
+ const parsedHast = useMemo(() => decodeHast(hast, "ApiExample"), [hast]);
18
+ if (import.meta.env.SSG_MD) return /* @__PURE__ */ jsx(Fragment, { children: `\`\`\`typescript
19
+ ${code.trim()}
20
+ \`\`\`
21
+ ` });
22
+ if (parsedHast) return createElement(ExampleBlock, {
23
+ hast: parsedHast,
24
+ code: code.trim()
25
+ });
26
+ return /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", {
27
+ className: "language-typescript",
28
+ children: code.trim()
29
+ }) });
26
30
  }
27
- const components_ApiExample = ApiExample;
28
- export default components_ApiExample;
29
- export { ApiExample };
31
+
32
+ //#endregion
33
+ export { ApiExample, ApiExample as default };