rspress-plugin-api-extractor 0.8.9 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,7 @@
1
- import { ApiParser } from "../../loader.js";
2
1
  import { TypeReferenceExtractor } from "../../type-reference-extractor.js";
3
2
  import { escapeMdxGenerics, formatExampleCode, generateAvailableFrom, generateFrontmatter, prepareExampleCode, prependHiddenImports, stripTwoslashDirectives } from "../helpers.js";
4
- import { markdownCrossLinker } from "../cross-linker.js";
5
- import { TypeSignatureFormatter } from "../../formatter.js";
3
+ import { linkProse } from "../prose-linker.js";
4
+ import { ApiItems, Signature, Tsdoc } from "@tsdoctor/model";
6
5
 
7
6
  //#region src/markdown/page-generators/type-alias-page.ts
8
7
  /**
@@ -23,15 +22,14 @@ import { TypeSignatureFormatter } from "../../formatter.js";
23
22
  *
24
23
  * **Relationships:**
25
24
  * - Created and invoked by {@link ApiExtractorPlugin} during page generation
26
- * - Uses {@link TypeSignatureFormatter} for formatting type signatures
27
- * - Uses {@link ApiParser} for extracting documentation from API models
28
- * - Uses {@link MarkdownCrossLinker} for adding type reference links
25
+ * - Uses `Signature.format` from `@tsdoctor/model` for formatting type signatures
26
+ * - Uses the `Tsdoc` / `ApiItems` modules from `@tsdoctor/model` for extracting documentation
27
+ * - Uses the per-build prose linker (`linkProse`) for adding type reference links
29
28
  *
30
29
  * @see {@link InterfacePageGenerator} for interface documentation
31
30
  * @see {@link EnumPageGenerator} for enum documentation
32
31
  */
33
32
  var TypeAliasPageGenerator = class {
34
- typeFormatter = new TypeSignatureFormatter();
35
33
  /**
36
34
  * Generate a markdown page for a type alias
37
35
  *
@@ -40,22 +38,22 @@ var TypeAliasPageGenerator = class {
40
38
  async generate(apiTypeAlias, baseRoute, packageName, singularName, apiScope, apiName, sourceConfig, suppressExampleErrors, llmsPlugin, availableFrom) {
41
39
  const shouldSuppressErrors = suppressExampleErrors ?? true;
42
40
  const name = apiTypeAlias.displayName;
43
- const summary = ApiParser.getSummary(apiTypeAlias) || "No description available.";
44
- const releaseTag = ApiParser.getReleaseTag(apiTypeAlias);
41
+ const summary = Tsdoc.summary(apiTypeAlias) || "No description available.";
42
+ const releaseTag = Tsdoc.releaseTag(apiTypeAlias);
45
43
  let content = generateFrontmatter(name, summary, singularName, apiName);
46
44
  content += `import { SourceCode } from "@rspress/core/theme";\n`;
47
45
  content += `import { ParametersTable } from "rspress-plugin-api-extractor/runtime";\n`;
48
46
  content += `import { ApiSignature, ApiExample } from "rspress-plugin-api-extractor/runtime";\n\n`;
49
47
  content += `# ${name}\n\n`;
50
- const deprecation = ApiParser.getDeprecation(apiTypeAlias);
48
+ const deprecation = Tsdoc.deprecation(apiTypeAlias);
51
49
  if (deprecation) {
52
- const message = escapeMdxGenerics(markdownCrossLinker.addCrossLinks(deprecation.message));
50
+ const message = escapeMdxGenerics(linkProse(deprecation.message));
53
51
  content += `> ⚠️ **Deprecated:** ${message}\n\n`;
54
52
  }
55
53
  if (releaseTag !== "Public") content += `\`${releaseTag}\`\n\n`;
56
54
  content += `${summary}\n\n`;
57
55
  content += generateAvailableFrom(packageName, availableFrom);
58
- const sourceLink = ApiParser.getSourceLink(apiTypeAlias, sourceConfig);
56
+ const sourceLink = ApiItems.sourceLink(apiTypeAlias, sourceConfig);
59
57
  if (sourceLink) {
60
58
  content += `<div className="api-docs-toolbar">\n`;
61
59
  content += ` <div className="api-docs-toolbar-left">\n`;
@@ -68,7 +66,7 @@ var TypeAliasPageGenerator = class {
68
66
  content += `</div>\n\n`;
69
67
  }
70
68
  if (apiTypeAlias.excerpt.text) {
71
- const signature = this.typeFormatter.format(apiTypeAlias.excerpt).trim();
69
+ const signature = Signature.format(apiTypeAlias.excerpt).trim();
72
70
  let signatureWithImports = signature;
73
71
  const apiPackage = apiTypeAlias.getAssociatedPackage?.();
74
72
  if (apiPackage) {
@@ -78,7 +76,7 @@ var TypeAliasPageGenerator = class {
78
76
  const displayCode = stripTwoslashDirectives(signatureWithImports);
79
77
  content += `<ApiSignature code={${JSON.stringify(displayCode)}} source={${JSON.stringify(signatureWithImports)}} apiScope={${JSON.stringify(apiScope)}} />\n\n`;
80
78
  }
81
- const examples = ApiParser.getExamples(apiTypeAlias);
79
+ const examples = Tsdoc.examples(apiTypeAlias);
82
80
  if (examples.length > 0) {
83
81
  content += `## Examples\n\n`;
84
82
  for (const example of examples) {
@@ -93,11 +91,11 @@ var TypeAliasPageGenerator = class {
93
91
  } else content += `\`\`\`${prepared.language}\n${formattedCode}\n\`\`\`\n\n`;
94
92
  }
95
93
  }
96
- const seeReferences = ApiParser.getSeeReferences(apiTypeAlias);
94
+ const seeReferences = Tsdoc.seeReferences(apiTypeAlias);
97
95
  if (seeReferences.length > 0) {
98
96
  content += `## See Also\n\n`;
99
97
  for (const reference of seeReferences) {
100
- const refText = escapeMdxGenerics(markdownCrossLinker.addCrossLinks(reference.text));
98
+ const refText = escapeMdxGenerics(linkProse(reference.text));
101
99
  content += `- ${refText}\n`;
102
100
  }
103
101
  content += `\n`;
@@ -1,8 +1,7 @@
1
- import { ApiParser } from "../../loader.js";
2
1
  import { TypeReferenceExtractor } from "../../type-reference-extractor.js";
3
2
  import { escapeMdxGenerics, formatExampleCode, generateAvailableFrom, generateFrontmatter, prepareExampleCode, prependHiddenImports, stripTwoslashDirectives } from "../helpers.js";
4
- import { markdownCrossLinker } from "../cross-linker.js";
5
- import { TypeSignatureFormatter } from "../../formatter.js";
3
+ import { linkProse } from "../prose-linker.js";
4
+ import { ApiItems, Signature, Tsdoc } from "@tsdoctor/model";
6
5
 
7
6
  //#region src/markdown/page-generators/variable-page.ts
8
7
  /**
@@ -23,15 +22,14 @@ import { TypeSignatureFormatter } from "../../formatter.js";
23
22
  *
24
23
  * **Relationships:**
25
24
  * - Created and invoked by {@link ApiExtractorPlugin} during page generation
26
- * - Uses {@link TypeSignatureFormatter} for formatting type signatures
27
- * - Uses {@link ApiParser} for extracting documentation from API models
28
- * - Uses {@link MarkdownCrossLinker} for adding type reference links
25
+ * - Uses `Signature.format` from `@tsdoctor/model` for formatting type signatures
26
+ * - Uses the `Tsdoc` / `ApiItems` modules from `@tsdoctor/model` for extracting documentation
27
+ * - Uses the per-build prose linker (`linkProse`) for adding type reference links
29
28
  *
30
29
  * @see {@link FunctionPageGenerator} for function documentation
31
30
  * @see {@link EnumPageGenerator} for enum documentation
32
31
  */
33
32
  var VariablePageGenerator = class {
34
- typeFormatter = new TypeSignatureFormatter();
35
33
  /**
36
34
  * Generate a markdown page for a variable
37
35
  *
@@ -40,22 +38,22 @@ var VariablePageGenerator = class {
40
38
  async generate(apiVariable, baseRoute, packageName, singularName, apiScope, apiName, sourceConfig, suppressExampleErrors, llmsPlugin, availableFrom) {
41
39
  const shouldSuppressErrors = suppressExampleErrors ?? true;
42
40
  const name = apiVariable.displayName;
43
- const summary = ApiParser.getSummary(apiVariable) || "No description available.";
44
- const releaseTag = ApiParser.getReleaseTag(apiVariable);
41
+ const summary = Tsdoc.summary(apiVariable) || "No description available.";
42
+ const releaseTag = Tsdoc.releaseTag(apiVariable);
45
43
  let content = generateFrontmatter(name, summary, singularName, apiName);
46
44
  content += `import { SourceCode } from "@rspress/core/theme";\n`;
47
45
  content += `import { ParametersTable } from "rspress-plugin-api-extractor/runtime";\n`;
48
46
  content += `import { ApiSignature, ApiExample } from "rspress-plugin-api-extractor/runtime";\n\n`;
49
47
  content += `# ${name}\n\n`;
50
- const deprecation = ApiParser.getDeprecation(apiVariable);
48
+ const deprecation = Tsdoc.deprecation(apiVariable);
51
49
  if (deprecation) {
52
- const message = escapeMdxGenerics(markdownCrossLinker.addCrossLinks(deprecation.message));
50
+ const message = escapeMdxGenerics(linkProse(deprecation.message));
53
51
  content += `> ⚠️ **Deprecated:** ${message}\n\n`;
54
52
  }
55
53
  if (releaseTag !== "Public") content += `\`${releaseTag}\`\n\n`;
56
54
  content += `${summary}\n\n`;
57
55
  content += generateAvailableFrom(packageName, availableFrom);
58
- const sourceLink = ApiParser.getSourceLink(apiVariable, sourceConfig);
56
+ const sourceLink = ApiItems.sourceLink(apiVariable, sourceConfig);
59
57
  if (sourceLink) {
60
58
  content += `<div className="api-docs-toolbar">\n`;
61
59
  content += ` <div className="api-docs-toolbar-left">\n`;
@@ -68,7 +66,7 @@ var VariablePageGenerator = class {
68
66
  content += `</div>\n\n`;
69
67
  }
70
68
  if (apiVariable.excerpt.text) {
71
- const signature = this.typeFormatter.format(apiVariable.excerpt).trim();
69
+ const signature = Signature.format(apiVariable.excerpt).trim();
72
70
  let signatureWithImports = signature;
73
71
  const apiPackage = apiVariable.getAssociatedPackage?.();
74
72
  if (apiPackage) {
@@ -78,7 +76,7 @@ var VariablePageGenerator = class {
78
76
  const displayCode = stripTwoslashDirectives(signatureWithImports);
79
77
  content += `<ApiSignature code={${JSON.stringify(displayCode)}} source={${JSON.stringify(signatureWithImports)}} apiScope={${JSON.stringify(apiScope)}} />\n\n`;
80
78
  }
81
- const examples = ApiParser.getExamples(apiVariable);
79
+ const examples = Tsdoc.examples(apiVariable);
82
80
  if (examples.length > 0) {
83
81
  content += `## Examples\n\n`;
84
82
  for (const example of examples) {
@@ -93,11 +91,11 @@ var VariablePageGenerator = class {
93
91
  } else content += `\`\`\`${prepared.language}\n${formattedCode}\n\`\`\`\n\n`;
94
92
  }
95
93
  }
96
- const seeReferences = ApiParser.getSeeReferences(apiVariable);
94
+ const seeReferences = Tsdoc.seeReferences(apiVariable);
97
95
  if (seeReferences.length > 0) {
98
96
  content += `## See Also\n\n`;
99
97
  for (const reference of seeReferences) {
100
- const refText = escapeMdxGenerics(markdownCrossLinker.addCrossLinks(reference.text));
98
+ const refText = escapeMdxGenerics(linkProse(reference.text));
101
99
  content += `- ${refText}\n`;
102
100
  }
103
101
  content += `\n`;
@@ -0,0 +1,22 @@
1
+ import { CrossLinker } from "@tsdoctor/model";
2
+
3
+ //#region src/markdown/prose-linker.ts
4
+ /**
5
+ * Per-build prose cross-linker holder. Adapter wiring, not logic: the build
6
+ * program installs the immutable `@tsdoctor/model` CrossLinker built from the
7
+ * routes `prepareWorkItems` computed, and page generators link prose through
8
+ * it. Page generators run synchronously outside any service context, hence
9
+ * the module-level holder (the same shape as the sync-island event emitters).
10
+ */
11
+ let current = CrossLinker.empty;
12
+ /** Install the cross-linker for the current API build from its route map. */
13
+ function setProseLinker(routes) {
14
+ current = CrossLinker.fromRoutes(routes);
15
+ }
16
+ /** Cross-link prose text with the currently installed linker. */
17
+ function linkProse(text) {
18
+ return current.link(text);
19
+ }
20
+
21
+ //#endregion
22
+ export { linkProse, setProseLinker };
package/model-loader.js CHANGED
@@ -1,128 +1,74 @@
1
- import { PluginEvent } from "./observability/events.js";
2
1
  import { isLoadedModel, isVersionConfig } from "./config-utils.js";
3
2
  import fs from "node:fs";
4
3
  import path from "node:path";
5
- import { loadApiModel } from "@tsdoctor/model";
4
+ import { Effect } from "effect";
5
+ import { Model } from "@tsdoctor/model";
6
6
 
7
7
  //#region src/model-loader.ts
8
8
  /**
9
- * Module-level emitter seam. `loadFromPath` is called from inside an
10
- * `Effect.promise(async () => {...})` body in `ConfigServiceLive.ts`, so a
11
- * load failure cannot `yield* emit(...)` — it mirrors the sync-island pattern
12
- * used by `twoslash-transformer.ts` (`setEventEmitter`) and `loader.ts`
13
- * (`setLoaderEventEmitter`, a DIFFERENT module — the ApiParser/TSDoc statics,
14
- * not this one). Default is a no-op; wired in plugin.ts via
15
- * `setModelLoaderEventEmitter(emitSync, buildId)` right after the runtime
16
- * emitter is created.
9
+ * Load package.json from a path (string, URL, or Buffer).
17
10
  */
18
- let emitEvent = () => {};
19
- let currentBuildId = "";
11
+ async function loadPackageJsonFromPath(pkgPath) {
12
+ const resolvedPath = path.resolve(pkgPath.toString());
13
+ if (!fs.existsSync(resolvedPath)) throw new Error(`Package.json file not found: ${resolvedPath}`);
14
+ const content = fs.readFileSync(resolvedPath, "utf-8");
15
+ try {
16
+ return JSON.parse(content);
17
+ } catch (error) {
18
+ throw new Error(`Failed to parse package.json at ${resolvedPath}: ${error.message}`);
19
+ }
20
+ }
20
21
  /**
21
- * Inject the runtime-bound emitter into the model-loader module.
22
- * Call this right after `makeRuntimeEmitter` in plugin.ts.
22
+ * Load package.json from PathLike or async function.
23
23
  */
24
- function setModelLoaderEventEmitter(fn, buildId = "") {
25
- emitEvent = fn;
26
- currentBuildId = buildId;
24
+ async function loadPackageJson(loader) {
25
+ if (typeof loader === "function") return await loader();
26
+ return await loadPackageJsonFromPath(loader);
27
27
  }
28
+ /** Extract the first package from a user-loader result (ApiModel-shaped object). */
29
+ const packageFromLoaderResult = (result) => {
30
+ if (result && typeof result === "object" && "packages" in result) return Model.firstPackage(result).pipe(Effect.mapError(() => new Model.EmptyModelError({ reason: "API model returned by function contains no packages" })));
31
+ return Effect.fail(new Model.EmptyModelError({ reason: "API model loader function must return an ApiModel" }));
32
+ };
28
33
  /**
29
- * Utility class for loading API models from various sources
34
+ * Load an API model from a PathLike (via `Model.load`, typed errors) or a
35
+ * user-supplied async loader function (ApiModel or LoadedModel result).
30
36
  */
31
- var ApiModelLoader = class ApiModelLoader {
32
- /**
33
- * Private constructor to prevent instantiation
34
- */
35
- constructor() {}
36
- /**
37
- * Load an API model from a path (string, URL, or Buffer)
38
- */
39
- static async loadFromPath(modelPath) {
40
- const resolvedPath = path.resolve(modelPath.toString());
41
- try {
42
- if (!fs.existsSync(resolvedPath)) throw new Error(`API model file not found: ${resolvedPath}`);
43
- return await loadApiModel(resolvedPath);
44
- } catch (error) {
45
- try {
46
- emitEvent(PluginEvent.ModelLoadFailed({
47
- ctx: { buildId: currentBuildId },
48
- level: "error",
49
- modelPath: resolvedPath,
50
- reason: error instanceof Error ? error.message : String(error)
51
- }));
52
- } catch {}
53
- throw error;
54
- }
55
- }
56
- /**
57
- * Load package.json from a path (string, URL, or Buffer)
58
- */
59
- static async loadPackageJsonFromPath(pkgPath) {
60
- const resolvedPath = path.resolve(pkgPath.toString());
61
- if (!fs.existsSync(resolvedPath)) throw new Error(`Package.json file not found: ${resolvedPath}`);
62
- const content = fs.readFileSync(resolvedPath, "utf-8");
63
- try {
64
- return JSON.parse(content);
65
- } catch (error) {
66
- throw new Error(`Failed to parse package.json at ${resolvedPath}: ${error.message}`);
67
- }
68
- }
69
- /**
70
- * Load package.json from PathLike or async function
71
- */
72
- static async loadPackageJson(loader) {
73
- if (typeof loader === "function") return await loader();
74
- return await ApiModelLoader.loadPackageJsonFromPath(loader);
75
- }
76
- /**
77
- * Load an API model from PathLike or async function
78
- */
79
- static async loadApiModel(loader) {
80
- if (typeof loader === "function") {
81
- const result = await loader();
82
- if (isLoadedModel(result)) {
83
- const model = result.model;
84
- if (model && typeof model === "object" && "packages" in model) {
85
- const packages = model.packages;
86
- if (packages.length === 0) throw new Error("API model returned by function contains no packages");
87
- const loadedResult = { apiPackage: packages[0] };
88
- if (result.source != null) loadedResult.source = result.source;
89
- return loadedResult;
90
- }
91
- throw new Error("API model loader function must return an ApiModel");
92
- }
93
- if (result && typeof result === "object" && "packages" in result) {
94
- const packages = result.packages;
95
- if (packages.length === 0) throw new Error("API model returned by function contains no packages");
96
- return { apiPackage: packages[0] };
97
- }
98
- throw new Error("API model loader function must return an ApiModel or LoadedModel");
37
+ function loadApiModel(loader) {
38
+ if (typeof loader !== "function") return Model.load(loader.toString()).pipe(Effect.map((apiPackage) => ({ apiPackage })));
39
+ return Effect.gen(function* () {
40
+ const result = yield* Effect.promise(() => loader());
41
+ if (isLoadedModel(result)) {
42
+ const apiPackage = yield* packageFromLoaderResult(result.model);
43
+ return result.source != null ? {
44
+ apiPackage,
45
+ source: result.source
46
+ } : { apiPackage };
99
47
  }
100
- return { apiPackage: await ApiModelLoader.loadFromPath(loader) };
101
- }
102
- /**
103
- * Resolve and load a version config
104
- */
105
- static async loadVersionModel(versionValue) {
106
- if (isVersionConfig(versionValue)) {
107
- const { apiPackage, source: loaderSource } = await ApiModelLoader.loadApiModel(versionValue.model);
108
- const packageJson = versionValue.packageJson ? await ApiModelLoader.loadPackageJson(versionValue.packageJson) : void 0;
109
- const versionResult = { apiPackage };
110
- if (packageJson != null) versionResult.packageJson = packageJson;
111
- if (versionValue.categories != null) versionResult.categories = versionValue.categories;
112
- const resolvedSource = loaderSource || versionValue.source;
113
- if (resolvedSource != null) versionResult.source = resolvedSource;
114
- if (versionValue.externalPackages != null) versionResult.externalPackages = versionValue.externalPackages;
115
- if (versionValue.autoDetectDependencies != null) versionResult.autoDetectDependencies = versionValue.autoDetectDependencies;
116
- if (versionValue.ogImage != null) versionResult.ogImage = versionValue.ogImage;
117
- if (versionValue.llmsPlugin != null) versionResult.llmsPlugin = versionValue.llmsPlugin;
118
- return versionResult;
119
- }
120
- const { apiPackage, source } = await ApiModelLoader.loadApiModel(versionValue);
121
- const simpleResult = { apiPackage };
122
- if (source != null) simpleResult.source = source;
123
- return simpleResult;
124
- }
125
- };
48
+ return { apiPackage: yield* packageFromLoaderResult(result) };
49
+ });
50
+ }
51
+ /**
52
+ * Resolve and load a version config (full VersionConfig, or a bare model
53
+ * path/loader).
54
+ */
55
+ function loadVersionModel(versionValue) {
56
+ if (!isVersionConfig(versionValue)) return loadApiModel(versionValue);
57
+ return Effect.gen(function* () {
58
+ const { apiPackage, source: loaderSource } = yield* loadApiModel(versionValue.model);
59
+ const packageJson = versionValue.packageJson ? yield* Effect.promise(() => loadPackageJson(versionValue.packageJson)) : void 0;
60
+ const versionResult = { apiPackage };
61
+ if (packageJson != null) versionResult.packageJson = packageJson;
62
+ if (versionValue.categories != null) versionResult.categories = versionValue.categories;
63
+ const resolvedSource = loaderSource || versionValue.source;
64
+ if (resolvedSource != null) versionResult.source = resolvedSource;
65
+ if (versionValue.externalPackages != null) versionResult.externalPackages = versionValue.externalPackages;
66
+ if (versionValue.autoDetectDependencies != null) versionResult.autoDetectDependencies = versionValue.autoDetectDependencies;
67
+ if (versionValue.ogImage != null) versionResult.ogImage = versionValue.ogImage;
68
+ if (versionValue.llmsPlugin != null) versionResult.llmsPlugin = versionValue.llmsPlugin;
69
+ return versionResult;
70
+ });
71
+ }
126
72
 
127
73
  //#endregion
128
- export { ApiModelLoader, setModelLoaderEventEmitter };
74
+ export { loadApiModel, loadPackageJson, loadVersionModel };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rspress-plugin-api-extractor",
3
- "version": "0.8.9",
3
+ "version": "0.9.0",
4
4
  "private": false,
5
5
  "description": "RSPress plugin for generating API documentation from TypeScript API Extractor models",
6
6
  "keywords": [
@@ -38,19 +38,25 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@effect/platform-node": "4.0.0-rc.109",
41
- "@effect/sql-sqlite-node": "4.0.0-rc.109",
41
+ "@effected/github": "^0.8.0",
42
+ "@effected/glob": "^0.4.0",
43
+ "@effected/npm": "^0.12.0",
44
+ "@effected/package-json": "^0.11.0",
42
45
  "@effected/semver": "^0.5.0",
43
46
  "@effected/store": "^0.4.0",
44
47
  "@effected/tsconfig-json": "^0.6.0",
48
+ "@effected/walker": "^0.5.0",
45
49
  "@effected/xdg": "^0.3.0",
50
+ "@effected/yaml": "^0.11.0",
46
51
  "@microsoft/api-extractor-model": "^7.33.11",
47
52
  "@shikijs/twoslash": "^4.4.3",
48
- "@tsdoctor/model": "0.1.0",
49
- "@tsdoctor/registry": "0.1.0",
53
+ "@tsdoctor/bundle": "0.1.0",
54
+ "@tsdoctor/model": "0.2.0",
55
+ "@tsdoctor/registry": "0.2.0",
56
+ "@tsdoctor/snapshot": "0.1.0",
50
57
  "@typescript/vfs": "^1.6.4",
51
58
  "clsx": "^2.1.1",
52
59
  "effect": "4.0.0-rc.109",
53
- "gray-matter": "^4.0.3",
54
60
  "hast-util-to-jsx-runtime": "^2.3.6",
55
61
  "image-size": "^2.0.2",
56
62
  "ioredis": "^5.7.0",
package/plugin.js CHANGED
@@ -3,7 +3,6 @@ import { emit, makeRuntimeEmitter } from "./observability/EventBus.js";
3
3
  import { runHeartbeat } from "./observability/heartbeat.js";
4
4
  import { writeIssuesJson } from "./observability/sinks/issues-sink.js";
5
5
  import { buildEventBus, logBuildSummary, makeSummaryLoggerLayer } from "./layers/ObservabilityLive.js";
6
- import { setLoaderEventEmitter } from "./loader.js";
7
6
  import { setPrettierEventEmitter } from "./prettier-formatter.js";
8
7
  import { setOgResolverEventEmitter } from "./og-resolver.js";
9
8
  import { setBuildStagesEventEmitter } from "./build-stages.js";
@@ -14,14 +13,12 @@ import { deriveOutputPaths, normalizeBaseRoute, unscopedName } from "./path-deri
14
13
  import { fromDir, fromParentDir } from "./config-helpers.js";
15
14
  import { classifyApiConfig, mergeLlmsPluginConfig } from "./config-utils.js";
16
15
  import { DEFAULT_SHIKI_THEMES, setShikiUtilsEventEmitter } from "./markdown/shiki-utils.js";
17
- import { setModelLoaderEventEmitter } from "./model-loader.js";
18
16
  import { resolveObservability } from "./schemas/observability.js";
19
17
  import { PluginOptions } from "./schemas/config.js";
20
18
  import "./schemas/index.js";
21
19
  import { ConfigService } from "./services/ConfigService.js";
22
20
  import { ConfigServiceLive } from "./layers/ConfigServiceLive.js";
23
21
  import { PathDerivationServiceLive } from "./layers/PathDerivationServiceLive.js";
24
- import { SnapshotServiceLive } from "./layers/SnapshotServiceLive.js";
25
22
  import { TypeRegistryServiceLive } from "./layers/TypeRegistryServiceLive.js";
26
23
  import { remarkApiCodeblocks, setRemarkApiCodeblocksEventEmitter } from "./remark-api-codeblocks.js";
27
24
  import { remarkWithApi, setRemarkWithApiEventEmitter } from "./remark-with-api.js";
@@ -31,6 +28,7 @@ import fs from "node:fs";
31
28
  import path from "node:path";
32
29
  import { fileURLToPath } from "node:url";
33
30
  import { NodeFileSystem } from "@effect/platform-node";
31
+ import { SnapshotServiceLive } from "@tsdoctor/snapshot";
34
32
  import { Effect, FileSystem, Layer, ManagedRuntime, Ref, Schema } from "effect";
35
33
 
36
34
  //#region src/plugin.ts
@@ -93,14 +91,12 @@ function ApiExtractorPluginImpl(rawOptions) {
93
91
  const effectRuntime = ManagedRuntime.make(EffectAppLayer);
94
92
  const emitSync = makeRuntimeEmitter(effectRuntime);
95
93
  setEventEmitter(emitSync, buildId);
96
- setLoaderEventEmitter(emitSync, buildId);
97
94
  setShikiUtilsEventEmitter(emitSync, buildId);
98
95
  setPrettierEventEmitter(emitSync, buildId);
99
96
  setOgResolverEventEmitter(emitSync, buildId);
100
97
  setRemarkWithApiEventEmitter(emitSync, buildId, obs.thresholds.slowCodeBlock);
101
98
  setRemarkApiCodeblocksEventEmitter(emitSync, buildId);
102
99
  setBuildStagesEventEmitter(emitSync, buildId);
103
- setModelLoaderEventEmitter(emitSync, buildId);
104
100
  const fileContextMap = /* @__PURE__ */ new Map();
105
101
  let docsRoot;
106
102
  let isFirstBuild = true;
@@ -8,7 +8,7 @@
8
8
  * class member references (e.g., `ClassName.methodName`).
9
9
  *
10
10
  * **How it works:**
11
- * 1. The transformer is initialized with route and kind maps from {@link MarkdownCrossLinker}
11
+ * 1. The transformer is initialized with route and kind maps from `prepareWorkItems`
12
12
  * 2. During Shiki rendering, it walks the HAST tree looking for type names
13
13
  * 3. When a match is found, it wraps the text node in an anchor tag with the route
14
14
  * 4. Semantic CSS classes are added based on the API item kind (class, interface, etc.)
@@ -20,7 +20,7 @@
20
20
  *
21
21
  * **Relationships:**
22
22
  * - Initialized by {@link ApiExtractorPlugin} during the beforeBuild hook
23
- * - Receives route data from {@link MarkdownCrossLinker.initialize}
23
+ * - Receives route data from the cross-link maps built in `prepareWorkItems`
24
24
  * - Used alongside {@link TwoslashManager} for type-aware code blocks
25
25
  * - Works with the hide-cut transformer for member signatures
26
26
  *
@@ -44,7 +44,7 @@
44
44
  * });
45
45
  * ```
46
46
  *
47
- * @see {@link MarkdownCrossLinker} for the markdown equivalent
47
+ * @see the `@tsdoctor/model` CrossLinker for the markdown equivalent
48
48
  * @see {@link TwoslashManager} for type-aware documentation features
49
49
  */
50
50
  var ShikiCrossLinker = class {
@@ -0,0 +1,80 @@
1
+ import fs from "node:fs";
2
+ import { Effect, FileSystem, Layer, Option, Path, PlatformError } from "effect";
3
+
4
+ //#region src/sync-node-fs.ts
5
+ /**
6
+ * A synchronous, read-only `FileSystem` implementation over `node:fs`'s sync
7
+ * API, covering exactly the members `@tsdoctor/bundle`'s discovery walks:
8
+ * `exists`, `stat`, `readFileString`, `readDirectory` and `readLink`.
9
+ *
10
+ * The config helpers (`fromDir`/`fromParentDir`) are a SYNC public API called
11
+ * at rspress.config evaluation time, while bundle discovery is an Effect
12
+ * program over the `FileSystem` service. `@effect/platform-node`'s
13
+ * `NodeFileSystem` is promise-backed, so running discovery under
14
+ * `Effect.runSync` needs this bridge; every other member stays
15
+ * `layerNoop`-denied, which is deliberate — a new discovery dependency on an
16
+ * unimplemented member should fail loudly here, not silently misbehave.
17
+ */
18
+ const tagOf = (cause) => {
19
+ const code = cause?.code;
20
+ if (code === "ENOENT") return "NotFound";
21
+ if (code === "EACCES" || code === "EPERM") return "PermissionDenied";
22
+ return "Unknown";
23
+ };
24
+ const fail = (method, pathOrDescriptor, cause) => PlatformError.systemError({
25
+ _tag: tagOf(cause),
26
+ module: "FileSystem",
27
+ method,
28
+ pathOrDescriptor,
29
+ cause
30
+ });
31
+ const typeOf = (stats) => {
32
+ if (stats.isDirectory()) return "Directory";
33
+ if (stats.isSymbolicLink()) return "SymbolicLink";
34
+ if (stats.isFile()) return "File";
35
+ return "Unknown";
36
+ };
37
+ const infoFromStats = (stats) => ({
38
+ type: typeOf(stats),
39
+ mtime: Option.some(stats.mtime),
40
+ atime: Option.some(stats.atime),
41
+ birthtime: Option.some(stats.birthtime),
42
+ dev: stats.dev,
43
+ ino: Option.some(stats.ino),
44
+ mode: stats.mode,
45
+ nlink: Option.some(stats.nlink),
46
+ uid: Option.some(stats.uid),
47
+ gid: Option.some(stats.gid),
48
+ rdev: Option.some(stats.rdev),
49
+ size: FileSystem.Size(stats.size),
50
+ blksize: Option.some(FileSystem.Size(stats.blksize)),
51
+ blocks: Option.some(stats.blocks)
52
+ });
53
+ const syncFileSystem = FileSystem.layerNoop({
54
+ exists: (path) => Effect.sync(() => fs.existsSync(path)),
55
+ stat: (path) => Effect.try({
56
+ try: () => infoFromStats(fs.statSync(path)),
57
+ catch: (cause) => fail("stat", path, cause)
58
+ }),
59
+ readFileString: (path) => Effect.try({
60
+ try: () => fs.readFileSync(path, "utf8"),
61
+ catch: (cause) => fail("readFileString", path, cause)
62
+ }),
63
+ readDirectory: (path) => Effect.try({
64
+ try: () => fs.readdirSync(path),
65
+ catch: (cause) => fail("readDirectory", path, cause)
66
+ }),
67
+ readLink: (path) => Effect.try({
68
+ try: () => fs.readlinkSync(path),
69
+ catch: (cause) => fail("readLink", path, cause)
70
+ })
71
+ });
72
+ /**
73
+ * The full sync environment bundle discovery runs under from the config
74
+ * helpers: the sync `FileSystem` bridge plus the (already sync) `Path`
75
+ * service.
76
+ */
77
+ const SyncDiscoveryLayer = Layer.mergeAll(syncFileSystem, Path.layer);
78
+
79
+ //#endregion
80
+ export { SyncDiscoveryLayer };
@@ -204,7 +204,7 @@ function renderMarkdownInline(markdown, context) {
204
204
  * **VFS Integration:**
205
205
  * The VFS is populated by {@link TypeRegistryService} with:
206
206
  * - The documented package's own type definitions (from API Extractor)
207
- * - External package types (fetched via type-registry-effect)
207
+ * - External package types (fetched via @tsdoctor/registry)
208
208
  *
209
209
  * **Error Handling:**
210
210
  * TypeScript errors in code blocks are captured (not thrown) and: