rspress-plugin-api-extractor 0.1.2 → 0.2.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 +2 -2
- package/api-extracted-package.js +426 -0
- package/build-program.js +120 -0
- package/build-stages.js +664 -0
- package/category-resolver.js +50 -0
- package/code-post-processor.js +38 -0
- package/config-helpers.js +106 -0
- package/config-utils.js +258 -0
- package/content-hash.js +79 -0
- package/errors.js +29 -0
- package/formatter.js +69 -0
- package/hide-cut-transformer.js +96 -0
- package/index.d.ts +901 -831
- package/index.js +4 -6128
- package/layers/ConfigServiceLive.js +377 -0
- package/layers/ObservabilityLive.js +136 -0
- package/layers/PathDerivationServiceLive.js +16 -0
- package/layers/SnapshotServiceLive.js +94 -0
- package/layers/TypeRegistryServiceLive.js +46 -0
- package/llms-processing.js +270 -0
- package/llms-program.js +262 -0
- package/loader.js +186 -0
- package/markdown/cross-linker.js +156 -0
- package/markdown/helpers.js +364 -0
- package/markdown/index.js +11 -0
- package/markdown/page-generators/class-page.js +357 -0
- package/markdown/page-generators/enum-page.js +152 -0
- package/markdown/page-generators/function-page.js +127 -0
- package/markdown/page-generators/index-pages.js +27 -0
- package/markdown/page-generators/interface-page.js +307 -0
- package/markdown/page-generators/namespace-page.js +280 -0
- package/markdown/page-generators/type-alias-page.js +110 -0
- package/markdown/page-generators/variable-page.js +110 -0
- package/markdown/shiki-utils.js +48 -0
- package/migrations/001_create_snapshots.js +25 -0
- package/model-loader.js +95 -0
- package/multi-entry-resolver.js +70 -0
- package/og-resolver.js +271 -0
- package/package.json +63 -73
- package/path-derivation.js +48 -0
- package/plugin.js +218 -0
- package/prettier-formatter.js +73 -0
- package/public/tsconfig/rspress.json +44 -0
- package/remark-api-codeblocks.js +130 -0
- package/remark-with-api.js +172 -0
- package/route-collisions.js +52 -0
- package/runtime/components/ApiExample/index.js +29 -25
- package/runtime/components/ApiLlmsPackageActions/index.js +252 -323
- package/runtime/components/ApiLlmsViewOptions/index.js +269 -340
- package/runtime/components/ApiMember/index.js +49 -47
- package/runtime/components/ApiSignature/index.js +32 -28
- package/runtime/components/EnumMembersTable/{index_module.css → index.css} +18 -19
- package/runtime/components/EnumMembersTable/index.js +36 -67
- package/runtime/components/EnumMembersTable/index.module.js +9 -6
- package/runtime/components/ExampleBlock/{index_module.css → index.css} +2 -3
- package/runtime/components/ExampleBlock/index.js +23 -28
- package/runtime/components/ExampleBlock/index.module.js +8 -5
- package/runtime/components/MarkdownContent/index.js +26 -18
- package/runtime/components/MarkdownText/index.js +28 -22
- package/runtime/components/MemberSignature/{index_module.css → index.css} +5 -6
- package/runtime/components/MemberSignature/index.js +46 -46
- package/runtime/components/MemberSignature/index.module.js +9 -6
- package/runtime/components/ParametersTable/{index_module.css → index.css} +19 -20
- package/runtime/components/ParametersTable/index.js +36 -67
- package/runtime/components/ParametersTable/index.module.js +9 -6
- package/runtime/components/SignatureBlock/{index_module.css → index.css} +5 -6
- package/runtime/components/SignatureBlock/index.js +30 -29
- package/runtime/components/SignatureBlock/index.module.js +9 -6
- package/runtime/components/SignatureCode/{index_module.css → index.css} +9 -10
- package/runtime/components/SignatureCode/index.js +40 -32
- package/runtime/components/SignatureCode/index.module.js +9 -6
- package/runtime/components/SignatureToolbar/{index_module.css → index.css} +18 -20
- package/runtime/components/SignatureToolbar/index.js +48 -51
- package/runtime/components/SignatureToolbar/index.module.js +13 -10
- package/runtime/components/buttons/ButtonGroup.js +13 -6
- package/runtime/components/buttons/CopyCodeButton.js +40 -38
- package/runtime/components/buttons/WrapSignatureButton.js +20 -16
- package/runtime/components/buttons/{index_module.css → index.css} +5 -6
- package/runtime/components/buttons/index.module.js +8 -5
- package/runtime/components/icons/CheckIcon/index.js +20 -17
- package/runtime/components/icons/CopyIcon/index.js +20 -17
- package/runtime/components/icons/UnwrapIcon/index.js +22 -18
- package/runtime/components/icons/WrapIcon/index.js +20 -17
- package/runtime/components/shared/_twoslash.css +3 -10
- package/runtime/components/shared/variables.css +0 -3
- package/runtime/hooks/useWrapToggle.js +32 -9
- package/runtime/index.d.ts +513 -173
- package/runtime/index.js +11 -9
- package/runtime/utils/decode-hast.js +31 -16
- package/runtime/utils/hast-renderer.js +21 -7
- package/schemas/config.js +199 -0
- package/schemas/index.js +5 -0
- package/schemas/opengraph.js +26 -0
- package/schemas/performance.js +19 -0
- package/serve.js +133 -0
- package/services/ConfigService.js +7 -0
- package/services/PathDerivationService.js +7 -0
- package/services/SnapshotService.js +7 -0
- package/services/TypeRegistryService.js +7 -0
- package/shiki-transformer.js +758 -0
- package/tsconfig-parser.js +127 -0
- package/tsdoc-metadata.json +11 -11
- package/twoslash-patterns.js +87 -0
- package/twoslash-transformer.js +316 -0
- package/type-reference-extractor.js +201 -0
- package/typescript-config.js +168 -0
- package/vfs-registry.js +121 -0
- package/0~llms-program.js +0 -344
- 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 "./node_modules/.pnpm/unist-util-visit@5.1.0/node_modules/unist-util-visit/lib/index.js";
|
|
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 { visit } from "./node_modules/.pnpm/unist-util-visit@5.1.0/node_modules/unist-util-visit/lib/index.js";
|
|
6
|
+
import { Effect, Metric } from "effect";
|
|
7
|
+
import { codeToHast, hastToHtml } from "shiki";
|
|
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(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, "\"").replace(/'/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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
export { ApiExample };
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
export { ApiExample, ApiExample as default };
|