rspress-plugin-api-extractor 0.15.2 → 0.16.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/build-program.js CHANGED
@@ -36,7 +36,7 @@ function generateApiDocs(apiConfig, fileContextMap) {
36
36
  const pageConcurrency = yield* PageConcurrency;
37
37
  const suppressExampleErrors = yield* SuppressExampleErrors;
38
38
  const snapshotSvc = yield* SnapshotService;
39
- const { apiPackage, packageName, apiName, outputDir, baseRoute, categories, source, packageJson, llmsPlugin, siteUrl, ogImage, manifest } = apiConfig;
39
+ const { apiPackage, packageName, apiName, outputDir, baseRoute, categories, source, packageJson, llmsPlugin, siteUrl, ogImage, manifest, siteName, bundleOgImage } = apiConfig;
40
40
  const phaseCtx = { packageName };
41
41
  const structuredDataPkg = manifest != null && siteUrl != null ? packageContext({
42
42
  siteUrl,
@@ -91,7 +91,9 @@ function generateApiDocs(apiConfig, fileContextMap) {
91
91
  ...apiConfig.docsRoot != null ? { docsRoot: apiConfig.docsRoot } : {},
92
92
  ...siteUrl != null ? { siteUrl } : {},
93
93
  ...ogImage != null ? { ogImage } : {},
94
- ...structuredDataPkg != null ? { structuredDataPkg } : {}
94
+ ...structuredDataPkg != null ? { structuredDataPkg } : {},
95
+ ...siteName != null ? { siteName } : {},
96
+ ...bundleOgImage != null ? { bundleOgImage } : {}
95
97
  }));
96
98
  const changedCount = fileResults.filter((r) => r.status !== "unchanged").length;
97
99
  yield* Effect.logDebug(`Generated ${changedCount} pages`);
package/build-stages.js CHANGED
@@ -163,7 +163,8 @@ function generateSinglePage(workItem, ctx) {
163
163
  siteUrl,
164
164
  docsRoot: ctx.docsRoot,
165
165
  packageName,
166
- ...apiName != null ? { apiName } : {}
166
+ ...apiName != null ? { apiName } : {},
167
+ fallbackAlt: `${apiName ?? packageName} API documentation`
167
168
  }));
168
169
  if (ogImageResult._tag === "Failure") {
169
170
  const failure = ogImageResult.failure;
@@ -178,6 +179,7 @@ function generateSinglePage(workItem, ctx) {
178
179
  level: "warn"
179
180
  }));
180
181
  } else if (Option.isSome(ogImageResult.success)) ogImageMetadata = ogImageResult.success.value;
182
+ if (ogImageMetadata == null && ctx.bundleOgImage != null) ogImageMetadata = ctx.bundleOgImage;
181
183
  if (ctx.structuredDataPkg != null) {
182
184
  const graphResult = deriveScriptBody(ctx.structuredDataPkg, {
183
185
  pageRoute: page.routePath,
@@ -215,11 +217,13 @@ function generateSinglePage(workItem, ctx) {
215
217
  const finalFrontmatter = (published, modified) => seoEnabled ? generateFrontmatter(item.displayName, description, categoryConfig.singularName, apiName, headTags({
216
218
  siteUrl: ctx.siteUrl,
217
219
  pageRoute: page.routePath,
220
+ title: item.displayName,
218
221
  description,
219
222
  publishedTime: published,
220
223
  modifiedTime: modified,
221
224
  section: categoryConfig.displayName,
222
225
  packageName,
226
+ ...ctx.siteName != null ? { siteName: ctx.siteName } : {},
223
227
  ...ogImageMetadata != null ? { ogImage: ogImageMetadata } : {},
224
228
  ...structuredData != null ? { structuredData } : {}
225
229
  })) : stringifyFrontmatter("", frontmatterData);
@@ -649,7 +653,9 @@ function buildPipelineForApi(input) {
649
653
  ...input.docsRoot !== void 0 ? { docsRoot: input.docsRoot } : {},
650
654
  ...input.siteUrl != null ? { siteUrl: input.siteUrl } : {},
651
655
  ...input.ogImage != null ? { ogImage: input.ogImage } : {},
652
- ...input.structuredDataPkg != null ? { structuredDataPkg: input.structuredDataPkg } : {}
656
+ ...input.structuredDataPkg != null ? { structuredDataPkg: input.structuredDataPkg } : {},
657
+ ...input.siteName != null ? { siteName: input.siteName } : {},
658
+ ...input.bundleOgImage != null ? { bundleOgImage: input.bundleOgImage } : {}
653
659
  };
654
660
  const writeCtx = {
655
661
  buildId: input.buildId,
@@ -9,7 +9,7 @@ import { TypeRegistryService } from "../services/TypeRegistryService.js";
9
9
  import { TwoslashCacheService } from "../services/TwoslashCacheService.js";
10
10
  import { ConfigService } from "../services/ConfigService.js";
11
11
  import { makeSummaryLoggerLayer } from "./observability.js";
12
- import { Layer } from "effect";
12
+ import { Layer, Path } from "effect";
13
13
  import { SnapshotService } from "@tsdoctor/snapshot";
14
14
  import { NodeFileSystem } from "@effect/platform-node";
15
15
 
@@ -58,7 +58,7 @@ function makeAppLayers(input) {
58
58
  /** Services scoped to this build's configuration. */
59
59
  const BuildLayer = Layer.mergeAll(Layer.succeed(PluginConfig, input.options), HighlighterLive, TwoslashEnvironments.layer, BuildEnvLayer);
60
60
  return {
61
- app: Layer.provideMerge(ConfigService.layer, Layer.mergeAll(BuildLayer, CoreLayer, ObservabilityLayer, NodeFileSystem.layer)),
61
+ app: Layer.provideMerge(ConfigService.layer, Layer.mergeAll(BuildLayer, CoreLayer, ObservabilityLayer, NodeFileSystem.layer, Path.layer)),
62
62
  emitter: Layer.mergeAll(ObservabilityLayer, BuildEnvLayer)
63
63
  };
64
64
  }
@@ -15,11 +15,13 @@ import { TypeRegistryService } from "../services/TypeRegistryService.js";
15
15
  import { emitVfsPayloadEvents, mergeApiResult } from "./api-results.js";
16
16
  import { mergeExternalTypes } from "./external-types.js";
17
17
  import { registerTypeEnvironments, resolveTsConfigTyped } from "./type-environment.js";
18
+ import path from "node:path";
18
19
  import { apiScopeOf, normalizeBaseRoute, unscopedName } from "@tsdoctor/pages";
19
20
  import { Effect, Metric } from "effect";
20
21
  import { ApiExtractedPackage, TypeReferenceExtractor } from "@tsdoctor/model";
21
22
  import { deriveSiteUrl } from "@tsdoctor/seo";
22
23
  import { hashContent } from "@tsdoctor/snapshot";
24
+ import { loadBundle, publishBundleAssets, resolveBundleFrom } from "@tsdoctor/bundle";
23
25
  import { PackageManifest } from "@effected/package-json";
24
26
 
25
27
  //#region src/layers/config-resolution.ts
@@ -95,6 +97,83 @@ function decodeManifest(packageJson, buildId, packageName) {
95
97
  });
96
98
  }
97
99
  /**
100
+ * Resolve an API's bundle — display identity and Open Graph images — from its
101
+ * model directory, with provenance across the manifest tiers.
102
+ *
103
+ * @remarks
104
+ * Every API resolves a bundle, even one declared with a loader function
105
+ * rather than a path: a loader has no directory to discover a `tsdoctor.json`
106
+ * sidecar beside, so resolution falls back to an inferred bundle carrying
107
+ * only the package name — the same floor {@link resolveBundleFrom} itself
108
+ * guarantees for `name`.
109
+ *
110
+ * A present-but-malformed `tsdoctor.json` fails the build typed, the same
111
+ * posture as a malformed tsconfig: {@link ConfigValidationError} with
112
+ * `field: "bundle"`. Asset publishing degrades instead — a malformed or
113
+ * unreadable image must not stop the docs from generating, it should just
114
+ * render without a bundle-supplied `og:image`.
115
+ */
116
+ function resolveApiBundle(input) {
117
+ const inferredBundle = {
118
+ bundle: { name: {
119
+ value: input.packageName,
120
+ source: "inferred"
121
+ } },
122
+ siteName: input.packageName
123
+ };
124
+ return Effect.gen(function* () {
125
+ const modelPath = input.model;
126
+ if (typeof modelPath !== "string") return inferredBundle;
127
+ const modelDir = path.dirname(modelPath);
128
+ const degradeToWarning = (cause) => Effect.andThen(emit(PluginEvent.ConfigValidationWarning({
129
+ ctx: {
130
+ buildId: input.buildId,
131
+ packageName: input.packageName
132
+ },
133
+ field: "bundle",
134
+ value: cause.path,
135
+ reason: cause.message,
136
+ level: "warn"
137
+ })), Effect.succeed(void 0));
138
+ const bundle = yield* loadBundle(modelDir, { overrides: {
139
+ modelPath,
140
+ name: input.packageName
141
+ } }).pipe(Effect.catchTag("BundleManifestError", (cause) => Effect.fail(new ConfigValidationError({
142
+ field: "bundle",
143
+ reason: cause.message,
144
+ cause
145
+ }))), Effect.catchTags({
146
+ BundleDiscoveryError: degradeToWarning,
147
+ BundleLayerError: degradeToWarning
148
+ }));
149
+ if (bundle === void 0) return inferredBundle;
150
+ const resolved = resolveBundleFrom(bundle, {});
151
+ const siteName = resolved.project?.value.name ?? resolved.name.value;
152
+ const published = resolved.openGraph !== void 0 && input.siteUrl != null ? yield* publishBundleAssets({
153
+ bundleDir: bundle.descriptor.dir,
154
+ images: resolved.openGraph.value.images,
155
+ publicDir: path.join(input.rspressRoot, "public"),
156
+ siteUrl: input.siteUrl,
157
+ unscopedName: unscopedName(input.packageName),
158
+ ...input.assetSubdir != null ? { subdir: input.assetSubdir } : {}
159
+ }).pipe(Effect.tapError((error) => emit(PluginEvent.ConfigValidationWarning({
160
+ ctx: {
161
+ buildId: input.buildId,
162
+ packageName: input.packageName
163
+ },
164
+ field: "openGraph",
165
+ value: error.path,
166
+ reason: error.message,
167
+ level: "warn"
168
+ }))), Effect.orElseSucceed(() => [])) : [];
169
+ return {
170
+ bundle: resolved,
171
+ siteName,
172
+ ...published[0] != null ? { bundleOgImage: published[0] } : {}
173
+ };
174
+ });
175
+ }
176
+ /**
98
177
  * Validate plugin options and return an Effect that fails with ConfigValidationError.
99
178
  */
100
179
  function validateOptions(options, rspressConfig) {
@@ -244,6 +323,13 @@ const makeConfigService = Effect.gen(function* () {
244
323
  const vfsPayloads = prependImportsToVfs(vfs, apiPackage, api.packageName, wantTrace);
245
324
  const resolvedOgImage = api.ogImage ?? options.ogImage;
246
325
  const resolvedTheme = normalizeThemeConfig(api.theme);
326
+ const { bundle, siteName, bundleOgImage } = yield* resolveApiBundle({
327
+ model,
328
+ packageName: api.packageName,
329
+ buildId,
330
+ siteUrl,
331
+ rspressRoot
332
+ });
247
333
  return {
248
334
  vfs,
249
335
  vfsPayloads,
@@ -262,7 +348,10 @@ const makeConfigService = Effect.gen(function* () {
262
348
  ...siteUrl != null ? { siteUrl } : {},
263
349
  ...resolvedOgImage != null ? { ogImage: resolvedOgImage } : {},
264
350
  ...docsRoot != null ? { docsRoot } : {},
265
- ...resolvedTheme != null ? { theme: resolvedTheme } : {}
351
+ ...resolvedTheme != null ? { theme: resolvedTheme } : {},
352
+ bundle,
353
+ siteName,
354
+ ...bundleOgImage != null ? { bundleOgImage } : {}
266
355
  }
267
356
  };
268
357
  }
@@ -325,6 +414,14 @@ const makeConfigService = Effect.gen(function* () {
325
414
  const resolvedTheme = normalizeThemeConfig(api.theme);
326
415
  const outputDir = versionDp.outputDir;
327
416
  const fullRoute = versionDp.routeBase;
417
+ const { bundle, siteName, bundleOgImage } = yield* resolveApiBundle({
418
+ model: versionConfig.model,
419
+ packageName: api.packageName,
420
+ buildId,
421
+ siteUrl,
422
+ rspressRoot,
423
+ assetSubdir: version
424
+ });
328
425
  return {
329
426
  vfs,
330
427
  vfsPayloads,
@@ -343,7 +440,10 @@ const makeConfigService = Effect.gen(function* () {
343
440
  ...siteUrl != null ? { siteUrl } : {},
344
441
  ...resolvedOgImage != null ? { ogImage: resolvedOgImage } : {},
345
442
  ...docsRoot != null ? { docsRoot } : {},
346
- ...resolvedTheme != null ? { theme: resolvedTheme } : {}
443
+ ...resolvedTheme != null ? { theme: resolvedTheme } : {},
444
+ bundle,
445
+ siteName,
446
+ ...bundleOgImage != null ? { bundleOgImage } : {}
347
447
  }
348
448
  };
349
449
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rspress-plugin-api-extractor",
3
- "version": "0.15.2",
3
+ "version": "0.16.0",
4
4
  "private": false,
5
5
  "description": "RSPress plugin for generating API documentation from TypeScript API Extractor models",
6
6
  "keywords": [
@@ -52,11 +52,12 @@
52
52
  "@effected/yaml": "^0.12.0",
53
53
  "@microsoft/api-extractor-model": "^7.33.11",
54
54
  "@shikijs/twoslash": "^4.4.3",
55
- "@tsdoctor/bundle": "0.2.4",
55
+ "@tsdoctor/bundle": "0.3.0",
56
+ "@tsdoctor/manifest": "0.1.0",
56
57
  "@tsdoctor/model": "0.6.1",
57
- "@tsdoctor/pages": "0.1.1",
58
+ "@tsdoctor/pages": "0.1.2",
58
59
  "@tsdoctor/registry": "0.3.2",
59
- "@tsdoctor/seo": "0.1.2",
60
+ "@tsdoctor/seo": "0.2.0",
60
61
  "@tsdoctor/snapshot": "0.2.4",
61
62
  "@tsdoctor/vfs": "0.2.1",
62
63
  "@typescript/vfs": "^1.6.4",
@@ -1,7 +1,7 @@
1
1
  import { PluginEvent } from "../observability/events.js";
2
2
  import { emit } from "../observability/EventBus.js";
3
3
  import { Context, Data, Effect, FileSystem, Layer, Option, Path } from "effect";
4
- import { imageMimeType, ogAltText, resolveUrl } from "@tsdoctor/seo";
4
+ import { imageMimeType, resolveUrl } from "@tsdoctor/seo";
5
5
  import { imageSize } from "image-size";
6
6
 
7
7
  //#region src/services/OgService.ts
@@ -127,7 +127,7 @@ const make = () => Effect.gen(function* () {
127
127
  type: facts?.type,
128
128
  width: facts?.width,
129
129
  height: facts?.height,
130
- alt: ogAltText(request.packageName, request.apiName)
130
+ alt: request.fallbackAlt
131
131
  });
132
132
  });
133
133
  const resolveFromMetadata = (metadata, request) => Effect.gen(function* () {
@@ -162,7 +162,7 @@ const make = () => Effect.gen(function* () {
162
162
  type,
163
163
  width,
164
164
  height,
165
- alt: alt ?? ogAltText(request.packageName, request.apiName)
165
+ alt: alt ?? request.fallbackAlt
166
166
  });
167
167
  });
168
168
  return { resolveImage: (request) => {