rspress-plugin-api-extractor 0.11.0 → 0.13.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.
Files changed (49) hide show
  1. package/BuildEnv.js +3 -3
  2. package/README.md +1 -0
  3. package/build-program.js +14 -5
  4. package/build-stages.js +88 -49
  5. package/config-helpers.js +7 -7
  6. package/errors.js +1 -5
  7. package/index.d.ts +1 -86
  8. package/layers/AppLayer.js +67 -0
  9. package/layers/api-results.js +83 -0
  10. package/layers/build-metrics.js +1 -1
  11. package/layers/{ConfigServiceLive.js → config-resolution.js} +115 -175
  12. package/layers/external-types.js +74 -0
  13. package/layers/{ObservabilityLive.js → observability.js} +3 -3
  14. package/layers/type-environment.js +109 -0
  15. package/layers/xdg.js +1 -1
  16. package/markdown/helpers.js +8 -33
  17. package/markdown/shiki-utils.js +1 -1
  18. package/model-loader.js +3 -3
  19. package/observability/EventBus.js +2 -2
  20. package/observability/heartbeat.js +1 -1
  21. package/observability/sinks/metrics-sink.js +1 -1
  22. package/observability/sinks/trace-sink.js +4 -4
  23. package/package.json +5 -4
  24. package/plugin.js +30 -33
  25. package/prettier-formatter.js +1 -1
  26. package/remark-api-codeblocks.js +1 -1
  27. package/remark-with-api.js +1 -1
  28. package/schemas/config.js +1 -3
  29. package/services/ConfigService.js +37 -2
  30. package/services/HighlighterService.js +48 -3
  31. package/services/OgService.js +154 -2
  32. package/services/PluginConfig.js +1 -1
  33. package/services/TwoslashCacheService.js +128 -2
  34. package/services/TwoslashEnvironments.js +30 -2
  35. package/services/TypeRegistryService.js +178 -2
  36. package/shiki-transformer.js +1 -1
  37. package/sync-node-fs.js +6 -6
  38. package/tsconfig-parser.js +77 -95
  39. package/twoslash-access.js +1 -1
  40. package/twoslash-transformer.js +1 -1
  41. package/layers/HighlighterServiceLive.js +0 -52
  42. package/layers/OgServiceLive.js +0 -134
  43. package/layers/TwoslashCacheServiceLive.js +0 -108
  44. package/layers/TwoslashEnvironmentsLive.js +0 -33
  45. package/layers/TypeRegistryServiceLive.js +0 -162
  46. package/markdown/index.js +0 -11
  47. package/og-resolver.js +0 -64
  48. package/schemas/index.js +0 -6
  49. package/schemas/opengraph.js +0 -56
package/BuildEnv.js CHANGED
@@ -7,12 +7,12 @@ import { Context } from "effect";
7
7
  *
8
8
  * @remarks
9
9
  * Each of these used to travel by hand. `thresholds` made a four-hop journey —
10
- * `plugin.ts` → `ConfigServiceLive`'s fourth constructor argument →
10
+ * `plugin.ts` → `ConfigService.layer`'s fourth constructor argument →
11
11
  * a `ResolvedBuildContext` field → destructured in `build-program.ts` → every
12
12
  * `withPhase` call — while `obs.thresholds` already held the same value one
13
13
  * scope away. `buildId` was worse: 24 event emit sites wrote
14
14
  * `ctx: { buildId: "" }` because the value was not reachable from where they
15
- * stood, and in `TypeRegistryServiceLive` it genuinely was not — that layer is
15
+ * stood, and in `TypeRegistryService.layer` it genuinely was not — that layer is
16
16
  * module-level and has no build to name. A Reference is the fix precisely
17
17
  * because it reaches module-level code that no parameter can.
18
18
  *
@@ -46,7 +46,7 @@ const Thresholds = Context.Reference("rspress-plugin-api-extractor/Thresholds",
46
46
  * How many pages the build pipeline generates concurrently.
47
47
  *
48
48
  * @remarks
49
- * Defaults to the CPU count, which is what `ConfigServiceLive` computed
49
+ * Defaults to the CPU count, which is what `ConfigService.layer` computed
50
50
  * inline. Kept a Reference rather than a constant so a consumer with a
51
51
  * constrained CI runner can lower it without a code change.
52
52
  */
package/README.md CHANGED
@@ -54,6 +54,7 @@ The plugin reads your `.api.json` model and writes one MDX page per public API i
54
54
  - Drives single-package sites, multi-package portals, RSPress multiVersion and i18n from one plugin.
55
55
  - Handles multi-entry-point packages: it deduplicates re-exports and notes which entry points each item is available from.
56
56
  - Writes per-package `llms*.txt` files and in-page actions for pointing an assistant at one package's docs.
57
+ - Gives every generated page complete `<head>` metadata: a canonical link, Open Graph and Twitter card tags, and a schema.org JSON-LD graph describing the package, the page and the documented symbol.
57
58
 
58
59
  ## Documentation
59
60
 
package/build-program.js CHANGED
@@ -1,17 +1,17 @@
1
1
  import { BuildId, PageConcurrency, SuppressExampleErrors } from "./BuildEnv.js";
2
2
  import { setProseLinker } from "./markdown/prose-linker.js";
3
- import "./markdown/index.js";
4
3
  import { buildPipelineForApi, cleanupAndCommit, prepareWorkItems, writeMetadata } from "./build-stages.js";
5
4
  import { HideCutLinesTransformer, MemberFormatTransformer } from "./hide-cut-transformer.js";
6
5
  import { withPhase } from "./observability/spans.js";
7
6
  import { HighlighterService } from "./services/HighlighterService.js";
7
+ import { addTypeRoutes } from "./twoslash-transformer.js";
8
8
  import { TwoslashEnvironments } from "./services/TwoslashEnvironments.js";
9
9
  import { ShikiCrossLinker } from "./shiki-transformer.js";
10
- import { addTypeRoutes } from "./twoslash-transformer.js";
11
10
  import { VfsRegistry } from "./vfs-registry.js";
12
11
  import path from "node:path";
13
- import { SnapshotService } from "@tsdoctor/snapshot";
14
12
  import { Effect, FileSystem } from "effect";
13
+ import { attributionFacts, packageContext } from "@tsdoctor/seo";
14
+ import { SnapshotService } from "@tsdoctor/snapshot";
15
15
 
16
16
  //#region src/build-program.ts
17
17
  /**
@@ -36,8 +36,16 @@ 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 } = apiConfig;
39
+ const { apiPackage, packageName, apiName, outputDir, baseRoute, categories, source, packageJson, llmsPlugin, siteUrl, ogImage, manifest } = apiConfig;
40
40
  const phaseCtx = { packageName };
41
+ const structuredDataPkg = manifest != null && siteUrl != null ? packageContext({
42
+ siteUrl,
43
+ baseRoute,
44
+ packageName,
45
+ ...manifest.version != null ? { version: manifest.version.toString() } : {},
46
+ ...manifest.description != null ? { description: manifest.description } : {},
47
+ attribution: attributionFacts(manifest)
48
+ }) : void 0;
41
49
  const resolvedOutputDir = path.resolve(process.cwd(), outputDir);
42
50
  const buildTime = (/* @__PURE__ */ new Date()).toISOString();
43
51
  const allSnapshots = yield* snapshotSvc.getAllForDirectory(resolvedOutputDir).pipe(Effect.orDie);
@@ -82,7 +90,8 @@ function generateApiDocs(apiConfig, fileContextMap) {
82
90
  ...llmsPlugin != null ? { llmsPlugin } : {},
83
91
  ...apiConfig.docsRoot != null ? { docsRoot: apiConfig.docsRoot } : {},
84
92
  ...siteUrl != null ? { siteUrl } : {},
85
- ...ogImage != null ? { ogImage } : {}
93
+ ...ogImage != null ? { ogImage } : {},
94
+ ...structuredDataPkg != null ? { structuredDataPkg } : {}
86
95
  }));
87
96
  const changedCount = fileResults.filter((r) => r.status !== "unchanged").length;
88
97
  yield* Effect.logDebug(`Generated ${changedCount} pages`);
package/build-stages.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import { parseFrontmatter, stringifyFrontmatter } from "./frontmatter.js";
2
+ import { BuildMetrics } from "./layers/build-metrics.js";
2
3
  import { PluginEvent } from "./observability/events.js";
3
4
  import { emit } from "./observability/EventBus.js";
4
- import { BuildMetrics } from "./layers/build-metrics.js";
5
- import "./layers/ObservabilityLive.js";
6
5
  import { emitSync, syncBuildId } from "./observability/sync-emitter.js";
7
6
  import { generateFrontmatter } from "./markdown/helpers.js";
8
7
  import { ClassPageGenerator } from "./markdown/page-generators/class-page.js";
@@ -13,12 +12,11 @@ import { InterfacePageGenerator } from "./markdown/page-generators/interface-pag
13
12
  import { NamespacePageGenerator } from "./markdown/page-generators/namespace-page.js";
14
13
  import { TypeAliasPageGenerator } from "./markdown/page-generators/type-alias-page.js";
15
14
  import { VariablePageGenerator } from "./markdown/page-generators/variable-page.js";
16
- import "./markdown/index.js";
17
- import { createPageMetadata } from "./og-resolver.js";
18
15
  import { OgService } from "./services/OgService.js";
19
16
  import path from "node:path";
20
- import { SnapshotService, hashContent, hashFrontmatter } from "@tsdoctor/snapshot";
21
17
  import { Effect, FileSystem, Metric, Option, Stream } from "effect";
18
+ import { deriveScriptBody, headTags } from "@tsdoctor/seo";
19
+ import { SnapshotService, hashContent, hashFrontmatter } from "@tsdoctor/snapshot";
22
20
  import { ApiItemKind } from "@microsoft/api-extractor-model";
23
21
  import { ApiItems, EntryPoints, Routes, SyntheticBases } from "@tsdoctor/model";
24
22
 
@@ -330,7 +328,79 @@ function generateSinglePage(workItem, ctx) {
330
328
  const frontmatterData = parsed.data;
331
329
  const relativePathWithExt = `${page.routePath.replace(baseRoute, "").replace(/^\//, "")}.mdx`;
332
330
  const contentHash = hashContent(bodyContent);
333
- const frontmatterHash = hashFrontmatter(frontmatterData);
331
+ const description = frontmatterData.description;
332
+ const seoEnabled = ctx.siteUrl != null && packageName !== "";
333
+ let ogImageMetadata;
334
+ let structuredData;
335
+ if (seoEnabled) {
336
+ const siteUrl = ctx.siteUrl;
337
+ const ogSvc = yield* OgService;
338
+ const ogImageResult = yield* Effect.result(ogSvc.resolveImage({
339
+ config: ctx.ogImage,
340
+ siteUrl,
341
+ docsRoot: ctx.docsRoot,
342
+ packageName,
343
+ ...apiName != null ? { apiName } : {}
344
+ }));
345
+ if (ogImageResult._tag === "Failure") {
346
+ const failure = ogImageResult.failure;
347
+ yield* emit(PluginEvent.ConfigValidationWarning({
348
+ ctx: {
349
+ buildId,
350
+ packageName
351
+ },
352
+ field: failure.field,
353
+ value: failure.value,
354
+ reason: failure.message,
355
+ level: "warn"
356
+ }));
357
+ } else if (Option.isSome(ogImageResult.success)) ogImageMetadata = ogImageResult.success.value;
358
+ if (ctx.structuredDataPkg != null) {
359
+ const graphResult = deriveScriptBody(ctx.structuredDataPkg, {
360
+ pageRoute: page.routePath,
361
+ symbolName: item.displayName,
362
+ description,
363
+ section: categoryConfig.displayName,
364
+ publishedTime: buildTime,
365
+ modifiedTime: buildTime
366
+ });
367
+ if (graphResult._tag === "Failure") yield* emit(PluginEvent.ConfigValidationWarning({
368
+ ctx: {
369
+ buildId,
370
+ packageName,
371
+ route: page.routePath
372
+ },
373
+ field: "structuredData",
374
+ value: ctx.structuredDataPkg.id,
375
+ reason: `schema.org document assembly failed: ${graphResult.failure._tag}`,
376
+ level: "warn"
377
+ }));
378
+ else structuredData = graphResult.success;
379
+ }
380
+ }
381
+ /**
382
+ * The final frontmatter for a given pair of timestamps.
383
+ *
384
+ * @remarks
385
+ * Called twice: once with the build time to compute the hash, and once
386
+ * with the resolved timestamps to write. That is sound only because
387
+ * `hashFrontmatter` strips every timestamp it can reach — the meta-pair
388
+ * form and the JSON-LD `datePublished`/`dateModified` keys alike — so the
389
+ * two calls hash identically. Without that stripping the hash would
390
+ * depend on the timestamps the hash itself decides.
391
+ */
392
+ const finalFrontmatter = (published, modified) => seoEnabled ? generateFrontmatter(item.displayName, description, categoryConfig.singularName, apiName, headTags({
393
+ siteUrl: ctx.siteUrl,
394
+ pageRoute: page.routePath,
395
+ description,
396
+ publishedTime: published,
397
+ modifiedTime: modified,
398
+ section: categoryConfig.displayName,
399
+ packageName,
400
+ ...ogImageMetadata != null ? { ogImage: ogImageMetadata } : {},
401
+ ...structuredData != null ? { structuredData } : {}
402
+ })) : stringifyFrontmatter("", frontmatterData);
403
+ const frontmatterHash = hashFrontmatter(parseFrontmatter(finalFrontmatter(buildTime, buildTime)).data);
334
404
  let publishedTime;
335
405
  let modifiedTime;
336
406
  let isUnchanged = false;
@@ -370,7 +440,7 @@ function generateSinglePage(workItem, ctx) {
370
440
  }
371
441
  return {
372
442
  workItem,
373
- content: page.content,
443
+ content: seoEnabled ? finalFrontmatter(publishedTime, modifiedTime) + bodyContent : page.content,
374
444
  bodyContent,
375
445
  frontmatter: frontmatterData,
376
446
  contentHash,
@@ -389,9 +459,9 @@ function generateSinglePage(workItem, ctx) {
389
459
  function writeSingleFile(result, ctx) {
390
460
  return Effect.gen(function* () {
391
461
  const fileSystem = yield* FileSystem.FileSystem;
392
- const { buildId, resolvedOutputDir, buildTime, siteUrl, docsRoot, ogImage, packageName, apiName } = ctx;
393
- const { workItem, bodyContent, frontmatter, contentHash, frontmatterHash, publishedTime, modifiedTime, isUnchanged, routePath, relativePathWithExt } = result;
394
- const { item, categoryKey, categoryConfig, namespaceMember } = workItem;
462
+ const { buildId, resolvedOutputDir, buildTime, packageName } = ctx;
463
+ const { workItem, contentHash, frontmatterHash, publishedTime, modifiedTime, isUnchanged, routePath, relativePathWithExt } = result;
464
+ const { item, categoryKey, namespaceMember } = workItem;
395
465
  const absolutePath = path.join(resolvedOutputDir, relativePathWithExt);
396
466
  const label = namespaceMember ? namespaceMember.qualifiedName : item.displayName;
397
467
  const snapshot = {
@@ -426,43 +496,7 @@ function writeSingleFile(result, ctx) {
426
496
  routePath
427
497
  };
428
498
  }
429
- let finalContent = stringifyFrontmatter(bodyContent, frontmatter);
430
- if (siteUrl && packageName) {
431
- const ogSvc = yield* OgService;
432
- const ogImageResult = yield* Effect.result(ogSvc.resolveImage({
433
- config: ogImage,
434
- siteUrl,
435
- docsRoot,
436
- packageName,
437
- ...apiName != null ? { apiName } : {}
438
- }));
439
- if (ogImageResult._tag === "Failure") {
440
- const failure = ogImageResult.failure;
441
- yield* emit(PluginEvent.ConfigValidationWarning({
442
- ctx: {
443
- buildId,
444
- packageName
445
- },
446
- field: failure.field,
447
- value: failure.value,
448
- reason: failure.message,
449
- level: "warn"
450
- }));
451
- }
452
- const ogImageMetadata = ogImageResult._tag === "Success" && Option.isSome(ogImageResult.success) ? ogImageResult.success.value : void 0;
453
- const ogMetadataOptions = {
454
- siteUrl,
455
- pageRoute: routePath,
456
- description: frontmatter.description,
457
- publishedTime,
458
- modifiedTime,
459
- section: categoryConfig.displayName,
460
- packageName
461
- };
462
- if (ogImageMetadata) ogMetadataOptions.ogImage = ogImageMetadata;
463
- const ogMetadata = createPageMetadata(ogMetadataOptions);
464
- finalContent = generateFrontmatter(item.displayName, frontmatter.description, categoryConfig.singularName, apiName, ogMetadata) + bodyContent;
465
- }
499
+ const finalContent = result.content;
466
500
  const fileExisted = yield* fileSystem.exists(absolutePath).pipe(Effect.orElseSucceed(() => false));
467
501
  const dirPath = path.dirname(absolutePath);
468
502
  yield* fileSystem.makeDirectory(dirPath, { recursive: true }).pipe(Effect.orDie);
@@ -795,7 +829,11 @@ function buildPipelineForApi(input) {
795
829
  buildTime: input.buildTime,
796
830
  resolvedOutputDir: input.resolvedOutputDir,
797
831
  ...input.suppressExampleErrors != null ? { suppressExampleErrors: input.suppressExampleErrors } : {},
798
- ...input.llmsPlugin != null ? { llmsPlugin: input.llmsPlugin } : {}
832
+ ...input.llmsPlugin != null ? { llmsPlugin: input.llmsPlugin } : {},
833
+ ...input.docsRoot !== void 0 ? { docsRoot: input.docsRoot } : {},
834
+ ...input.siteUrl != null ? { siteUrl: input.siteUrl } : {},
835
+ ...input.ogImage != null ? { ogImage: input.ogImage } : {},
836
+ ...input.structuredDataPkg != null ? { structuredDataPkg: input.structuredDataPkg } : {}
799
837
  };
800
838
  const writeCtx = {
801
839
  buildId: input.buildId,
@@ -805,7 +843,8 @@ function buildPipelineForApi(input) {
805
843
  ...input.siteUrl != null ? { siteUrl: input.siteUrl } : {},
806
844
  ...input.ogImage != null ? { ogImage: input.ogImage } : {},
807
845
  ...input.packageName != null ? { packageName: input.packageName } : {},
808
- ...input.apiName != null ? { apiName: input.apiName } : {}
846
+ ...input.apiName != null ? { apiName: input.apiName } : {},
847
+ ...input.structuredDataPkg != null ? { structuredDataPkg: input.structuredDataPkg } : {}
809
848
  };
810
849
  return Stream.fromIterable(input.workItems).pipe(Stream.mapEffect((workItem) => generateSinglePage(workItem, generateCtx), { concurrency: input.pageConcurrency }), Stream.filter((result) => result !== null), Stream.mapEffect((result) => writeSingleFile(result, writeCtx), { concurrency: input.pageConcurrency }), Stream.runFold(() => [], (acc, result) => [...acc, result]));
811
850
  }
package/config-helpers.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { normalizeBaseRoute } from "./path-derivation.js";
2
2
  import { SyncDiscoveryLayer } from "./sync-node-fs.js";
3
- import fs from "node:fs";
3
+ import fsSync from "node:fs";
4
4
  import path from "node:path";
5
5
  import { Effect, Result } from "effect";
6
6
  import { discoverBundle } from "@tsdoctor/bundle";
@@ -18,14 +18,14 @@ const PREFIX = "[rspress-plugin-api-extractor]";
18
18
  function requirePackageJson(dir) {
19
19
  let stat;
20
20
  try {
21
- stat = fs.statSync(dir);
21
+ stat = fsSync.statSync(dir);
22
22
  } catch {
23
23
  throw new Error(`${PREFIX} api.fromDir: directory not found: ${dir}`);
24
24
  }
25
25
  if (!stat.isDirectory()) throw new Error(`${PREFIX} api.fromDir: not a directory: ${dir}`);
26
26
  let pkg;
27
27
  try {
28
- pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
28
+ pkg = JSON.parse(fsSync.readFileSync(path.join(dir, "package.json"), "utf8"));
29
29
  } catch {
30
30
  throw new Error(`${PREFIX} api.fromDir: missing or unreadable package.json in ${dir}`);
31
31
  }
@@ -103,9 +103,9 @@ function fromDir(dir, overrides = {}) {
103
103
  };
104
104
  }
105
105
  function isModelFolder(dir) {
106
- if (!fs.existsSync(path.join(dir, "package.json"))) return false;
106
+ if (!fsSync.existsSync(path.join(dir, "package.json"))) return false;
107
107
  try {
108
- return fs.readdirSync(dir).some((f) => f.endsWith(".api.json"));
108
+ return fsSync.readdirSync(dir).some((f) => f.endsWith(".api.json"));
109
109
  } catch {
110
110
  return false;
111
111
  }
@@ -124,12 +124,12 @@ function fromParentDir(parentDir, options = {}) {
124
124
  const absParent = path.resolve(cwd ?? process.cwd(), parentDir);
125
125
  let stat;
126
126
  try {
127
- stat = fs.statSync(absParent);
127
+ stat = fsSync.statSync(absParent);
128
128
  } catch {
129
129
  throw new Error(`${PREFIX} apis.fromDir: directory not found: ${absParent}`);
130
130
  }
131
131
  if (!stat.isDirectory()) throw new Error(`${PREFIX} apis.fromDir: not a directory: ${absParent}`);
132
- const subdirs = fs.readdirSync(absParent, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort();
132
+ const subdirs = fsSync.readdirSync(absParent, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort();
133
133
  const configs = [];
134
134
  for (const name of subdirs) {
135
135
  const subdir = path.join(absParent, name);
package/errors.js CHANGED
@@ -7,16 +7,12 @@ var ConfigValidationError = class extends ConfigValidationErrorBase {
7
7
  return `Config validation failed for '${this.field}': ${this.reason}`;
8
8
  }
9
9
  };
10
- const ApiModelLoadErrorBase = Data.TaggedError("ApiModelLoadError");
11
10
  const TypeRegistryErrorBase = Data.TaggedError("TypeRegistryError");
12
11
  var TypeRegistryError = class extends TypeRegistryErrorBase {
13
12
  get message() {
14
13
  return `Type registry error for '${this.packageName}@${this.version}': ${this.reason}`;
15
14
  }
16
15
  };
17
- const PageGenerationErrorBase = Data.TaggedError("PageGenerationError");
18
- const TwoslashProcessingErrorBase = Data.TaggedError("TwoslashProcessingError");
19
- const PrettierFormatErrorBase = Data.TaggedError("PrettierFormatError");
20
16
 
21
17
  //#endregion
22
- export { ConfigValidationError, ConfigValidationErrorBase, TypeRegistryError, TypeRegistryErrorBase };
18
+ export { ConfigValidationError, TypeRegistryError };
package/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { OpenGraphImageConfig, OpenGraphImageMetadata, OpenGraphMetadata } from "@tsdoctor/seo";
1
2
  import { ApiItemKind, ApiModel } from "@microsoft/api-extractor-model";
2
3
  import { Schema } from "effect";
3
4
  import "typescript";
@@ -669,8 +670,6 @@ declare const PluginOptions: Schema.Struct<{
669
670
  */
670
671
  readonly compilerOptions: Schema.optional<Schema.Unknown>;
671
672
  }>>>>>;
672
- /** Canonical site URL used for Open Graph absolute URLs. */
673
- readonly siteUrl: Schema.optional<Schema.String>;
674
673
  /** Global Open Graph image configuration (overridden per-API). */
675
674
  readonly ogImage: Schema.optional<Schema.Union<readonly [Schema.String, Schema.Struct<{
676
675
  readonly url: Schema.String;
@@ -746,90 +745,6 @@ declare const PluginOptions: Schema.Struct<{
746
745
  /** @public */
747
746
  type PluginOptions = typeof PluginOptions.Encoded;
748
747
  //#endregion
749
- //#region src/schemas/opengraph.d.ts
750
- /**
751
- * Structured Open Graph image metadata (alternative to a plain URL string).
752
- *
753
- * @public
754
- */
755
- declare const OpenGraphImageMetadata: Schema.Struct<{
756
- /** Absolute URL of the image. */
757
- readonly url: Schema.String;
758
- /** HTTPS URL of the image (for secure contexts). */
759
- readonly secureUrl: Schema.optional<Schema.String>;
760
- /** MIME type of the image (e.g. `"image/png"`). */
761
- readonly type: Schema.optional<Schema.String>;
762
- /** Image width in pixels. */
763
- readonly width: Schema.optional<Schema.Number>;
764
- /** Image height in pixels. */
765
- readonly height: Schema.optional<Schema.Number>;
766
- /** Alt text for the image. */
767
- readonly alt: Schema.optional<Schema.String>;
768
- }>;
769
- /** @public */
770
- type OpenGraphImageMetadata = typeof OpenGraphImageMetadata.Type;
771
- /**
772
- * Open Graph image: either a plain URL string or structured `OpenGraphImageMetadata`.
773
- *
774
- * @public
775
- */
776
- declare const OpenGraphImageConfig: Schema.Union<readonly [Schema.String, Schema.Struct<{
777
- /** Absolute URL of the image. */
778
- readonly url: Schema.String;
779
- /** HTTPS URL of the image (for secure contexts). */
780
- readonly secureUrl: Schema.optional<Schema.String>;
781
- /** MIME type of the image (e.g. `"image/png"`). */
782
- readonly type: Schema.optional<Schema.String>;
783
- /** Image width in pixels. */
784
- readonly width: Schema.optional<Schema.Number>;
785
- /** Image height in pixels. */
786
- readonly height: Schema.optional<Schema.Number>;
787
- /** Alt text for the image. */
788
- readonly alt: Schema.optional<Schema.String>;
789
- }>]>;
790
- /** @public */
791
- type OpenGraphImageConfig = typeof OpenGraphImageConfig.Type;
792
- /**
793
- * Resolved Open Graph metadata emitted into each generated page's frontmatter.
794
- *
795
- * @public
796
- */
797
- declare const OpenGraphMetadata: Schema.Struct<{
798
- /** Canonical site base URL. */
799
- readonly siteUrl: Schema.String;
800
- /** Page route path (e.g. `/api/classes/myclass`). */
801
- readonly pageRoute: Schema.String;
802
- /** Page description for the `og:description` tag. */
803
- readonly description: Schema.String;
804
- /** ISO 8601 date string for `article:published_time`. */
805
- readonly publishedTime: Schema.String;
806
- /** ISO 8601 date string for `article:modified_time`. */
807
- readonly modifiedTime: Schema.String;
808
- /** Article section label (e.g. `"API"`). */
809
- readonly section: Schema.String;
810
- /** Article tag keywords. */
811
- readonly tags: Schema.mutable<Schema.$Array<Schema.String>>;
812
- /** Optional structured image metadata. */
813
- readonly ogImage: Schema.optional<Schema.Struct<{
814
- /** Absolute URL of the image. */
815
- readonly url: Schema.String;
816
- /** HTTPS URL of the image (for secure contexts). */
817
- readonly secureUrl: Schema.optional<Schema.String>;
818
- /** MIME type of the image (e.g. `"image/png"`). */
819
- readonly type: Schema.optional<Schema.String>;
820
- /** Image width in pixels. */
821
- readonly width: Schema.optional<Schema.Number>;
822
- /** Image height in pixels. */
823
- readonly height: Schema.optional<Schema.Number>;
824
- /** Alt text for the image. */
825
- readonly alt: Schema.optional<Schema.String>;
826
- }>>;
827
- /** Open Graph object type (e.g. `"article"`). */
828
- readonly ogType: Schema.String;
829
- }>;
830
- /** @public */
831
- type OpenGraphMetadata = typeof OpenGraphMetadata.Type;
832
- //#endregion
833
748
  //#region src/config-helpers.d.ts
834
749
  /**
835
750
  * Metadata discovered from a single rslib-builder localPaths package folder.
@@ -0,0 +1,67 @@
1
+ import { BuildId, PageConcurrency, SuppressExampleErrors, Thresholds } from "../BuildEnv.js";
2
+ import { OgService } from "../services/OgService.js";
3
+ import { collectShikiThemes } from "../markdown/shiki-utils.js";
4
+ import { HighlighterService } from "../services/HighlighterService.js";
5
+ import { TwoslashEnvironments } from "../services/TwoslashEnvironments.js";
6
+ import { PluginConfig } from "../services/PluginConfig.js";
7
+ import { PlatformLive } from "./xdg.js";
8
+ import { TypeRegistryService } from "../services/TypeRegistryService.js";
9
+ import { TwoslashCacheService } from "../services/TwoslashCacheService.js";
10
+ import { ConfigService } from "../services/ConfigService.js";
11
+ import { makeSummaryLoggerLayer } from "./observability.js";
12
+ import { Layer } from "effect";
13
+ import { SnapshotService } from "@tsdoctor/snapshot";
14
+ import { NodeFileSystem } from "@effect/platform-node";
15
+
16
+ //#region src/layers/AppLayer.ts
17
+ /**
18
+ * The build's layer stack, tiered.
19
+ *
20
+ * @remarks
21
+ * `plugin.ts` used to merge eleven layers side by side in one `Layer.mergeAll`
22
+ * — `NodeFileSystem.layer` (platform) next to `TypeRegistryService.layer`
23
+ * (domain) next to `makeSummaryLoggerLayer` (observability) — with one of them
24
+ * carrying a local `Layer.provide` because a flat merge could not feed it its
25
+ * dependencies. Reading that told you what the build contains but not what
26
+ * depends on what.
27
+ *
28
+ * The tiers below are ordered by what they may reach: platform knows nothing
29
+ * about this plugin, core services know the platform, and build-scoped services
30
+ * know both.
31
+ *
32
+ * @packageDocumentation
33
+ */
34
+ /**
35
+ * Build both stacks for one build.
36
+ *
37
+ * @remarks
38
+ * **A layer factory: call it once and bind the result to a `const`.** Layers
39
+ * memoize by reference, so a second call mints a second stack — a second Shiki
40
+ * highlighter, a second snapshot database, a second metric registry.
41
+ */
42
+ function makeAppLayers(input) {
43
+ /**
44
+ * Per-build configuration, provided to BOTH stacks. Sharing these values is
45
+ * what lets a sync island and an Effect program agree on the build id and
46
+ * the slow-block threshold without either being handed them.
47
+ */
48
+ const BuildEnvLayer = Layer.mergeAll(Layer.succeed(BuildId, input.buildId), Layer.succeed(Thresholds, input.obs.thresholds), Layer.succeed(PageConcurrency, input.pageConcurrency), Layer.succeed(SuppressExampleErrors, input.options.errors?.example !== "show"));
49
+ /**
50
+ * Sinks, metrics and the logger gate. Synchronously buildable, which is what
51
+ * lets the emitter stack below reuse it wholesale.
52
+ */
53
+ const ObservabilityLayer = Layer.mergeAll(input.eventBus, input.metrics.layer, makeSummaryLoggerLayer(input.obs.logLevel));
54
+ /** Services that own a resource and need only the platform to build. */
55
+ const CoreLayer = Layer.mergeAll(TypeRegistryService.layer, TwoslashCacheService.layer, SnapshotService.layer(input.dbPath), Layer.provide(OgService.layer, PlatformLive));
56
+ /** Bound to a `const`: this is a factory, and a second call acquires a second highlighter. */
57
+ const HighlighterLive = HighlighterService.layer(collectShikiThemes(input.options.api ? [input.options.api] : input.options.apis ?? []));
58
+ /** Services scoped to this build's configuration. */
59
+ const BuildLayer = Layer.mergeAll(Layer.succeed(PluginConfig, input.options), HighlighterLive, TwoslashEnvironments.layer, BuildEnvLayer);
60
+ return {
61
+ app: Layer.provideMerge(ConfigService.layer, Layer.mergeAll(BuildLayer, CoreLayer, ObservabilityLayer, NodeFileSystem.layer)),
62
+ emitter: Layer.mergeAll(ObservabilityLayer, BuildEnvLayer)
63
+ };
64
+ }
65
+
66
+ //#endregion
67
+ export { makeAppLayers };
@@ -0,0 +1,83 @@
1
+ import { PluginEvent } from "../observability/events.js";
2
+ import { emit } from "../observability/EventBus.js";
3
+ import { Effect } from "effect";
4
+
5
+ //#region src/layers/api-results.ts
6
+ /**
7
+ * Accumulating one API's resolution result into the build-wide totals.
8
+ *
9
+ * @remarks
10
+ * `ConfigService.layer` resolves APIs down three paths — versioned, single
11
+ * non-versioned, and multi-API — and each one ended with a near-identical
12
+ * ~35-line block that merged the same three accumulators and then emitted the
13
+ * same two events per VFS entry. Three copies of one algorithm is three places
14
+ * for it to drift, and the third copy had ALREADY drifted: it emits its events
15
+ * inside the per-API effect and merges afterwards, rather than doing both in
16
+ * one pass.
17
+ *
18
+ * Splitting the block in two is what makes all three paths expressible. The
19
+ * merge is pure and the emission is effectful, and the multi-API path needs
20
+ * them at different moments; a single combined helper would have fitted two
21
+ * paths and forced the third to keep its own copy.
22
+ *
23
+ * @packageDocumentation
24
+ */
25
+ /**
26
+ * Merge one result into the build-wide accumulators.
27
+ *
28
+ * @remarks
29
+ * Mutates `acc` rather than returning a new one. The accumulators are three
30
+ * `const` collections in a long generator that appends to them from several
31
+ * branches, and threading a replacement through every branch would be a larger
32
+ * change than this task is buying.
33
+ *
34
+ * **The VFS is a single flat namespace shared by every documented API.** A
35
+ * later entry silently overwrites an earlier one at the same path, which is
36
+ * load-bearing rather than accidental: cross-package type references resolve
37
+ * only because every package's declarations live in one environment (see
38
+ * `type-loading-vfs.md`).
39
+ */
40
+ function mergeApiResult(acc, result) {
41
+ for (const [filepath, content] of result.vfs.entries()) acc.combinedVfs.set(filepath, content);
42
+ if (result.externalPackages.length > 0) acc.allExternalPackages.push(...result.externalPackages);
43
+ if (result.config) acc.apiConfigs.push(result.config);
44
+ }
45
+ /**
46
+ * Emit the per-entry VFS events for one API's payloads.
47
+ *
48
+ * @remarks
49
+ * `ImportsPrepended` fires only when imports were actually prepended, so an
50
+ * entry that needed none produces one event rather than two.
51
+ *
52
+ * `wantTrace` gates the two heavy fields — the full declaration text and the
53
+ * resolved import refs. Both are only ever read by the JSONL trace sink, and
54
+ * carrying them unconditionally would put every generated declaration file
55
+ * through the event bus on every build.
56
+ */
57
+ function emitVfsPayloadEvents(packageName, payloads, wantTrace) {
58
+ return Effect.gen(function* () {
59
+ for (const payload of payloads) {
60
+ const ctx = {
61
+ packageName,
62
+ ...payload.entryPoint ? { entryPoint: payload.entryPoint } : {}
63
+ };
64
+ yield* emit(PluginEvent.VfsGenerated({
65
+ ctx,
66
+ level: "debug",
67
+ file: payload.file,
68
+ declCount: payload.declCount,
69
+ contentHash: payload.contentHash,
70
+ ...wantTrace && payload.content ? { content: payload.content } : {}
71
+ }));
72
+ if (payload.hasImports) yield* emit(PluginEvent.ImportsPrepended({
73
+ ctx,
74
+ level: "debug",
75
+ file: payload.file,
76
+ imports: wantTrace ? payload.importRefs : []
77
+ }));
78
+ }
79
+ });
80
+ }
81
+
82
+ //#endregion
83
+ export { emitVfsPayloadEvents, mergeApiResult };
@@ -18,7 +18,7 @@ function makeMetricStore() {
18
18
  * build still gets its own counters — see {@link MetricRegistryLive}.
19
19
  *
20
20
  * Extracted into its own module so that `metrics-sink.ts` can import it
21
- * without creating a circular dependency through `ObservabilityLive.ts`
21
+ * without creating a circular dependency through `observability.ts`
22
22
  * (which itself imports `metrics-sink.ts`).
23
23
  */
24
24
  const BuildMetrics = {