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.
- 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 +64 -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,50 @@
|
|
|
1
|
+
//#region src/category-resolver.ts
|
|
2
|
+
/**
|
|
3
|
+
* Resolver for category and source configurations with proper precedence handling.
|
|
4
|
+
* Encapsulates the logic for merging category configs and resolving source configs.
|
|
5
|
+
*/
|
|
6
|
+
var CategoryResolver = class {
|
|
7
|
+
/**
|
|
8
|
+
* Deep merge two category config objects
|
|
9
|
+
* Later config overrides earlier config
|
|
10
|
+
*/
|
|
11
|
+
mergeTwoCategories(base, override) {
|
|
12
|
+
const result = { ...base };
|
|
13
|
+
for (const [key, config] of Object.entries(override)) if (result[key]) result[key] = {
|
|
14
|
+
...result[key],
|
|
15
|
+
...config,
|
|
16
|
+
itemKinds: config.itemKinds ?? result[key].itemKinds,
|
|
17
|
+
overviewHeaders: config.overviewHeaders ?? result[key].overviewHeaders
|
|
18
|
+
};
|
|
19
|
+
else result[key] = config;
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Merge category configs with proper precedence
|
|
24
|
+
* Later configs override earlier ones
|
|
25
|
+
*/
|
|
26
|
+
mergeCategories(...configs) {
|
|
27
|
+
let result = {};
|
|
28
|
+
for (const config of configs) if (config) result = this.mergeTwoCategories(result, config);
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolve final category config for a specific API/version.
|
|
33
|
+
* Precedence: built-in defaults, then plugin defaults, then package categories, then version categories.
|
|
34
|
+
*/
|
|
35
|
+
resolveCategoryConfig(pluginDefaults, packageCategories, versionCategories) {
|
|
36
|
+
return this.mergeCategories(pluginDefaults, packageCategories, versionCategories);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve final source config for a specific API/version.
|
|
40
|
+
* Precedence: package source, then version source, then loader source (highest).
|
|
41
|
+
* Loader source is passed in as versionSource since it's already been extracted.
|
|
42
|
+
*/
|
|
43
|
+
resolveSourceConfig(packageSource, versionSource) {
|
|
44
|
+
if (versionSource) return versionSource;
|
|
45
|
+
if (packageSource) return packageSource;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
//#endregion
|
|
50
|
+
export { CategoryResolver };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isTwoslashDirective } from "./twoslash-patterns.js";
|
|
2
|
+
|
|
3
|
+
//#region src/code-post-processor.ts
|
|
4
|
+
/**
|
|
5
|
+
* Add logical blank lines between code sections for visual clarity.
|
|
6
|
+
*
|
|
7
|
+
* Runs after Prettier formatting to insert breathing room between
|
|
8
|
+
* imports, comments, and return statements without affecting the
|
|
9
|
+
* structural formatting Prettier already applied.
|
|
10
|
+
*/
|
|
11
|
+
function addLogicalBlankLines(code) {
|
|
12
|
+
const lines = code.split("\n");
|
|
13
|
+
const result = [];
|
|
14
|
+
let inMultiLineImport = false;
|
|
15
|
+
for (let i = 0; i < lines.length; i++) {
|
|
16
|
+
const line = lines[i];
|
|
17
|
+
const trimmed = line.trim();
|
|
18
|
+
const wasInMultiLineImport = inMultiLineImport;
|
|
19
|
+
if (!inMultiLineImport && trimmed.startsWith("import ") && !trimmed.endsWith(";")) inMultiLineImport = true;
|
|
20
|
+
else if (inMultiLineImport && trimmed.endsWith(";")) inMultiLineImport = false;
|
|
21
|
+
const isCurrentImport = trimmed.startsWith("import ") || wasInMultiLineImport;
|
|
22
|
+
if (result.length > 0 && !isCurrentImport) {
|
|
23
|
+
const prevTrimmed = result[result.length - 1].trim();
|
|
24
|
+
if (prevTrimmed !== "") {
|
|
25
|
+
const isDirective = isTwoslashDirective(trimmed);
|
|
26
|
+
const prevIsImportEnd = prevTrimmed.startsWith("import ") && prevTrimmed.endsWith(";") || /}\s*from\s+/.test(prevTrimmed) && prevTrimmed.endsWith(";");
|
|
27
|
+
if (prevIsImportEnd && trimmed !== "" && !isDirective) result.push("");
|
|
28
|
+
if (trimmed.startsWith("//") && !isDirective && !prevTrimmed.startsWith("//") && !prevIsImportEnd) result.push("");
|
|
29
|
+
if (/^return[\s;(]/.test(trimmed) && !prevTrimmed.startsWith("//")) result.push("");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
result.push(line);
|
|
33
|
+
}
|
|
34
|
+
return result.join("\n");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
export { addLogicalBlankLines };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { normalizeBaseRoute, unscopedName } from "./path-derivation.js";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
//#region src/config-helpers.ts
|
|
6
|
+
const PREFIX = "[rspress-plugin-api-extractor]";
|
|
7
|
+
function discoverModel(dir, packageName) {
|
|
8
|
+
const apiJsonFiles = fs.readdirSync(dir).filter((f) => f.endsWith(".api.json"));
|
|
9
|
+
if (apiJsonFiles.length === 1) return path.join(dir, apiJsonFiles[0]);
|
|
10
|
+
if (apiJsonFiles.length === 0) throw new Error(`${PREFIX} api.fromDir: no *.api.json model found in ${dir}. Pass an explicit \`model\` to override.`);
|
|
11
|
+
const unscoped = unscopedName(packageName);
|
|
12
|
+
const preferred = apiJsonFiles.find((f) => f === `${unscoped}.api.json`);
|
|
13
|
+
if (preferred) return path.join(dir, preferred);
|
|
14
|
+
throw new Error(`${PREFIX} api.fromDir: multiple *.api.json files in ${dir} (${apiJsonFiles.join(", ")}) and none match "${unscoped}.api.json". Pass an explicit \`model\`.`);
|
|
15
|
+
}
|
|
16
|
+
function discoverDir(dir) {
|
|
17
|
+
let stat;
|
|
18
|
+
try {
|
|
19
|
+
stat = fs.statSync(dir);
|
|
20
|
+
} catch {
|
|
21
|
+
throw new Error(`${PREFIX} api.fromDir: directory not found: ${dir}`);
|
|
22
|
+
}
|
|
23
|
+
if (!stat.isDirectory()) throw new Error(`${PREFIX} api.fromDir: not a directory: ${dir}`);
|
|
24
|
+
let pkg;
|
|
25
|
+
try {
|
|
26
|
+
pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
27
|
+
} catch {
|
|
28
|
+
throw new Error(`${PREFIX} api.fromDir: missing or unreadable package.json in ${dir}`);
|
|
29
|
+
}
|
|
30
|
+
if (!pkg.name) throw new Error(`${PREFIX} api.fromDir: package.json in ${dir} has no "name" field`);
|
|
31
|
+
return {
|
|
32
|
+
dir,
|
|
33
|
+
dirname: path.basename(dir),
|
|
34
|
+
packageName: pkg.name,
|
|
35
|
+
version: pkg.version ?? "",
|
|
36
|
+
modelPath: discoverModel(dir, pkg.name)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function resolveBaseRoute(baseRoute, info) {
|
|
40
|
+
return normalizeBaseRoute((typeof baseRoute === "function" ? baseRoute(info) : baseRoute).replace(/\{dirname\}/g, info.dirname).replace(/\{packageName\}/g, info.packageName));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build a `MultiApiConfig` by discovering fields from a single package folder
|
|
44
|
+
* produced by `@savvy-web/rslib-builder`'s `localPaths` option. Exposed as
|
|
45
|
+
* `ApiExtractorPlugin.api.fromDir`; the returned config can be passed to the
|
|
46
|
+
* single-API `api:` option or used as an element of the multi-API `apis:` array.
|
|
47
|
+
*
|
|
48
|
+
* `baseRoute` is intentionally left unset unless overridden, so the plugin
|
|
49
|
+
* applies its own context-aware default (`/api` under `api:`,
|
|
50
|
+
* `/{packageName}/api` under `apis:`). See {@link BaseRoute}.
|
|
51
|
+
*/
|
|
52
|
+
function fromDir(dir, overrides = {}) {
|
|
53
|
+
const { baseRoute, cwd, ...rest } = overrides;
|
|
54
|
+
const info = discoverDir(path.resolve(cwd ?? process.cwd(), dir));
|
|
55
|
+
const discovered = {
|
|
56
|
+
packageName: info.packageName,
|
|
57
|
+
name: info.packageName,
|
|
58
|
+
model: info.modelPath,
|
|
59
|
+
packageJson: path.join(info.dir, "package.json")
|
|
60
|
+
};
|
|
61
|
+
if (baseRoute !== void 0) discovered.baseRoute = resolveBaseRoute(baseRoute, info);
|
|
62
|
+
const tsconfigPath = path.join(info.dir, "tsconfig.json");
|
|
63
|
+
if (fs.existsSync(tsconfigPath)) discovered.tsconfig = tsconfigPath;
|
|
64
|
+
return {
|
|
65
|
+
...discovered,
|
|
66
|
+
...rest
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function isModelFolder(dir) {
|
|
70
|
+
if (!fs.existsSync(path.join(dir, "package.json"))) return false;
|
|
71
|
+
try {
|
|
72
|
+
return fs.readdirSync(dir).some((f) => f.endsWith(".api.json"));
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Strictly scan a parent directory of package folders and build one
|
|
79
|
+
* `MultiApiConfig` per subfolder. Exposed as `ApiExtractorPlugin.apis.fromDir`;
|
|
80
|
+
* the returned array is intended for the multi-API `apis:` option. Every
|
|
81
|
+
* non-dotfile subdirectory MUST be a valid model folder. `options` (minus
|
|
82
|
+
* `cwd`) is applied as shared defaults to each `api.fromDir` call.
|
|
83
|
+
*/
|
|
84
|
+
function fromParentDir(parentDir, options = {}) {
|
|
85
|
+
const { cwd, ...rest } = options;
|
|
86
|
+
const absParent = path.resolve(cwd ?? process.cwd(), parentDir);
|
|
87
|
+
let stat;
|
|
88
|
+
try {
|
|
89
|
+
stat = fs.statSync(absParent);
|
|
90
|
+
} catch {
|
|
91
|
+
throw new Error(`${PREFIX} apis.fromDir: directory not found: ${absParent}`);
|
|
92
|
+
}
|
|
93
|
+
if (!stat.isDirectory()) throw new Error(`${PREFIX} apis.fromDir: not a directory: ${absParent}`);
|
|
94
|
+
const subdirs = fs.readdirSync(absParent, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort();
|
|
95
|
+
const configs = [];
|
|
96
|
+
for (const name of subdirs) {
|
|
97
|
+
const subdir = path.join(absParent, name);
|
|
98
|
+
if (!isModelFolder(subdir)) throw new Error(`${PREFIX} apis.fromDir: "${name}" in ${absParent} is not a valid model folder (needs package.json and a *.api.json). Use api.fromDir for selective inclusion.`);
|
|
99
|
+
configs.push(fromDir(subdir, rest));
|
|
100
|
+
}
|
|
101
|
+
if (configs.length === 0) throw new Error(`${PREFIX} apis.fromDir: no model folders found in ${absParent}. Have the package models been built?`);
|
|
102
|
+
return configs;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
//#endregion
|
|
106
|
+
export { fromDir, fromParentDir };
|
package/config-utils.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { SemVer } from "semver-effect";
|
|
3
|
+
|
|
4
|
+
//#region src/config-utils.ts
|
|
5
|
+
/**
|
|
6
|
+
* Type guard to check if version value is a full VersionConfig
|
|
7
|
+
*/
|
|
8
|
+
function isVersionConfig(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && !(value instanceof URL) && !Buffer.isBuffer(value) && "model" in value;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Type guard to check if loader result includes source config
|
|
13
|
+
*/
|
|
14
|
+
function isLoadedModel(result) {
|
|
15
|
+
return typeof result === "object" && result !== null && "model" in result;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Normalize llmsPlugin config to always be an LlmsPlugin object
|
|
19
|
+
*/
|
|
20
|
+
function normalizeLlmsPluginConfig(config) {
|
|
21
|
+
if (config === false) return { enabled: false };
|
|
22
|
+
if (config === true || config === void 0) return { enabled: true };
|
|
23
|
+
return {
|
|
24
|
+
enabled: true,
|
|
25
|
+
...config
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Merge LLM plugin configurations with precedence: version, then API, then global.
|
|
30
|
+
* Returns merged config with sensible defaults.
|
|
31
|
+
*/
|
|
32
|
+
function mergeLlmsPluginConfig(globalConfig, apiConfig, versionConfig) {
|
|
33
|
+
const merged = {
|
|
34
|
+
...normalizeLlmsPluginConfig(globalConfig),
|
|
35
|
+
...apiConfig,
|
|
36
|
+
...versionConfig
|
|
37
|
+
};
|
|
38
|
+
if (merged.enabled) return {
|
|
39
|
+
enabled: true,
|
|
40
|
+
scopes: merged.scopes ?? true,
|
|
41
|
+
apiTxt: merged.apiTxt ?? true,
|
|
42
|
+
showCopyButton: merged.showCopyButton ?? true,
|
|
43
|
+
showViewOptions: merged.showViewOptions ?? true,
|
|
44
|
+
copyButtonText: merged.copyButtonText ?? "Copy Markdown",
|
|
45
|
+
viewOptions: merged.viewOptions ?? [
|
|
46
|
+
"markdownLink",
|
|
47
|
+
"chatgpt",
|
|
48
|
+
"claude"
|
|
49
|
+
]
|
|
50
|
+
};
|
|
51
|
+
return { enabled: false };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Common type utility packages to automatically load from devDependencies.
|
|
55
|
+
* These packages provide type transformations and utilities commonly used in TypeScript projects.
|
|
56
|
+
*/
|
|
57
|
+
const TYPE_UTILITY_PACKAGES = ["type-fest", "ts-extras"];
|
|
58
|
+
/**
|
|
59
|
+
* Extract peerDependencies from PackageJson and convert to ExternalPackageSpec array.
|
|
60
|
+
* This allows automatic loading of peer dependency types for documentation examples.
|
|
61
|
+
*
|
|
62
|
+
* @param packageJson - The parsed package.json object
|
|
63
|
+
* @returns Array of external package specs from peerDependencies, or empty array if none
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* const pkg = { name: "my-lib", peerDependencies: { "zod": "^3.22.4" } };
|
|
68
|
+
* const external = extractPeerDependencies(pkg);
|
|
69
|
+
* // Returns: [{ name: "zod", version: "^3.22.4" }]
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
function extractPeerDependencies(packageJson) {
|
|
73
|
+
if (!packageJson?.peerDependencies) return [];
|
|
74
|
+
return Object.entries(packageJson.peerDependencies).map(([name, version]) => ({
|
|
75
|
+
name,
|
|
76
|
+
version
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Extract type utility packages (type-fest, ts-extras) from devDependencies.
|
|
81
|
+
* These packages are commonly used for type transformations and should be available in documentation examples.
|
|
82
|
+
*
|
|
83
|
+
* @param packageJson - The parsed package.json object
|
|
84
|
+
* @returns Array of external package specs for type utilities found in devDependencies
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* const pkg = { devDependencies: { "type-fest": "^4.0.0", "ts-extras": "^0.12.0" } };
|
|
89
|
+
* const external = extractTypeUtilities(pkg);
|
|
90
|
+
* // Returns: [{ name: "type-fest", version: "^4.0.0" }, { name: "ts-extras", version: "^0.12.0" }]
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
function extractTypeUtilities(packageJson) {
|
|
94
|
+
if (!packageJson?.devDependencies) return [];
|
|
95
|
+
const utilities = [];
|
|
96
|
+
for (const utilityName of TYPE_UTILITY_PACKAGES) {
|
|
97
|
+
const version = packageJson.devDependencies[utilityName];
|
|
98
|
+
if (version) utilities.push({
|
|
99
|
+
name: utilityName,
|
|
100
|
+
version
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return utilities;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Extract all automatically-detected external packages from package.json.
|
|
107
|
+
* Controlled by AutoDetectDependencies to determine which dependency types to include.
|
|
108
|
+
*
|
|
109
|
+
* @param packageJson - The parsed package.json object
|
|
110
|
+
* @param options - Options controlling which dependency types to include
|
|
111
|
+
* @returns Array of all external package specs to load for documentation
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* const pkg = {
|
|
116
|
+
* dependencies: { "effect": "^3.0.0" },
|
|
117
|
+
* peerDependencies: { "zod": "^3.22.4" },
|
|
118
|
+
* devDependencies: { "type-fest": "^4.0.0" }
|
|
119
|
+
* };
|
|
120
|
+
*
|
|
121
|
+
* // Default: only peerDependencies + type utilities
|
|
122
|
+
* extractAutoDetectedPackages(pkg);
|
|
123
|
+
* // Returns: [{ name: "zod", version: "^3.22.4" }, { name: "type-fest", version: "^4.0.0" }]
|
|
124
|
+
*
|
|
125
|
+
* // Include all dependency types
|
|
126
|
+
* extractAutoDetectedPackages(pkg, { dependencies: true, peerDependencies: true, autoDependencies: true });
|
|
127
|
+
* // Returns: [{ name: "effect", ... }, { name: "zod", ... }, { name: "type-fest", ... }]
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
function extractAutoDetectedPackages(packageJson, options = {}) {
|
|
131
|
+
const { dependencies = false, devDependencies = false, peerDependencies = true, autoDependencies = true } = options;
|
|
132
|
+
const packages = [];
|
|
133
|
+
if (dependencies && packageJson?.dependencies) packages.push(...Object.entries(packageJson.dependencies).map(([name, version]) => ({
|
|
134
|
+
name,
|
|
135
|
+
version
|
|
136
|
+
})));
|
|
137
|
+
if (devDependencies && packageJson?.devDependencies) packages.push(...Object.entries(packageJson.devDependencies).filter(([name]) => !autoDependencies || !TYPE_UTILITY_PACKAGES.includes(name)).map(([name, version]) => ({
|
|
138
|
+
name,
|
|
139
|
+
version
|
|
140
|
+
})));
|
|
141
|
+
if (peerDependencies) packages.push(...extractPeerDependencies(packageJson));
|
|
142
|
+
if (autoDependencies) packages.push(...extractTypeUtilities(packageJson));
|
|
143
|
+
return resolvePackageVersionConflicts(packages);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Deduplicate external packages by name, resolving to the highest version when conflicts exist.
|
|
147
|
+
* Uses semver-effect to pick the highest version from duplicates.
|
|
148
|
+
*
|
|
149
|
+
* @param packages - Array of external package specs (may contain duplicates)
|
|
150
|
+
* @returns Deduplicated array with highest versions
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```ts
|
|
154
|
+
* const packages = [
|
|
155
|
+
* { name: "zod", version: "^3.22.4" },
|
|
156
|
+
* { name: "zod", version: "^3.23.0" },
|
|
157
|
+
* { name: "effect", version: "^3.0.0" }
|
|
158
|
+
* ];
|
|
159
|
+
* const resolved = resolvePackageVersionConflicts(packages);
|
|
160
|
+
* // Returns: [{ name: "zod", version: "^3.23.0" }, { name: "effect", version: "^3.0.0" }]
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
function resolvePackageVersionConflicts(packages) {
|
|
164
|
+
const packageMap = /* @__PURE__ */ new Map();
|
|
165
|
+
for (const pkg of packages) {
|
|
166
|
+
const versions = packageMap.get(pkg.name) || [];
|
|
167
|
+
versions.push(pkg.version);
|
|
168
|
+
packageMap.set(pkg.name, versions);
|
|
169
|
+
}
|
|
170
|
+
const resolved = [];
|
|
171
|
+
for (const [name, versions] of packageMap) {
|
|
172
|
+
if (versions.length === 1) {
|
|
173
|
+
resolved.push({
|
|
174
|
+
name,
|
|
175
|
+
version: versions[0]
|
|
176
|
+
});
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const highestVersion = findHighestVersion(versions);
|
|
180
|
+
resolved.push({
|
|
181
|
+
name,
|
|
182
|
+
version: highestVersion
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return resolved;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Strip range prefixes from a version string to get a clean semver.
|
|
189
|
+
*/
|
|
190
|
+
function stripRangePrefix(version) {
|
|
191
|
+
return version.replace(/^[~^>=<]+\s*/, "");
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Find the highest version from a list of version specifiers using semver-effect.
|
|
195
|
+
* Handles version ranges and exact versions.
|
|
196
|
+
*
|
|
197
|
+
* @param versions - Array of version strings (can be ranges or exact versions)
|
|
198
|
+
* @returns The highest version specifier
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```ts
|
|
202
|
+
* findHighestVersion(["^3.22.4", "^3.23.0", "3.22.5"])
|
|
203
|
+
* // Returns: "^3.23.0"
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
function findHighestVersion(versions) {
|
|
207
|
+
const parsedVersions = [];
|
|
208
|
+
for (const version of versions) {
|
|
209
|
+
const cleaned = stripRangePrefix(version);
|
|
210
|
+
const result = Effect.runSyncExit(SemVer.parse(cleaned));
|
|
211
|
+
if (result._tag === "Success") parsedVersions.push({
|
|
212
|
+
original: version,
|
|
213
|
+
version: result.value
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (parsedVersions.length === 0) return versions[versions.length - 1];
|
|
217
|
+
parsedVersions.sort((a, b) => {
|
|
218
|
+
if (SemVer.gt(a.version, b.version)) return -1;
|
|
219
|
+
if (SemVer.lt(a.version, b.version)) return 1;
|
|
220
|
+
return 0;
|
|
221
|
+
});
|
|
222
|
+
return parsedVersions[0].original;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Validate that manually specified externalPackages don't conflict with peerDependencies.
|
|
226
|
+
* Throws an error if a package appears in both with different versions.
|
|
227
|
+
*
|
|
228
|
+
* @param externalPackages - Manually specified external packages
|
|
229
|
+
* @param packageJson - The parsed package.json object
|
|
230
|
+
* @throws Error if versions conflict
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```ts
|
|
234
|
+
* const external = [{ name: "zod", version: "3.22.4" }];
|
|
235
|
+
* const pkg = { peerDependencies: { "zod": "^3.22.4" } };
|
|
236
|
+
* validateExternalPackages(external, pkg);
|
|
237
|
+
* // Throws if versions conflict
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
function validateExternalPackages(externalPackages, packageJson) {
|
|
241
|
+
if (!externalPackages || !packageJson?.peerDependencies) return;
|
|
242
|
+
const conflicts = [];
|
|
243
|
+
for (const pkg of externalPackages) {
|
|
244
|
+
const peerVersion = packageJson.peerDependencies[pkg.name];
|
|
245
|
+
if (peerVersion && peerVersion !== pkg.version) conflicts.push({
|
|
246
|
+
name: pkg.name,
|
|
247
|
+
external: pkg.version,
|
|
248
|
+
peer: peerVersion
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (conflicts.length > 0) {
|
|
252
|
+
const details = conflicts.map((c) => ` - ${c.name}: externalPackages="${c.external}" vs peerDependencies="${c.peer}"`).join("\n");
|
|
253
|
+
throw new Error(`Version conflict detected between externalPackages and peerDependencies:\n${details}\n\nRemove conflicting entries from externalPackages to use peerDependencies versions automatically.`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
//#endregion
|
|
258
|
+
export { extractAutoDetectedPackages, isLoadedModel, isVersionConfig, mergeLlmsPluginConfig, validateExternalPackages };
|
package/content-hash.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
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 };
|
package/errors.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Data } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/errors.ts
|
|
4
|
+
const ConfigValidationErrorBase = Data.TaggedError("ConfigValidationError");
|
|
5
|
+
var ConfigValidationError = class extends ConfigValidationErrorBase {
|
|
6
|
+
get message() {
|
|
7
|
+
return `Config validation failed for '${this.field}': ${this.reason}`;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
const ApiModelLoadErrorBase = Data.TaggedError("ApiModelLoadError");
|
|
11
|
+
const SnapshotDbErrorBase = Data.TaggedError("SnapshotDbError");
|
|
12
|
+
var SnapshotDbError = class extends SnapshotDbErrorBase {
|
|
13
|
+
get message() {
|
|
14
|
+
return `Snapshot DB error during '${this.operation}' at '${this.dbPath}': ${this.reason}`;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const PathDerivationErrorBase = Data.TaggedError("PathDerivationError");
|
|
18
|
+
const TypeRegistryErrorBase = Data.TaggedError("TypeRegistryError");
|
|
19
|
+
var TypeRegistryError = class extends TypeRegistryErrorBase {
|
|
20
|
+
get message() {
|
|
21
|
+
return `Type registry error for '${this.packageName}@${this.version}': ${this.reason}`;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const PageGenerationErrorBase = Data.TaggedError("PageGenerationError");
|
|
25
|
+
const TwoslashProcessingErrorBase = Data.TaggedError("TwoslashProcessingError");
|
|
26
|
+
const PrettierFormatErrorBase = Data.TaggedError("PrettierFormatError");
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
export { ConfigValidationError, SnapshotDbError, TypeRegistryError };
|
package/formatter.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
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 };
|