@savvy-web/tsdown-plugins 2.5.10 → 2.6.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/errors.js CHANGED
@@ -39,6 +39,18 @@ var ConfigValidationError = class extends Data.TaggedError("ConfigValidationErro
39
39
  return `Config validation failed at "${this.path}": ${this.reason}`;
40
40
  }
41
41
  };
42
+ /**
43
+ * Writing the `tsdoctor.json` sidecar failed — the composed manifest did not encode, or the file
44
+ * could not be written (a read-only or full disk). Recorded in `issues.json` as a `meta` error.
45
+ *
46
+ * @public
47
+ */
48
+ var TsdoctorEmitError = class extends Data.TaggedError("TsdoctorEmitError") {
49
+ get message() {
50
+ const reason = this.cause instanceof Error ? this.cause.message : String(this.cause);
51
+ return `Could not emit ${this.path} for ${this.packageName}: ${reason}`;
52
+ }
53
+ };
42
54
 
43
55
  //#endregion
44
- export { ConfigValidationError, MetaGenerationError };
56
+ export { ConfigValidationError, MetaGenerationError, TsdoctorEmitError };
package/index.d.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  import { CatalogAssemblyError, DependencyResolutionError, ManifestDecodeError, UnresolvedDependencyError } from "@effected/npm";
10
10
  import { Plugin } from "rolldown";
11
11
  import { Context, Effect, Layer, Schema } from "effect";
12
+ import { BundleManifest, ManifestSource, OpenGraphImage, RegistryRef } from "@tsdoctor/manifest";
12
13
  //#region src/manifest/transform.d.ts
13
14
  /** @public */
14
15
  type Json = Record<string, unknown>;
@@ -174,7 +175,7 @@ declare const ReportTimings_base: Schema.Class<ReportTimings, Schema.Struct<{
174
175
  /** @public */
175
176
  declare class ReportTimings extends ReportTimings_base {}
176
177
  declare const DiagnosticEntry_base: Schema.Class<DiagnosticEntry, Schema.Struct<{
177
- readonly source: Schema.Literals<readonly ["tsdown", "rolldown", "api-extractor"]>;
178
+ readonly source: Schema.Literals<readonly ["tsdown", "rolldown", "api-extractor", "meta"]>;
178
179
  readonly level: Schema.Literals<readonly ["warn", "error"]>;
179
180
  readonly text: Schema.String;
180
181
  /**
@@ -189,7 +190,8 @@ declare const DiagnosticEntry_base: Schema.Class<DiagnosticEntry, Schema.Struct<
189
190
  readonly column: Schema.optional<Schema.Number>;
190
191
  }>, {}>;
191
192
  /**
192
- * A captured warning or error, from tsdown's logger, rolldown's onLog, or API Extractor.
193
+ * A captured warning or error, from tsdown's logger, rolldown's onLog, API Extractor, or the meta
194
+ * pass's own sidecar work (`tsdoctor.json` sources, Open Graph generation).
193
195
  *
194
196
  * @public
195
197
  */
@@ -783,6 +785,22 @@ declare class ConfigValidationError extends ConfigValidationError_base<{
783
785
  }> {
784
786
  get message(): string;
785
787
  }
788
+ declare const TsdoctorEmitError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
789
+ readonly _tag: "TsdoctorEmitError";
790
+ } & Readonly<A>;
791
+ /**
792
+ * Writing the `tsdoctor.json` sidecar failed — the composed manifest did not encode, or the file
793
+ * could not be written (a read-only or full disk). Recorded in `issues.json` as a `meta` error.
794
+ *
795
+ * @public
796
+ */
797
+ declare class TsdoctorEmitError extends TsdoctorEmitError_base<{
798
+ readonly packageName: string;
799
+ readonly path: string;
800
+ readonly cause: unknown;
801
+ }> {
802
+ get message(): string;
803
+ }
786
804
  //#endregion
787
805
  //#region src/entry/extract.d.ts
788
806
  /** @public */
@@ -1047,6 +1065,50 @@ interface PkgOsCpu {
1047
1065
  */
1048
1066
  declare function normalizeExeOptions(exe: ExeConfig | ReadonlyArray<ExeConfig>, pkg: PkgOsCpu): ReadonlyArray<NormalizedExe>;
1049
1067
  //#endregion
1068
+ //#region src/meta/tsdoctor-config.d.ts
1069
+ /**
1070
+ * What an Open Graph image generator receives: the merged identity of the
1071
+ * package being built, after the config, leaf and project tiers resolved.
1072
+ *
1073
+ * @public
1074
+ */
1075
+ interface OgImageInfo {
1076
+ /** Display name — the merged `name`, falling back to the npm name. */
1077
+ readonly name: string;
1078
+ /** The npm package name. */
1079
+ readonly packageName: string;
1080
+ /** The emitted version (the optimistic next version when enabled). */
1081
+ readonly version: string;
1082
+ readonly tagline?: string | undefined;
1083
+ readonly description?: string | undefined;
1084
+ /** The inherited project tier, when the workspace root declares one. */
1085
+ readonly project?: {
1086
+ readonly name?: string | undefined;
1087
+ readonly tagline?: string | undefined;
1088
+ } | undefined;
1089
+ }
1090
+ /**
1091
+ * The `meta.tsdoctor` block: the CONFIG tier of the emitted `tsdoctor.json`,
1092
+ * ranked over the package's `tsdoctor.json` (leaf) and the workspace root's
1093
+ * (project).
1094
+ *
1095
+ * @public
1096
+ */
1097
+ interface TsdoctorMetaOptions {
1098
+ readonly name?: string | undefined;
1099
+ readonly tagline?: string | undefined;
1100
+ readonly description?: string | undefined;
1101
+ readonly openGraph?: {
1102
+ /** Static images, path (bundle-relative) or url. Listed after a generated image. */
1103
+ readonly images?: ReadonlyArray<OpenGraphImage> | undefined;
1104
+ readonly themeColor?: string | undefined;
1105
+ /** Render an image at build time; the bytes are written to `meta/og/<unscoped>.png` and listed first. */
1106
+ readonly generate?: ((info: OgImageInfo) => Promise<Uint8Array>) | undefined;
1107
+ } | undefined;
1108
+ /** Registries; `false` disables the default derived from `targets.json`. */
1109
+ readonly registries?: ReadonlyArray<RegistryRef> | false | undefined;
1110
+ }
1111
+ //#endregion
1050
1112
  //#region src/meta/config.d.ts
1051
1113
  /**
1052
1114
  * A single TSDoc tag definition (parity with api-extractor's TSDoc config).
@@ -1091,6 +1153,14 @@ interface MetaOptions {
1091
1153
  */
1092
1154
  readonly optimistic?: "auto" | boolean | undefined;
1093
1155
  readonly tsdoc?: TsdocOptions | undefined;
1156
+ /**
1157
+ * The CONFIG tier of the emitted `tsdoctor.json` sidecar (ranked over the package's and the
1158
+ * workspace root's `tsdoctor.json` source files) and the optional build-time Open Graph image.
1159
+ * The project tier is found through workspace discovery, which requires the workspace root's
1160
+ * `package.json` to declare a `version`; without one, discovery fails silently and no `project`
1161
+ * tier is emitted.
1162
+ */
1163
+ readonly tsdoctor?: TsdoctorMetaOptions | undefined;
1094
1164
  }
1095
1165
  /**
1096
1166
  * Fully-resolved meta options (no optionals).
@@ -1104,6 +1174,8 @@ interface NormalizedMeta {
1104
1174
  readonly suppressWarnings: ReadonlyArray<WarningSuppressionRule>;
1105
1175
  readonly tagDefinitions: ReadonlyArray<TsdocTagDefinition>;
1106
1176
  };
1177
+ /** Passed through verbatim; `undefined` means no config tier (the source tiers still apply). */
1178
+ readonly tsdoctor: TsdoctorMetaOptions | undefined;
1107
1179
  }
1108
1180
  /**
1109
1181
  * Fill defaults so downstream code never branches on undefined.
@@ -1437,6 +1509,83 @@ declare function resolveJsxConfig(tsconfig: TsconfigJsx, override: JsxConfig | u
1437
1509
  */
1438
1510
  declare function readTsconfigJsx(cwd: string): TsconfigJsx;
1439
1511
  //#endregion
1512
+ //#region src/meta/tsdoctor-manifest.d.ts
1513
+ /**
1514
+ * A `targets.json` target as the manifest composer sees it: the human label and the registry endpoint.
1515
+ *
1516
+ * @public
1517
+ */
1518
+ interface ManifestTarget {
1519
+ readonly name: string;
1520
+ readonly registry: string;
1521
+ }
1522
+ /**
1523
+ * Everything {@link composeTsdoctorManifest} needs: the three authoring tiers plus the build facts
1524
+ * that derive the rest.
1525
+ *
1526
+ * @public
1527
+ */
1528
+ interface ComposeManifestInput {
1529
+ readonly config: TsdoctorMetaOptions | undefined;
1530
+ readonly leaf: ManifestSource | undefined;
1531
+ readonly project: ManifestSource | undefined;
1532
+ readonly packageName: string;
1533
+ readonly isPrivate: boolean;
1534
+ /** `targets.json` targets for the group being emitted. */
1535
+ readonly targets: ReadonlyArray<ManifestTarget>;
1536
+ /** The image `og-image.ts` wrote, already sized. */
1537
+ readonly generatedImage: OpenGraphImage | undefined;
1538
+ /** The emitted manifest's `repository` field; a GitHub Packages target derives its page URL from it. */
1539
+ readonly repository?: ManifestRepository | undefined;
1540
+ }
1541
+ /**
1542
+ * The `repository` field of the emitted `package.json`, as far as the manifest composer reads it.
1543
+ *
1544
+ * @public
1545
+ */
1546
+ interface ManifestRepository {
1547
+ readonly url: string;
1548
+ readonly directory?: string | undefined;
1549
+ }
1550
+ /**
1551
+ * `owner/repo` from any of the GitHub URL spellings a `repository.url` carries (https, `git+https`,
1552
+ * `git@`, `ssh://`, `git://`, the `github:` shorthand), or `undefined` for anything else.
1553
+ *
1554
+ * @public
1555
+ */
1556
+ declare function githubOwnerRepo(url: string): {
1557
+ readonly owner: string;
1558
+ readonly repo: string;
1559
+ } | undefined;
1560
+ /**
1561
+ * Derive the registries block from the build's targets. Only for a public
1562
+ * package: a private one is published nowhere.
1563
+ *
1564
+ * @public
1565
+ */
1566
+ declare function registriesFromTargets(input: Pick<ComposeManifestInput, "targets" | "packageName" | "isPrivate" | "repository">): ReadonlyArray<RegistryRef>;
1567
+ /**
1568
+ * Flatten the three authoring tiers into the emitted manifest. Pure.
1569
+ *
1570
+ * @remarks
1571
+ * Config beats leaf beats project per FIELD; the project tier is emitted
1572
+ * nested, never flattened, because the consumer's provenance ranking depends
1573
+ * on telling the tiers apart. Returns `undefined` when there is nothing to
1574
+ * say, so a package with no metadata emits no file. An `sbom` pointer is
1575
+ * never written here — the release action upserts it at publish.
1576
+ *
1577
+ * @public
1578
+ */
1579
+ declare function composeTsdoctorManifest(input: ComposeManifestInput): BundleManifest | undefined;
1580
+ /**
1581
+ * What the generator sees. Built from the same tiers as the manifest.
1582
+ *
1583
+ * @public
1584
+ */
1585
+ declare function ogImageInfoOf(input: ComposeManifestInput & {
1586
+ readonly version: string;
1587
+ }): OgImageInfo;
1588
+ //#endregion
1440
1589
  //#region src/meta/generate.d.ts
1441
1590
  /** @public */
1442
1591
  interface GenerateMetaOptions {
@@ -1474,6 +1623,16 @@ interface GenerateMetaOptions {
1474
1623
  readonly ci?: boolean | undefined;
1475
1624
  /** When set, messages matched by `suppressWarnings` are routed here for accounting. */
1476
1625
  readonly onSuppressed?: ((entry: DiagnosticInput) => void) | undefined;
1626
+ /**
1627
+ * The `tsdoctor.json` sidecar inputs: the config tier, the two source tiers, and the targets bound
1628
+ * to this group (registries derive from them). Omitted means no sidecar and no Open Graph image.
1629
+ */
1630
+ readonly tsdoctor?: {
1631
+ readonly config: TsdoctorMetaOptions | undefined;
1632
+ readonly leaf: ManifestSource | undefined;
1633
+ readonly project: ManifestSource | undefined;
1634
+ readonly targets: ReadonlyArray<ManifestTarget>;
1635
+ } | undefined;
1477
1636
  }
1478
1637
  /** @public */
1479
1638
  interface MetaResult {
@@ -1490,6 +1649,42 @@ interface MetaResult {
1490
1649
  */
1491
1650
  declare function generateMeta(options: GenerateMetaOptions): Promise<MetaResult>;
1492
1651
  //#endregion
1652
+ //#region src/meta/og-image.d.ts
1653
+ declare const OgGenerateError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
1654
+ readonly _tag: "OgGenerateError";
1655
+ } & Readonly<A>;
1656
+ /**
1657
+ * A configured `openGraph.generate` renderer threw, returned no bytes, or returned bytes that are
1658
+ * not an image. Fails the build: a half-written OG image is worse than none.
1659
+ *
1660
+ * @public
1661
+ */
1662
+ declare class OgGenerateError extends OgGenerateError_base<{
1663
+ readonly packageName: string;
1664
+ readonly cause: unknown;
1665
+ }> {
1666
+ get message(): string;
1667
+ }
1668
+ /**
1669
+ * Options for {@link writeGeneratedOgImage}.
1670
+ *
1671
+ * @public
1672
+ */
1673
+ interface WriteGeneratedOgImageOptions {
1674
+ readonly generate: (info: OgImageInfo) => Promise<Uint8Array>;
1675
+ readonly info: OgImageInfo;
1676
+ /** The meta bundle dir; the image lands at `og/<unscopedName>.<ext>` beneath it. */
1677
+ readonly outMetaDir: string;
1678
+ readonly unscopedName: string;
1679
+ }
1680
+ /**
1681
+ * Run the generator, size the bytes, and write `og/<unscoped>.<ext>` under the meta dir. Returns the
1682
+ * manifest image entry (bundle-relative path, MIME type, dimensions).
1683
+ *
1684
+ * @public
1685
+ */
1686
+ declare function writeGeneratedOgImage(options: WriteGeneratedOgImageOptions): Promise<OpenGraphImage>;
1687
+ //#endregion
1493
1688
  //#region src/meta/optimistic.d.ts
1494
1689
  /**
1495
1690
  * Rewrite a meta `package.json` so the package's own `version` and any workspace-sibling
@@ -1500,6 +1695,46 @@ declare function generateMeta(options: GenerateMetaOptions): Promise<MetaResult>
1500
1695
  */
1501
1696
  declare function rewriteMetaVersions(pkg: Record<string, unknown>, versions: ReadonlyMap<string, string>, selfName: string): Record<string, unknown>;
1502
1697
  //#endregion
1698
+ //#region src/meta/tsdoctor-source.d.ts
1699
+ declare const TsdoctorSourceError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
1700
+ readonly _tag: "TsdoctorSourceError";
1701
+ } & Readonly<A>;
1702
+ /**
1703
+ * A present `tsdoctor.json` source file that could not be parsed or decoded. Absence is never
1704
+ * this error — a missing source tier is the normal case.
1705
+ *
1706
+ * @public
1707
+ */
1708
+ declare class TsdoctorSourceError extends TsdoctorSourceError_base<{
1709
+ readonly path: string;
1710
+ readonly cause: unknown;
1711
+ }> {
1712
+ get message(): string;
1713
+ }
1714
+ /**
1715
+ * The two source tiers a build reads: the package's own file and the workspace root's.
1716
+ *
1717
+ * @public
1718
+ */
1719
+ interface TsdoctorSources {
1720
+ readonly leaf: ManifestSource | undefined;
1721
+ readonly project: ManifestSource | undefined;
1722
+ /**
1723
+ * Why workspace discovery failed, when it did. The project tier is then unknown rather than
1724
+ * absent; `runMetaPass` records it as a `meta` warning so the degradation is visible in `issues.json`.
1725
+ */
1726
+ readonly discoveryFailure?: string | undefined;
1727
+ }
1728
+ /**
1729
+ * Read the leaf (`<cwd>/tsdoctor.json`) and project (`<workspaceRoot>/tsdoctor.json`)
1730
+ * source tiers. Absence is normal; a present file that does not decode throws
1731
+ * {@link TsdoctorSourceError}. A package that IS the workspace root reads its file once, as the
1732
+ * leaf, and has no project tier.
1733
+ *
1734
+ * @public
1735
+ */
1736
+ declare function loadTsdoctorSources(cwd: string): Promise<TsdoctorSources>;
1737
+ //#endregion
1503
1738
  //#region src/meta/run-pass.d.ts
1504
1739
  /**
1505
1740
  * Options for the meta-pass orchestrator.
@@ -1529,6 +1764,17 @@ interface RunMetaPassOptions {
1529
1764
  readonly resolveNextVersions?: (cwd: string) => Promise<{
1530
1765
  versions: ReadonlyMap<string, string>;
1531
1766
  }>;
1767
+ /**
1768
+ * The resolved `targets.json` targets; each group's `tsdoctor.json` derives its registries from the
1769
+ * targets bound to that group. Omitted (an escape-hatch build with no resolution) means none.
1770
+ */
1771
+ readonly targets?: ReadonlyArray<{
1772
+ group: string;
1773
+ id: string;
1774
+ registry: string;
1775
+ }> | undefined;
1776
+ /** Injectable for tests; defaults to the real loadTsdoctorSources. */
1777
+ readonly loadTsdoctorSources?: (cwd: string) => Promise<TsdoctorSources>;
1532
1778
  }
1533
1779
  /**
1534
1780
  * Meta-pass orchestrator: derives export paths, filters bin/ entries, resolves optimistic
@@ -1865,5 +2111,5 @@ declare function resolveTargets(options: {
1865
2111
  baseName: string;
1866
2112
  }): TargetResolution;
1867
2113
  //#endregion
1868
- export { type AmbientDtsEntry, BuildCollector, BuildCollectorTag, type BuildEmittedManifestOptions, type BuildFormat, type BuildGroupSpec, type BuildIssues, type BuildPlatform, type BuildReport, BuildReport as BuildReportSchema, type BuildTargetGroupsOptions, CatalogAssemblyError, CiAnnotationsFormatter, ConfigValidationError, ConfigValidator, type CopyAmbientDtsOptions, type CssOptions, DEFAULT_EXE_NODE_VERSION, DependencyResolutionError, type DeriveOptions, type DerivedTsdownOptions, type DiagnosticEntry, type DiagnosticInput, type DtsExportClass, type DualExports, type EmitManifestOptions, type EmittedFile, type EntryOverride, type Environment, EnvironmentDetector, type ExeBuild, type ExeConfig, type ExeRewrite, type ExeSeaConfig, type ExeTarget, type ExeTargetInput, type Executor, ExecutorResolver, type ExtractAmbientOptions, type ExtractOptions, type ExtractResult, FormatSelector, type Formatter, type FormatterContext, type GenerateMetaOptions, type Json, JsonFormatter, type JsxConfig, type LooseFileSpec, type LooseFiles, ManifestDecodeError, type ManifestLike, MarkdownFormatter, MetaGenerationError, type MetaOptions, type MetaResult, type ModuleExportNames, type NextVersions, type NormalizedExe, type NormalizedLooseFile, type NormalizedMeta, type OutputFormat, OutputRenderer, type PackageJsonEntriesOptions, type PackageJsonLike, type PassKind, type PassReport, type PkgOsCpu, type PlainDiagnostic, type PortableTsconfig, type PublishTargetObject, type PublishTargetValue, type PublishTargets, type ReexportBarrelAnalysis, type RenderReportOptions, type RenderedOutput, ReportPipeline, ReportTimings, type ResolvedCompilerOptions, type ResolvedGroup, type ResolvedTarget, type ResolvedTsconfig, type ResolvedTsconfigOptions, type RunExeBuildOptions, type RunMetaPassOptions, SilentFormatter, type TargetGroupId, type TargetGroupRef, type TargetGroupReport, TargetGroupReport as TargetGroupReportSchema, type TargetResolution, TerminalFormatter, type Timer, type TransformManifestOptions, type TsconfigJsx, type TsdocOptions, type TsdocTagDefinition, type TsdownBuild, type TsdownLogger, UnresolvedDependencyError, type ValidationInput, type WarningSuppressionRule, ambientOutName, analyzeReexportBarrel, applySubdirMetaEntries, assertNoEntryCollisions, buildEmittedManifest, buildMetricsPlugin, buildResolvedTsconfig, buildTargetGroups, cjsDefaultInterop, classifyDtsExport, collectExportNames, computeExeFileName, copyAmbientDts, copyPublicDir, createEntryName, createTimer, createTsdownLogger, declarationExt, defaultManifestTransform, deriveExportPaths, deriveTargetGroupOptions, emitManifest, extractAmbientDts, extractEntries, findRelativeSpecifiers, flattenIssues, formatTime, generateMeta, isTargetObject, mixedDtsExportError, nodeBuiltinDefaultInterop, normalizeBinPaths, normalizeExeOptions, normalizeLooseFiles, normalizeMetaOptions, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReexportStub, renderReport, resolveJsxConfig, resolveManifest, resolveNextVersions, resolvePortableTsconfig, resolveTargets, rewriteMetaVersions, runExeBuild, runMetaPass, serializeIssues, transformBin, transformExports, transformManifest, writeDtsEmitTsconfig, writeIssuesArtifact, writeResolvedTsconfig, writeTargetsBinding };
2114
+ export { type AmbientDtsEntry, BuildCollector, BuildCollectorTag, type BuildEmittedManifestOptions, type BuildFormat, type BuildGroupSpec, type BuildIssues, type BuildPlatform, type BuildReport, BuildReport as BuildReportSchema, type BuildTargetGroupsOptions, CatalogAssemblyError, CiAnnotationsFormatter, type ComposeManifestInput, ConfigValidationError, ConfigValidator, type CopyAmbientDtsOptions, type CssOptions, DEFAULT_EXE_NODE_VERSION, DependencyResolutionError, type DeriveOptions, type DerivedTsdownOptions, type DiagnosticEntry, type DiagnosticInput, type DtsExportClass, type DualExports, type EmitManifestOptions, type EmittedFile, type EntryOverride, type Environment, EnvironmentDetector, type ExeBuild, type ExeConfig, type ExeRewrite, type ExeSeaConfig, type ExeTarget, type ExeTargetInput, type Executor, ExecutorResolver, type ExtractAmbientOptions, type ExtractOptions, type ExtractResult, FormatSelector, type Formatter, type FormatterContext, type GenerateMetaOptions, type Json, JsonFormatter, type JsxConfig, type LooseFileSpec, type LooseFiles, ManifestDecodeError, type ManifestLike, type ManifestRepository, type ManifestTarget, MarkdownFormatter, MetaGenerationError, type MetaOptions, type MetaResult, type ModuleExportNames, type NextVersions, type NormalizedExe, type NormalizedLooseFile, type NormalizedMeta, OgGenerateError, type OgImageInfo, type OutputFormat, OutputRenderer, type PackageJsonEntriesOptions, type PackageJsonLike, type PassKind, type PassReport, type PkgOsCpu, type PlainDiagnostic, type PortableTsconfig, type PublishTargetObject, type PublishTargetValue, type PublishTargets, type ReexportBarrelAnalysis, type RenderReportOptions, type RenderedOutput, ReportPipeline, ReportTimings, type ResolvedCompilerOptions, type ResolvedGroup, type ResolvedTarget, type ResolvedTsconfig, type ResolvedTsconfigOptions, type RunExeBuildOptions, type RunMetaPassOptions, SilentFormatter, type TargetGroupId, type TargetGroupRef, type TargetGroupReport, TargetGroupReport as TargetGroupReportSchema, type TargetResolution, TerminalFormatter, type Timer, type TransformManifestOptions, type TsconfigJsx, type TsdocOptions, type TsdocTagDefinition, TsdoctorEmitError, type TsdoctorMetaOptions, TsdoctorSourceError, type TsdoctorSources, type TsdownBuild, type TsdownLogger, UnresolvedDependencyError, type ValidationInput, type WarningSuppressionRule, type WriteGeneratedOgImageOptions, ambientOutName, analyzeReexportBarrel, applySubdirMetaEntries, assertNoEntryCollisions, buildEmittedManifest, buildMetricsPlugin, buildResolvedTsconfig, buildTargetGroups, cjsDefaultInterop, classifyDtsExport, collectExportNames, composeTsdoctorManifest, computeExeFileName, copyAmbientDts, copyPublicDir, createEntryName, createTimer, createTsdownLogger, declarationExt, defaultManifestTransform, deriveExportPaths, deriveTargetGroupOptions, emitManifest, extractAmbientDts, extractEntries, findRelativeSpecifiers, flattenIssues, formatTime, generateMeta, githubOwnerRepo, isTargetObject, loadTsdoctorSources, mixedDtsExportError, nodeBuiltinDefaultInterop, normalizeBinPaths, normalizeExeOptions, normalizeLooseFiles, normalizeMetaOptions, ogImageInfoOf, packageJsonEntries, readTsconfigJsx, registriesFromTargets, removeDeclarationMaps, renderReexportStub, renderReport, resolveJsxConfig, resolveManifest, resolveNextVersions, resolvePortableTsconfig, resolveTargets, rewriteMetaVersions, runExeBuild, runMetaPass, serializeIssues, transformBin, transformExports, transformManifest, writeDtsEmitTsconfig, writeGeneratedOgImage, writeIssuesArtifact, writeResolvedTsconfig, writeTargetsBinding };
1869
2115
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { analyzeReexportBarrel, collectExportNames, renderReexportStub } from "./dts/reexport-stub.js";
2
2
  import { buildResolvedTsconfig, writeDtsEmitTsconfig, writeResolvedTsconfig } from "./dts/resolved-tsconfig.js";
3
- import { ConfigValidationError, MetaGenerationError } from "./errors.js";
3
+ import { ConfigValidationError, MetaGenerationError, TsdoctorEmitError } from "./errors.js";
4
4
  import { createEntryName, extractEntries } from "./entry/extract.js";
5
5
  import { ambientOutName, assertNoEntryCollisions, classifyDtsExport, declarationExt, extractAmbientDts, mixedDtsExportError } from "./entry/ambient-dts.js";
6
6
  import { resolveManifest } from "./catalog/resolve-catalogs.js";
@@ -27,9 +27,12 @@ import { runExeBuild } from "./exe/build.js";
27
27
  import { computeExeFileName } from "./exe/filename.js";
28
28
  import { readTsconfigJsx, resolveJsxConfig } from "./jsx/config.js";
29
29
  import { normalizeMetaOptions } from "./meta/config.js";
30
+ import { OgGenerateError, writeGeneratedOgImage } from "./meta/og-image.js";
30
31
  import { resolvePortableTsconfig } from "./meta/tsconfig-resolver.js";
32
+ import { composeTsdoctorManifest, githubOwnerRepo, ogImageInfoOf, registriesFromTargets } from "./meta/tsdoctor-manifest.js";
31
33
  import { generateMeta } from "./meta/generate.js";
32
34
  import { rewriteMetaVersions } from "./meta/optimistic.js";
35
+ import { TsdoctorSourceError, loadTsdoctorSources } from "./meta/tsdoctor-source.js";
33
36
  import { applySubdirMetaEntries, deriveExportPaths, runMetaPass } from "./meta/run-pass.js";
34
37
  import { BuildReport, ReportTimings, TargetGroupReport } from "./report/schema.js";
35
38
  import { BuildCollector, BuildCollectorTag } from "./report/collector.js";
@@ -47,4 +50,4 @@ import { ReportPipeline, renderReport } from "./report/pipeline.js";
47
50
  import { writeTargetsBinding } from "./targets/binding.js";
48
51
  import { CatalogAssemblyError, DependencyResolutionError, ManifestDecodeError, UnresolvedDependencyError } from "@effected/npm";
49
52
 
50
- export { BuildCollector, BuildCollectorTag, BuildReport as BuildReportSchema, CatalogAssemblyError, CiAnnotationsFormatter, ConfigValidationError, ConfigValidator, DEFAULT_EXE_NODE_VERSION, DependencyResolutionError, EnvironmentDetector, ExecutorResolver, FormatSelector, JsonFormatter, ManifestDecodeError, MarkdownFormatter, MetaGenerationError, OutputRenderer, ReportPipeline, ReportTimings, SilentFormatter, TargetGroupReport as TargetGroupReportSchema, TerminalFormatter, UnresolvedDependencyError, ambientOutName, analyzeReexportBarrel, applySubdirMetaEntries, assertNoEntryCollisions, buildEmittedManifest, buildMetricsPlugin, buildResolvedTsconfig, buildTargetGroups, cjsDefaultInterop, classifyDtsExport, collectExportNames, computeExeFileName, copyAmbientDts, copyPublicDir, createEntryName, createTimer, createTsdownLogger, declarationExt, defaultManifestTransform, deriveExportPaths, deriveTargetGroupOptions, emitManifest, extractAmbientDts, extractEntries, findRelativeSpecifiers, flattenIssues, formatTime, generateMeta, isTargetObject, mixedDtsExportError, nodeBuiltinDefaultInterop, normalizeBinPaths, normalizeExeOptions, normalizeLooseFiles, normalizeMetaOptions, packageJsonEntries, readTsconfigJsx, removeDeclarationMaps, renderReexportStub, renderReport, resolveJsxConfig, resolveManifest, resolveNextVersions, resolvePortableTsconfig, resolveTargets, rewriteMetaVersions, runExeBuild, runMetaPass, serializeIssues, transformBin, transformExports, transformManifest, writeDtsEmitTsconfig, writeIssuesArtifact, writeResolvedTsconfig, writeTargetsBinding };
53
+ export { BuildCollector, BuildCollectorTag, BuildReport as BuildReportSchema, CatalogAssemblyError, CiAnnotationsFormatter, ConfigValidationError, ConfigValidator, DEFAULT_EXE_NODE_VERSION, DependencyResolutionError, EnvironmentDetector, ExecutorResolver, FormatSelector, JsonFormatter, ManifestDecodeError, MarkdownFormatter, MetaGenerationError, OgGenerateError, OutputRenderer, ReportPipeline, ReportTimings, SilentFormatter, TargetGroupReport as TargetGroupReportSchema, TerminalFormatter, TsdoctorEmitError, TsdoctorSourceError, UnresolvedDependencyError, ambientOutName, analyzeReexportBarrel, applySubdirMetaEntries, assertNoEntryCollisions, buildEmittedManifest, buildMetricsPlugin, buildResolvedTsconfig, buildTargetGroups, cjsDefaultInterop, classifyDtsExport, collectExportNames, composeTsdoctorManifest, computeExeFileName, copyAmbientDts, copyPublicDir, createEntryName, createTimer, createTsdownLogger, declarationExt, defaultManifestTransform, deriveExportPaths, deriveTargetGroupOptions, emitManifest, extractAmbientDts, extractEntries, findRelativeSpecifiers, flattenIssues, formatTime, generateMeta, githubOwnerRepo, isTargetObject, loadTsdoctorSources, mixedDtsExportError, nodeBuiltinDefaultInterop, normalizeBinPaths, normalizeExeOptions, normalizeLooseFiles, normalizeMetaOptions, ogImageInfoOf, packageJsonEntries, readTsconfigJsx, registriesFromTargets, removeDeclarationMaps, renderReexportStub, renderReport, resolveJsxConfig, resolveManifest, resolveNextVersions, resolvePortableTsconfig, resolveTargets, rewriteMetaVersions, runExeBuild, runMetaPass, serializeIssues, transformBin, transformExports, transformManifest, writeDtsEmitTsconfig, writeGeneratedOgImage, writeIssuesArtifact, writeResolvedTsconfig, writeTargetsBinding };
package/meta/config.js CHANGED
@@ -16,7 +16,8 @@ function normalizeMetaOptions(meta, env = process.env) {
16
16
  tsdoc: {
17
17
  suppressWarnings: meta.tsdoc?.suppressWarnings ?? [],
18
18
  tagDefinitions: meta.tsdoc?.tagDefinitions ?? []
19
- }
19
+ },
20
+ tsdoctor: meta.tsdoctor
20
21
  };
21
22
  }
22
23
 
package/meta/generate.js CHANGED
@@ -1,15 +1,31 @@
1
+ import { TsdoctorEmitError } from "../errors.js";
1
2
  import { runApiExtractor } from "./api-extractor.js";
2
3
  import { mergeApiModels } from "./merge-models.js";
4
+ import { writeGeneratedOgImage } from "./og-image.js";
3
5
  import { resolvePortableTsconfig } from "./tsconfig-resolver.js";
4
6
  import { writeTsdocConfig } from "./tsdoc-config.js";
7
+ import { composeTsdoctorManifest, ogImageInfoOf } from "./tsdoctor-manifest.js";
5
8
  import { copyFileSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
9
  import { join } from "node:path";
10
+ import { Effect } from "effect";
11
+ import { TSDOCTOR_MANIFEST_FILENAME, encodeBundleManifest } from "@tsdoctor/manifest";
7
12
 
8
13
  //#region src/meta/generate.ts
9
14
  function unscopedName(name) {
10
15
  const slash = name.lastIndexOf("/");
11
16
  return slash >= 0 ? name.slice(slash + 1) : name;
12
17
  }
18
+ /** Normalize the manifest's `repository` (string shorthand or object) to the composer's shape. */
19
+ function manifestRepositoryOf(value) {
20
+ if (typeof value === "string") return value.length > 0 ? { url: value } : void 0;
21
+ if (typeof value === "object" && value !== null && "url" in value && typeof value.url === "string") {
22
+ const directory = "directory" in value && typeof value.directory === "string" ? value.directory : void 0;
23
+ return {
24
+ url: value.url,
25
+ ...directory !== void 0 ? { directory } : {}
26
+ };
27
+ }
28
+ }
13
29
  /**
14
30
  * Generate the api-model meta bundle from already-emitted .d.ts. Writes tsdoc.json (idempotent),
15
31
  * runs the extractor per entry, merges if needed, and writes the "virtual TS env" trio to
@@ -116,12 +132,70 @@ async function generateMeta(options) {
116
132
  const bundleTsconfig = join(outMetaDir, "tsconfig.json");
117
133
  const portableTsconfig = resolvePortableTsconfig(cwd, tsconfigPath);
118
134
  writeFileSync(bundleTsconfig, `${JSON.stringify(portableTsconfig, null, 2)}\n`, "utf-8");
135
+ rmSync(join(outMetaDir, TSDOCTOR_MANIFEST_FILENAME), { force: true });
136
+ rmSync(join(outMetaDir, "og"), {
137
+ recursive: true,
138
+ force: true
139
+ });
140
+ let manifestPath;
141
+ let generatedImageRelative;
142
+ if (options.tsdoctor !== void 0) {
143
+ const composeInput = {
144
+ config: options.tsdoctor.config,
145
+ leaf: options.tsdoctor.leaf,
146
+ project: options.tsdoctor.project,
147
+ packageName,
148
+ isPrivate: finalPkg.private === true,
149
+ targets: options.tsdoctor.targets,
150
+ generatedImage: void 0,
151
+ repository: manifestRepositoryOf(finalPkg.repository)
152
+ };
153
+ const generate = options.tsdoctor.config?.openGraph?.generate;
154
+ if (generate !== void 0) {
155
+ const generated = await writeGeneratedOgImage({
156
+ generate,
157
+ info: ogImageInfoOf({
158
+ ...composeInput,
159
+ version: String(finalPkg.version ?? "0.0.0")
160
+ }),
161
+ outMetaDir,
162
+ unscopedName: unscopedName(packageName)
163
+ });
164
+ composeInput.generatedImage = generated;
165
+ generatedImageRelative = generated.path;
166
+ }
167
+ const manifest = composeTsdoctorManifest(composeInput);
168
+ if (manifest !== void 0) {
169
+ const path = join(outMetaDir, TSDOCTOR_MANIFEST_FILENAME);
170
+ try {
171
+ const encoded = await Effect.runPromise(encodeBundleManifest(manifest));
172
+ writeFileSync(path, `${JSON.stringify(encoded, null, 2)}\n`, "utf-8");
173
+ } catch (cause) {
174
+ throw new TsdoctorEmitError({
175
+ packageName,
176
+ path,
177
+ cause
178
+ });
179
+ }
180
+ manifestPath = path;
181
+ }
182
+ }
119
183
  for (const localPath of localPaths) {
120
184
  const dest = join(cwd, localPath);
121
185
  mkdirSync(dest, { recursive: true });
122
186
  copyFileSync(apiJsonPath, join(dest, apiJsonFilename));
123
187
  copyFileSync(bundlePackageJson, join(dest, "package.json"));
124
188
  copyFileSync(bundleTsconfig, join(dest, "tsconfig.json"));
189
+ if (manifestPath !== void 0) copyFileSync(manifestPath, join(dest, TSDOCTOR_MANIFEST_FILENAME));
190
+ else rmSync(join(dest, TSDOCTOR_MANIFEST_FILENAME), { force: true });
191
+ rmSync(join(dest, "og"), {
192
+ recursive: true,
193
+ force: true
194
+ });
195
+ if (generatedImageRelative !== void 0) {
196
+ mkdirSync(join(dest, "og"), { recursive: true });
197
+ copyFileSync(join(outMetaDir, generatedImageRelative), join(dest, generatedImageRelative));
198
+ }
125
199
  }
126
200
  return {
127
201
  apiJsonPath,
@@ -0,0 +1,79 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { Data } from "effect";
4
+ import { imageSize } from "image-size";
5
+
6
+ //#region src/meta/og-image.ts
7
+ /**
8
+ * A configured `openGraph.generate` renderer threw, returned no bytes, or returned bytes that are
9
+ * not an image. Fails the build: a half-written OG image is worse than none.
10
+ *
11
+ * @public
12
+ */
13
+ var OgGenerateError = class extends Data.TaggedError("OgGenerateError") {
14
+ get message() {
15
+ const reason = this.cause instanceof Error ? this.cause.message : String(this.cause);
16
+ return `Open Graph image generation failed for ${this.packageName}: ${reason}`;
17
+ }
18
+ };
19
+ /** The image types an Open Graph consumer can render; anything else fails rather than shipping a mislabeled file. */
20
+ const MIME_BY_TYPE = {
21
+ png: "image/png",
22
+ jpg: "image/jpeg",
23
+ webp: "image/webp"
24
+ };
25
+ /**
26
+ * Run the generator, size the bytes, and write `og/<unscoped>.<ext>` under the meta dir. Returns the
27
+ * manifest image entry (bundle-relative path, MIME type, dimensions).
28
+ *
29
+ * @public
30
+ */
31
+ async function writeGeneratedOgImage(options) {
32
+ let bytes;
33
+ try {
34
+ bytes = await options.generate(options.info);
35
+ } catch (cause) {
36
+ throw new OgGenerateError({
37
+ packageName: options.info.packageName,
38
+ cause
39
+ });
40
+ }
41
+ if (bytes.byteLength === 0) throw new OgGenerateError({
42
+ packageName: options.info.packageName,
43
+ cause: /* @__PURE__ */ new Error("generator returned no bytes")
44
+ });
45
+ let size;
46
+ try {
47
+ size = imageSize(bytes);
48
+ } catch (cause) {
49
+ throw new OgGenerateError({
50
+ packageName: options.info.packageName,
51
+ cause
52
+ });
53
+ }
54
+ const ext = size.type ?? "png";
55
+ const type = MIME_BY_TYPE[ext];
56
+ if (type === void 0) throw new OgGenerateError({
57
+ packageName: options.info.packageName,
58
+ cause: /* @__PURE__ */ new Error(`generator returned a ${ext} image; Open Graph images must be png, jpg or webp`)
59
+ });
60
+ const relative = `og/${options.unscopedName}.${ext}`;
61
+ try {
62
+ mkdirSync(join(options.outMetaDir, "og"), { recursive: true });
63
+ writeFileSync(join(options.outMetaDir, relative), bytes);
64
+ } catch (cause) {
65
+ throw new OgGenerateError({
66
+ packageName: options.info.packageName,
67
+ cause
68
+ });
69
+ }
70
+ return {
71
+ path: relative,
72
+ type,
73
+ width: size.width,
74
+ height: size.height
75
+ };
76
+ }
77
+
78
+ //#endregion
79
+ export { OgGenerateError, writeGeneratedOgImage };
package/meta/run-pass.js CHANGED
@@ -1,9 +1,12 @@
1
+ import { TsdoctorEmitError } from "../errors.js";
1
2
  import { createEntryName } from "../entry/extract.js";
2
3
  import { declarationsDirFor } from "../build/target-groups.js";
3
4
  import { resolveNextVersions } from "../changesets/next-versions.js";
4
5
  import { normalizeMetaOptions } from "./config.js";
6
+ import { OgGenerateError } from "./og-image.js";
5
7
  import { generateMeta } from "./generate.js";
6
8
  import { rewriteMetaVersions } from "./optimistic.js";
9
+ import { TsdoctorSourceError, loadTsdoctorSources } from "./tsdoctor-source.js";
7
10
  import { join } from "node:path";
8
11
 
9
12
  //#region src/meta/run-pass.ts
@@ -27,22 +30,71 @@ async function runMetaPass(o) {
27
30
  const resolveNext = o.resolveNextVersions ?? resolveNextVersions;
28
31
  const nextVersions = norm.optimistic ? await resolveNext(o.cwd) : void 0;
29
32
  const manifestTransform = nextVersions ? (m) => rewriteMetaVersions(m, nextVersions.versions, o.packageName) : void 0;
30
- for (const g of o.groups) await gen({
31
- cwd: o.cwd,
32
- packageName: o.packageName,
33
- tsconfigPath: o.tsconfigPath,
34
- dtsDir: join(o.cwd, "dist", "prod", g.id, "pkg"),
35
- aeInputDir: declarationsDirFor(o.cwd, g.id),
36
- entries: dtsBasenames,
37
- exportPaths,
38
- outMetaDir: join(o.cwd, "dist", "prod", g.id, "meta"),
39
- localPaths: g.id === canonicalId ? norm.localPaths : [],
40
- tsdoc: norm.tsdoc,
41
- ...manifestTransform !== void 0 ? { manifestTransform } : {},
42
- ci: o.ci,
43
- onMessage: (e) => e.level === "error" ? o.collector.recordError(g.id, e) : o.collector.recordWarning(g.id, e),
44
- onSuppressed: (e) => o.collector.recordSuppressed(g.id, e)
33
+ const loadSources = o.loadTsdoctorSources ?? loadTsdoctorSources;
34
+ let sources;
35
+ try {
36
+ sources = await loadSources(o.cwd);
37
+ } catch (err) {
38
+ if (err instanceof TsdoctorSourceError) for (const g of o.groups) o.collector.recordError(g.id, {
39
+ source: "meta",
40
+ level: "error",
41
+ code: "tsdoctor-source-invalid",
42
+ text: err.message,
43
+ file: err.path
44
+ });
45
+ throw err;
46
+ }
47
+ if (sources.discoveryFailure !== void 0) for (const g of o.groups) o.collector.recordWarning(g.id, {
48
+ source: "meta",
49
+ level: "warn",
50
+ code: "tsdoctor-workspace-discovery-failed",
51
+ text: `Workspace discovery failed, so tsdoctor.json has no project tier: ${sources.discoveryFailure}`
45
52
  });
53
+ for (const g of o.groups) {
54
+ const targets = (o.targets ?? []).filter((t) => t.group === g.id).map((t) => ({
55
+ name: t.id,
56
+ registry: t.registry
57
+ }));
58
+ try {
59
+ await gen({
60
+ cwd: o.cwd,
61
+ packageName: o.packageName,
62
+ tsconfigPath: o.tsconfigPath,
63
+ dtsDir: join(o.cwd, "dist", "prod", g.id, "pkg"),
64
+ aeInputDir: declarationsDirFor(o.cwd, g.id),
65
+ entries: dtsBasenames,
66
+ exportPaths,
67
+ outMetaDir: join(o.cwd, "dist", "prod", g.id, "meta"),
68
+ localPaths: g.id === canonicalId ? norm.localPaths : [],
69
+ tsdoc: norm.tsdoc,
70
+ ...manifestTransform !== void 0 ? { manifestTransform } : {},
71
+ ci: o.ci,
72
+ tsdoctor: {
73
+ config: norm.tsdoctor,
74
+ leaf: sources.leaf,
75
+ project: sources.project,
76
+ targets
77
+ },
78
+ onMessage: (e) => e.level === "error" ? o.collector.recordError(g.id, e) : o.collector.recordWarning(g.id, e),
79
+ onSuppressed: (e) => o.collector.recordSuppressed(g.id, e)
80
+ });
81
+ } catch (err) {
82
+ if (err instanceof OgGenerateError) o.collector.recordError(g.id, {
83
+ source: "meta",
84
+ level: "error",
85
+ code: "og-generate-failed",
86
+ text: err.message
87
+ });
88
+ else if (err instanceof TsdoctorEmitError) o.collector.recordError(g.id, {
89
+ source: "meta",
90
+ level: "error",
91
+ code: "tsdoctor-emit-failed",
92
+ text: err.message,
93
+ file: err.path
94
+ });
95
+ throw err;
96
+ }
97
+ }
46
98
  }
47
99
  /**
48
100
  * Map entry names to export paths using the package exports map. index maps to ".".
@@ -0,0 +1,135 @@
1
+ import { MANIFEST_SPEC } from "@tsdoctor/manifest";
2
+
3
+ //#region src/meta/tsdoctor-manifest.ts
4
+ const first = (...values) => values.find((v) => v !== void 0);
5
+ /**
6
+ * `owner/repo` from any of the GitHub URL spellings a `repository.url` carries (https, `git+https`,
7
+ * `git@`, `ssh://`, `git://`, the `github:` shorthand), or `undefined` for anything else.
8
+ *
9
+ * @public
10
+ */
11
+ function githubOwnerRepo(url) {
12
+ const match = /^(?:git\+)?(?:https?:\/\/|git:\/\/|ssh:\/\/(?:git@)?|git@)?(?:www\.)?github\.com[/:]([^/]{1,214})\/([^/]{1,214}?)(?:\.git)?\/?$/i.exec(url.trim()) ?? /^github:([^/]{1,214})\/([^/]{1,214}?)(?:\.git)?$/i.exec(url.trim());
13
+ if (match === null) return void 0;
14
+ const owner = match[1];
15
+ const repo = match[2];
16
+ if (owner === void 0 || repo === void 0 || owner.length === 0 || repo.length === 0) return void 0;
17
+ return {
18
+ owner,
19
+ repo
20
+ };
21
+ }
22
+ function unscopedName(name) {
23
+ const slash = name.lastIndexOf("/");
24
+ return slash >= 0 ? name.slice(slash + 1) : name;
25
+ }
26
+ /**
27
+ * The package's human page on a registry, or `undefined` when no real page can be derived. npmjs
28
+ * has a well-known page; GitHub Packages pages hang off the repository, so they need a parseable
29
+ * `repository.url` and are omitted rather than emitted as a dead link when it is missing.
30
+ */
31
+ /** Strip trailing slashes without a `/\/+$/` regex, which backtracks polynomially on slash runs (CodeQL js/polynomial-redos). */
32
+ function stripTrailingSlashes(value) {
33
+ let end = value.length;
34
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
35
+ return value.slice(0, end);
36
+ }
37
+ function packagePageUrl(registry, packageName, repository) {
38
+ const host = stripTrailingSlashes(registry);
39
+ if (host === "https://registry.npmjs.org") return `https://www.npmjs.com/package/${packageName}`;
40
+ if (/^https?:\/\/npm\.pkg\.github\.com$/i.test(host)) {
41
+ const gh = repository !== void 0 ? githubOwnerRepo(repository.url) : void 0;
42
+ return gh === void 0 ? void 0 : `https://github.com/${gh.owner}/${gh.repo}/pkgs/npm/${unscopedName(packageName)}`;
43
+ }
44
+ return `${host}/package/${packageName}`;
45
+ }
46
+ function projectIdentityOf(project) {
47
+ if (project === void 0) return void 0;
48
+ if (project.name === void 0 && project.tagline === void 0) return void 0;
49
+ return {
50
+ ...project.name !== void 0 ? { name: project.name } : {},
51
+ ...project.tagline !== void 0 ? { tagline: project.tagline } : {}
52
+ };
53
+ }
54
+ /**
55
+ * Derive the registries block from the build's targets. Only for a public
56
+ * package: a private one is published nowhere.
57
+ *
58
+ * @public
59
+ */
60
+ function registriesFromTargets(input) {
61
+ if (input.isPrivate) return [];
62
+ const out = [];
63
+ for (const t of input.targets) {
64
+ const url = packagePageUrl(t.registry, input.packageName, input.repository);
65
+ if (url !== void 0) out.push({
66
+ type: "npm",
67
+ name: t.name,
68
+ url
69
+ });
70
+ }
71
+ return out;
72
+ }
73
+ /**
74
+ * Flatten the three authoring tiers into the emitted manifest. Pure.
75
+ *
76
+ * @remarks
77
+ * Config beats leaf beats project per FIELD; the project tier is emitted
78
+ * nested, never flattened, because the consumer's provenance ranking depends
79
+ * on telling the tiers apart. Returns `undefined` when there is nothing to
80
+ * say, so a package with no metadata emits no file. An `sbom` pointer is
81
+ * never written here — the release action upserts it at publish.
82
+ *
83
+ * @public
84
+ */
85
+ function composeTsdoctorManifest(input) {
86
+ const { config, leaf, project } = input;
87
+ const images = [
88
+ ...input.generatedImage !== void 0 ? [input.generatedImage] : [],
89
+ ...config?.openGraph?.images ?? [],
90
+ ...leaf?.openGraph?.images ?? [],
91
+ ...project?.openGraph?.images ?? []
92
+ ];
93
+ const themeColor = first(config?.openGraph?.themeColor, leaf?.openGraph?.themeColor, project?.openGraph?.themeColor);
94
+ const openGraph = images.length > 0 || themeColor !== void 0 ? {
95
+ ...images.length > 0 ? { images } : {},
96
+ ...themeColor !== void 0 ? { themeColor } : {}
97
+ } : void 0;
98
+ const registries = config?.registries === false ? void 0 : first(config?.registries, leaf?.registries) ?? registriesFromTargets(input);
99
+ const name = first(config?.name, leaf?.name);
100
+ const tagline = first(config?.tagline, leaf?.tagline);
101
+ const description = first(config?.description, leaf?.description);
102
+ const projectIdentity = projectIdentityOf(project);
103
+ const manifest = {
104
+ spec: MANIFEST_SPEC,
105
+ ...name !== void 0 ? { name } : {},
106
+ ...tagline !== void 0 ? { tagline } : {},
107
+ ...description !== void 0 ? { description } : {},
108
+ ...projectIdentity !== void 0 ? { project: projectIdentity } : {},
109
+ ...openGraph !== void 0 ? { openGraph } : {},
110
+ ...registries !== void 0 && registries.length > 0 ? { registries } : {}
111
+ };
112
+ return Object.keys(manifest).length > 1 ? manifest : void 0;
113
+ }
114
+ /**
115
+ * What the generator sees. Built from the same tiers as the manifest.
116
+ *
117
+ * @public
118
+ */
119
+ function ogImageInfoOf(input) {
120
+ const { config, leaf, project } = input;
121
+ const tagline = first(config?.tagline, leaf?.tagline, project?.tagline);
122
+ const description = first(config?.description, leaf?.description);
123
+ const projectIdentity = projectIdentityOf(project);
124
+ return {
125
+ name: first(config?.name, leaf?.name) ?? input.packageName,
126
+ packageName: input.packageName,
127
+ version: input.version,
128
+ ...tagline !== void 0 ? { tagline } : {},
129
+ ...description !== void 0 ? { description } : {},
130
+ ...projectIdentity !== void 0 ? { project: projectIdentity } : {}
131
+ };
132
+ }
133
+
134
+ //#endregion
135
+ export { composeTsdoctorManifest, githubOwnerRepo, ogImageInfoOf, registriesFromTargets };
@@ -0,0 +1,93 @@
1
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { Data, Effect, Layer } from "effect";
4
+ import { NodeFileSystem, NodePath } from "@effect/platform-node";
5
+ import { WorkspaceDiscovery, WorkspaceRootNotFoundError, Workspaces } from "@effected/workspaces";
6
+ import { TSDOCTOR_MANIFEST_FILENAME, decodeManifestSource } from "@tsdoctor/manifest";
7
+
8
+ //#region src/meta/tsdoctor-source.ts
9
+ /**
10
+ * A present `tsdoctor.json` source file that could not be parsed or decoded. Absence is never
11
+ * this error — a missing source tier is the normal case.
12
+ *
13
+ * @public
14
+ */
15
+ var TsdoctorSourceError = class extends Data.TaggedError("TsdoctorSourceError") {
16
+ get message() {
17
+ return `Invalid ${TSDOCTOR_MANIFEST_FILENAME} at ${this.path}`;
18
+ }
19
+ };
20
+ /** Bound once: the platform layer is stateless and layers memoize by reference. */
21
+ const platform = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer);
22
+ /** The canonical absolute form of a directory, so a relative or symlinked `cwd` compares equal to the discovered root. */
23
+ function canonical(dir) {
24
+ const absolute = resolve(dir);
25
+ try {
26
+ return realpathSync(absolute);
27
+ } catch {
28
+ return absolute;
29
+ }
30
+ }
31
+ /** `Effect.runPromise` rejects with the failure itself or with a `FiberFailure` wrapping it; check both. */
32
+ function isRootNotFound(cause) {
33
+ if (cause instanceof WorkspaceRootNotFoundError) return true;
34
+ return typeof cause === "object" && cause !== null && "_tag" in cause && cause._tag === "WorkspaceRootNotFoundError";
35
+ }
36
+ /**
37
+ * The workspace root containing `cwd`, or the reason discovery failed. A package outside any
38
+ * workspace is the `root: undefined` case with no failure. Mirrors the `WorkspaceDiscovery`
39
+ * invocation in `changesets/next-versions.ts`.
40
+ */
41
+ async function findWorkspaceRoot(cwd) {
42
+ try {
43
+ const packages = await Effect.runPromise(Effect.gen(function* () {
44
+ return yield* (yield* WorkspaceDiscovery).listPackages();
45
+ }).pipe(Effect.provide(Workspaces.layer({ cwd }).pipe(Layer.provide(platform)))));
46
+ return { root: packages[0]?.isRootWorkspace ? packages[0].path : void 0 };
47
+ } catch (cause) {
48
+ if (isRootNotFound(cause)) return { root: void 0 };
49
+ return {
50
+ root: void 0,
51
+ failure: cause instanceof Error ? cause.message : String(cause)
52
+ };
53
+ }
54
+ }
55
+ async function readSource(path) {
56
+ if (!existsSync(path)) return void 0;
57
+ let parsed;
58
+ try {
59
+ parsed = JSON.parse(readFileSync(path, "utf-8"));
60
+ } catch (cause) {
61
+ throw new TsdoctorSourceError({
62
+ path,
63
+ cause
64
+ });
65
+ }
66
+ const result = await Effect.runPromise(Effect.result(decodeManifestSource(parsed, path)));
67
+ if (result._tag === "Failure") throw new TsdoctorSourceError({
68
+ path,
69
+ cause: result.failure
70
+ });
71
+ return result.success;
72
+ }
73
+ /**
74
+ * Read the leaf (`<cwd>/tsdoctor.json`) and project (`<workspaceRoot>/tsdoctor.json`)
75
+ * source tiers. Absence is normal; a present file that does not decode throws
76
+ * {@link TsdoctorSourceError}. A package that IS the workspace root reads its file once, as the
77
+ * leaf, and has no project tier.
78
+ *
79
+ * @public
80
+ */
81
+ async function loadTsdoctorSources(cwd) {
82
+ const leafDir = canonical(cwd);
83
+ const leaf = await readSource(join(leafDir, TSDOCTOR_MANIFEST_FILENAME));
84
+ const { root, failure } = await findWorkspaceRoot(leafDir);
85
+ return {
86
+ leaf,
87
+ project: root !== void 0 && canonical(root) !== leafDir ? await readSource(join(root, TSDOCTOR_MANIFEST_FILENAME)) : void 0,
88
+ ...failure !== void 0 ? { discoveryFailure: failure } : {}
89
+ };
90
+ }
91
+
92
+ //#endregion
93
+ export { TsdoctorSourceError, loadTsdoctorSources };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/tsdown-plugins",
3
- "version": "2.5.10",
3
+ "version": "2.6.0",
4
4
  "private": false,
5
5
  "description": "Interface-only tsdown/rolldown plugin pack powering @savvy-web/bundler",
6
6
  "homepage": "https://github.com/savvy-web/systems/tree/main/packages/tsdown-plugins",
@@ -33,12 +33,14 @@
33
33
  "@effect/platform-node": "4.0.0-rc.109",
34
34
  "@effected/npm": "^0.12.1",
35
35
  "@effected/package-json": "^0.13.0",
36
- "@effected/tsconfig-json": "^0.6.1",
36
+ "@effected/tsconfig-json": "^0.7.0",
37
37
  "@effected/workspaces": "^0.18.3",
38
38
  "@microsoft/api-extractor": "^7.59.0",
39
39
  "@microsoft/tsdoc": "^0.16.0",
40
40
  "@microsoft/tsdoc-config": "^0.18.1",
41
+ "@tsdoctor/manifest": "^0.1.0",
41
42
  "effect": "4.0.0-rc.109",
43
+ "image-size": "^2.0.2",
42
44
  "picocolors": "^1.1.1",
43
45
  "std-env": "^4.2.0",
44
46
  "tsdown": "^0.22.14",
package/report/schema.js CHANGED
@@ -4,7 +4,8 @@ import { Schema } from "effect";
4
4
  /** @public */
5
5
  var ReportTimings = class extends Schema.Class("ReportTimings")({ totalMs: Schema.Number }) {};
6
6
  /**
7
- * A captured warning or error, from tsdown's logger, rolldown's onLog, or API Extractor.
7
+ * A captured warning or error, from tsdown's logger, rolldown's onLog, API Extractor, or the meta
8
+ * pass's own sidecar work (`tsdoctor.json` sources, Open Graph generation).
8
9
  *
9
10
  * @public
10
11
  */
@@ -12,7 +13,8 @@ var DiagnosticEntry = class extends Schema.Class("DiagnosticEntry")({
12
13
  source: Schema.Literals([
13
14
  "tsdown",
14
15
  "rolldown",
15
- "api-extractor"
16
+ "api-extractor",
17
+ "meta"
16
18
  ]),
17
19
  level: Schema.Literals(["warn", "error"]),
18
20
  text: Schema.String,