@tsdoctor/bundle 0.2.4 → 0.3.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/Bundle.js CHANGED
@@ -1,4 +1,4 @@
1
- import { BundleManifestError, TSDOCTOR_MANIFEST_FILENAME, decodeBundleManifest } from "@tsdoctor/manifest";
1
+ import { BundleManifestError, decodeBundleManifest } from "@tsdoctor/manifest";
2
2
  import { PackageJsonFile } from "@effected/package-json";
3
3
  import { TsconfigLoader } from "@effected/tsconfig-json";
4
4
  import { Effect, FileSystem, Option, Schema } from "effect";
@@ -118,4 +118,4 @@ function readBundle(descriptor) {
118
118
  }
119
119
 
120
120
  //#endregion
121
- export { BundleLayerError, TSDOCTOR_MANIFEST_FILENAME, readApiModelInfo, readBundle };
121
+ export { BundleLayerError, readApiModelInfo, readBundle };
@@ -0,0 +1,130 @@
1
+ import { isSafeAssetPath } from "./internal/asset-path.js";
2
+ import { Effect, FileSystem, Path, Schema } from "effect";
3
+ import { imageSize } from "image-size";
4
+
5
+ //#region src/BundleAssets.ts
6
+ /**
7
+ * Publishing a resolved bundle's Open Graph images into a site's public
8
+ * directory.
9
+ *
10
+ * @remarks
11
+ * `resolveBundle` produces `ResolvedOpenGraphImage` values whose `path` field
12
+ * (when present) is bundle-relative — a location inside the bundle directory,
13
+ * not the site's own output tree. An adapter must copy that file somewhere a
14
+ * built site actually serves and hand back an absolute URL a `<head>` tag can
15
+ * use; `url` images need no copy and pass straight through.
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+ /**
20
+ * A bundle-relative Open Graph image could not be published: the source file
21
+ * could not be read, or the destination could not be written.
22
+ *
23
+ * @public
24
+ */
25
+ var BundleAssetError = class extends Schema.TaggedError()("BundleAssetError", {
26
+ /** The source or destination path at fault. */
27
+ path: Schema.String,
28
+ cause: Schema.Defect()
29
+ }) {
30
+ get message() {
31
+ return `Could not publish bundle asset ${this.path}`;
32
+ }
33
+ };
34
+ const bytesEqual = (a, b) => a.byteLength === b.byteLength && a.every((value, index) => value === b[index]);
35
+ /** `imageSize` throws on bytes it cannot parse; that degrades to no measurement, never a failure. */
36
+ function safeSize(bytes) {
37
+ try {
38
+ const size = imageSize(bytes);
39
+ return {
40
+ ...size.width !== void 0 ? { width: size.width } : {},
41
+ ...size.height !== void 0 ? { height: size.height } : {}
42
+ };
43
+ } catch {
44
+ return;
45
+ }
46
+ }
47
+ /**
48
+ * Copy every bundle-relative image into `<publicDir>/tsdoctor/<unscopedName>/`
49
+ * and return every image — bundle-relative or external — as a
50
+ * {@link PublishedOpenGraphImage} carrying an absolute URL.
51
+ *
52
+ * @remarks
53
+ * Identical bytes are not rewritten, so a rebuild over an unchanged image
54
+ * leaves the published file's own metadata (mtime and any framework cache
55
+ * keyed on it) untouched. Width and height are read from the resolved image
56
+ * when the manifest declared them, and measured from the file's bytes only
57
+ * when it did not.
58
+ *
59
+ * @public
60
+ */
61
+ const publishBundleAssets = Effect.fn("publishBundleAssets")(function* (input) {
62
+ const fs = yield* FileSystem.FileSystem;
63
+ const path = yield* Path.Path;
64
+ const out = [];
65
+ for (const image of input.images) {
66
+ const common = {
67
+ ...image.type !== void 0 ? { type: image.type.value } : {},
68
+ ...image.width !== void 0 ? { width: image.width } : {},
69
+ ...image.height !== void 0 ? { height: image.height } : {},
70
+ alt: image.alt.value
71
+ };
72
+ if (image.url !== void 0) {
73
+ out.push({
74
+ url: image.url,
75
+ ...common
76
+ });
77
+ continue;
78
+ }
79
+ if (image.path === void 0) return yield* Effect.fail(new BundleAssetError({
80
+ path: input.bundleDir,
81
+ cause: "image has no path or url"
82
+ }));
83
+ if (!isSafeAssetPath(image.path)) return yield* Effect.fail(new BundleAssetError({
84
+ path: image.path,
85
+ cause: `openGraph image path escapes the bundle: "${image.path}"`
86
+ }));
87
+ const source = path.join(input.bundleDir, image.path);
88
+ const bytes = yield* fs.readFile(source).pipe(Effect.mapError((cause) => new BundleAssetError({
89
+ path: source,
90
+ cause
91
+ })));
92
+ const basename = path.basename(source);
93
+ const destSegments = [
94
+ input.publicDir,
95
+ "tsdoctor",
96
+ input.unscopedName,
97
+ ...input.subdir ? [input.subdir] : []
98
+ ];
99
+ const destDir = path.join(...destSegments);
100
+ const dest = path.join(destDir, basename);
101
+ const existing = yield* fs.readFile(dest).pipe(Effect.option);
102
+ if (existing._tag === "None" || !bytesEqual(existing.value, bytes)) {
103
+ yield* fs.makeDirectory(destDir, { recursive: true }).pipe(Effect.mapError((cause) => new BundleAssetError({
104
+ path: destDir,
105
+ cause
106
+ })));
107
+ yield* fs.writeFile(dest, bytes).pipe(Effect.mapError((cause) => new BundleAssetError({
108
+ path: dest,
109
+ cause
110
+ })));
111
+ }
112
+ const measured = image.width === void 0 || image.height === void 0 ? safeSize(bytes) : void 0;
113
+ const urlSegments = [
114
+ "tsdoctor",
115
+ input.unscopedName,
116
+ ...input.subdir ? [input.subdir] : [],
117
+ basename
118
+ ];
119
+ out.push({
120
+ url: `${input.siteUrl}/${urlSegments.join("/")}`,
121
+ ...image.width === void 0 && measured?.width !== void 0 ? { width: measured.width } : {},
122
+ ...image.height === void 0 && measured?.height !== void 0 ? { height: measured.height } : {},
123
+ ...common
124
+ });
125
+ }
126
+ return out;
127
+ });
128
+
129
+ //#endregion
130
+ export { BundleAssetError, publishBundleAssets };
@@ -1,4 +1,5 @@
1
- import { TSDOCTOR_MANIFEST_FILENAME, readApiModelInfo, readBundle } from "./Bundle.js";
1
+ import { readApiModelInfo, readBundle } from "./Bundle.js";
2
+ import { TSDOCTOR_MANIFEST_FILENAME } from "@tsdoctor/manifest";
2
3
  import { LenientManifest } from "@effected/package-json";
3
4
  import { Effect, FileSystem, Option, Path, Schema } from "effect";
4
5
  import { GlobPatternOptions } from "@effected/glob";
package/BundleFetch.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { readBundle } from "./Bundle.js";
2
+ import { isSafeAssetPath } from "./internal/asset-path.js";
2
3
  import { discoverBundle } from "./BundleDiscovery.js";
4
+ import { decodeBundleManifest } from "@tsdoctor/manifest";
3
5
  import { Effect, FileSystem, Option, Path, Schema } from "effect";
4
6
  import { GitHubRelease, Repo, RepoRef } from "@effected/github";
5
7
  import { NpmRegistry, PackageTarball, PublishedVersion } from "@effected/npm";
@@ -31,6 +33,7 @@ var BundleFetchError = class extends Schema.TaggedError()("BundleFetchError", {
31
33
  "assetAmbiguous",
32
34
  "download",
33
35
  "notABundle",
36
+ "missingAsset",
34
37
  "cache"
35
38
  ]),
36
39
  /** The remote coordinate: `name@version` or `owner/repo@tag#asset`. */
@@ -68,6 +71,21 @@ function validateSegments(source, ref, segments) {
68
71
  }));
69
72
  return Effect.void;
70
73
  }
74
+ /**
75
+ * The bundle-relative `openGraph.images[].path` entries a manifest declares,
76
+ * when the manifest at `manifestPath` exists and decodes. A manifest that
77
+ * fails to parse or decode here degrades to no assets — `readBundle` at the
78
+ * end of {@link persistAndRead}'s caller re-reads the cached manifest and
79
+ * surfaces the typed `BundleManifestError` itself; this probe must not fail
80
+ * the fetch on its own.
81
+ */
82
+ function manifestImagePaths(manifestPath) {
83
+ return Effect.gen(function* () {
84
+ const text = yield* (yield* FileSystem.FileSystem).readFileString(manifestPath);
85
+ const parsed = yield* Effect.try(() => JSON.parse(text));
86
+ return ((yield* decodeBundleManifest(parsed, manifestPath)).openGraph?.images ?? []).map((image) => image.path).filter((imagePath) => imagePath !== void 0);
87
+ }).pipe(Effect.orElseSucceed(() => []));
88
+ }
71
89
  /** The durable cache directory for one remote bundle coordinate. */
72
90
  function bundleCacheDir(segments) {
73
91
  return Effect.gen(function* () {
@@ -134,6 +152,26 @@ function persistAndRead(source, ref, key, extractedDir, cacheDir) {
134
152
  yield* fs.copyFile(layerPath, path.join(cacheDir, fileName)).pipe(Effect.mapError(failCache));
135
153
  files.push(fileName);
136
154
  }
155
+ const assetPaths = discovered.manifestPath !== void 0 ? yield* manifestImagePaths(discovered.manifestPath) : [];
156
+ for (const assetPath of assetPaths) {
157
+ if (!isSafeAssetPath(assetPath)) return yield* Effect.fail(new BundleFetchError({
158
+ source,
159
+ reason: "invalidRef",
160
+ ref,
161
+ detail: `openGraph image path escapes the bundle: "${assetPath}"`
162
+ }));
163
+ const assetSource = path.join(extractedDir, assetPath);
164
+ if (!(yield* fs.exists(assetSource).pipe(Effect.orElseSucceed(() => false)))) return yield* Effect.fail(new BundleFetchError({
165
+ source,
166
+ reason: "missingAsset",
167
+ ref,
168
+ detail: `openGraph image declared at "${assetPath}" is not present in the fetched bundle`
169
+ }));
170
+ const assetDest = path.join(cacheDir, assetPath);
171
+ yield* fs.makeDirectory(path.dirname(assetDest), { recursive: true }).pipe(Effect.mapError(failCache));
172
+ yield* fs.copyFile(assetSource, assetDest).pipe(Effect.mapError(failCache));
173
+ files.push(assetPath);
174
+ }
137
175
  yield* cache.set({
138
176
  key,
139
177
  value: utf8Encode(JSON.stringify({ files })),
package/index.d.ts CHANGED
@@ -457,6 +457,283 @@ declare function readApiModelInfo(modelPath: string): Effect.Effect<ApiModelInfo
457
457
  */
458
458
  declare function readBundle(descriptor: BundleDescriptor): Effect.Effect<Bundle, BundleLayerError | BundleManifestError, FileSystem.FileSystem | Path.Path>;
459
459
  //#endregion
460
+ //#region src/PlatformOverrides.d.ts
461
+ /**
462
+ * The `manifest.platform` tier: a data-override object a consumer passes
463
+ * through platform options (e.g. `ApiExtractorPlugin(options)`), sitting at
464
+ * the TOP of the tier ranking.
465
+ *
466
+ * @remarks
467
+ * Same field surface as the authored manifest tiers — name, tagline,
468
+ * description, openGraph, sbom, registries — with no `spec` field (it is not
469
+ * a file with an independent version) and no `project` block (it is a single
470
+ * tier, not a flattened hierarchy). Lets a user with ONLY an api.json declare
471
+ * identity/OG/registries declaratively; the resolver does the merging.
472
+ *
473
+ * @public
474
+ */
475
+ declare const PlatformOverrides: Schema.Struct<{
476
+ /** Human display name override. */
477
+ readonly name: Schema.optionalKey<Schema.String>;
478
+ /** Tagline override. */
479
+ readonly tagline: Schema.optionalKey<Schema.String>;
480
+ /** Description override. */
481
+ readonly description: Schema.optionalKey<Schema.String>;
482
+ /** Open Graph override. */
483
+ readonly openGraph: Schema.optionalKey<Schema.Struct<{
484
+ readonly images: Schema.optionalKey<Schema.$Array<Schema.Struct<{
485
+ readonly path: Schema.optionalKey<Schema.String>;
486
+ readonly url: Schema.optionalKey<Schema.String>;
487
+ readonly type: Schema.optionalKey<Schema.String>;
488
+ readonly width: Schema.optionalKey<Schema.Int>;
489
+ readonly height: Schema.optionalKey<Schema.Int>;
490
+ readonly alt: Schema.optionalKey<Schema.String>;
491
+ }>>>;
492
+ readonly themeColor: Schema.optionalKey<Schema.String>;
493
+ }>>;
494
+ /** SBOM pointer override. */
495
+ readonly sbom: Schema.optionalKey<Schema.Struct<{
496
+ readonly path: Schema.String;
497
+ readonly format: Schema.optionalKey<Schema.String>;
498
+ }>>;
499
+ /** Registries override. */
500
+ readonly registries: Schema.optionalKey<Schema.$Array<Schema.Struct<{
501
+ readonly type: Schema.String;
502
+ readonly name: Schema.String;
503
+ readonly url: Schema.String;
504
+ }>>>;
505
+ }>;
506
+ /**
507
+ * The decoded type of {@link (PlatformOverrides:variable)}.
508
+ *
509
+ * @public
510
+ */
511
+ type PlatformOverrides = typeof PlatformOverrides.Type;
512
+ /**
513
+ * Decode an unknown value into a {@link (PlatformOverrides:type)}.
514
+ *
515
+ * @remarks
516
+ * For adapters decoding raw platform options. Failures share
517
+ * {@link BundleManifestError} — the platform tier is manifest data by another
518
+ * route, and a caller handles both boundaries with one tag.
519
+ *
520
+ * @public
521
+ */
522
+ declare function decodePlatformOverrides(input: unknown): Effect.Effect<PlatformOverrides, BundleManifestError>;
523
+ //#endregion
524
+ //#region src/BundleResolver.d.ts
525
+ /**
526
+ * Where a resolved field's value came from, highest-ranked tier first.
527
+ *
528
+ * @remarks
529
+ * The first six values are the spec's tier ladder. `"tsconfig"` is this
530
+ * package's one addition: the spec passes tsconfig compiler options through
531
+ * as a resolved field but its ladder has no source that names the tsconfig
532
+ * layer, so the union carries one.
533
+ *
534
+ * @public
535
+ */
536
+ type ProvenanceSource = "manifest.platform" | "manifest.leaf" | "manifest.project" | "packageJson" | "apiModel" | "tsconfig" | "inferred";
537
+ /**
538
+ * A resolved value carrying its provenance.
539
+ *
540
+ * @remarks
541
+ * Provenance is load-bearing: a field is user-overridden iff its source
542
+ * outranks the derivation that would otherwise supply it, an `inferred`
543
+ * field tracks upstream changes while an authored field is pinned, and the
544
+ * change-detection fingerprints hash value AND source together so an
545
+ * override flip is a visible diff.
546
+ *
547
+ * @public
548
+ */
549
+ interface Provenanced<A> {
550
+ /** The resolved value. */
551
+ readonly value: A;
552
+ /** The tier that supplied it. */
553
+ readonly source: ProvenanceSource;
554
+ }
555
+ /**
556
+ * One Open Graph image after resolution: authored fields passed through,
557
+ * `type` and `alt` filled by the documented inference rules when absent.
558
+ *
559
+ * @public
560
+ */
561
+ interface ResolvedOpenGraphImage {
562
+ /** Bundle-relative asset path, when the image is bundle-supplied. */
563
+ readonly path?: string;
564
+ /** Absolute external URL, when the image is external. */
565
+ readonly url?: string;
566
+ /** MIME type — authored, or inferred from the file extension. */
567
+ readonly type?: Provenanced<string>;
568
+ /** Pixel width, as authored. */
569
+ readonly width?: number;
570
+ /** Pixel height, as authored. */
571
+ readonly height?: number;
572
+ /** Alt text — authored, or inferred (tagline → description → `"<name> API documentation"`); never empty. */
573
+ readonly alt: Provenanced<string>;
574
+ }
575
+ /**
576
+ * The Open Graph block after resolution.
577
+ *
578
+ * @public
579
+ */
580
+ interface ResolvedOpenGraph {
581
+ /** Resolved images, first-declared-wins per OG array semantics. */
582
+ readonly images: ReadonlyArray<ResolvedOpenGraphImage>;
583
+ /** Embed accent color, when authored. */
584
+ readonly themeColor?: string;
585
+ }
586
+ /**
587
+ * A bundle's manifest data resolved across the six tiers, every field
588
+ * carrying value + provenance.
589
+ *
590
+ * @remarks
591
+ * Fields that no tier supplies are absent — with two floors: `name` always
592
+ * resolves (the api.json model always has one) and every resolved image's
593
+ * `alt` always resolves (the inference chain bottoms out on `name`).
594
+ *
595
+ * @public
596
+ */
597
+ interface ResolvedBundle {
598
+ /** Display name: platform → leaf manifest → package.json → api.json model. */
599
+ readonly name: Provenanced<string>;
600
+ /** Package version, from package.json. */
601
+ readonly version?: Provenanced<string>;
602
+ /** Tagline: platform → leaf manifest → project tier. */
603
+ readonly tagline?: Provenanced<string>;
604
+ /** Description: platform → leaf manifest → package.json. */
605
+ readonly description?: Provenanced<string>;
606
+ /** The project identity block, when the manifest carries one. */
607
+ readonly project?: Provenanced<ProjectIdentity>;
608
+ /** Open Graph block: platform → leaf manifest, with per-image inference applied. */
609
+ readonly openGraph?: Provenanced<ResolvedOpenGraph>;
610
+ /** SBOM pointer: platform → leaf manifest. */
611
+ readonly sbom?: Provenanced<SbomRef>;
612
+ /** Registries: platform → leaf manifest. */
613
+ readonly registries?: Provenanced<ReadonlyArray<RegistryRef>>;
614
+ /** Runtime dependencies from package.json, key-sorted. Feeds the type registry's rendering scope. */
615
+ readonly dependencies?: Provenanced<Readonly<Record<string, string>>>;
616
+ /** Peer dependencies from package.json, key-sorted. Feeds the type registry's rendering scope. */
617
+ readonly peerDependencies?: Provenanced<Readonly<Record<string, string>>>;
618
+ /** Extends-resolved compiler options from tsconfig.json. Feeds the Twoslash environment. */
619
+ readonly compilerOptions?: Provenanced<CompilerOptions.Type>;
620
+ }
621
+ /**
622
+ * The parsed layers {@link resolveBundle} resolves — plain optional fields,
623
+ * so pure call sites (and tests) need no `Option` wrapping.
624
+ *
625
+ * @public
626
+ */
627
+ interface ResolveBundleInput {
628
+ /** Layer 0: the model header (required — the one layer a bundle must have). */
629
+ readonly apiModel: ApiModelInfo;
630
+ /** Layer 1: the package.json manifest, when present. */
631
+ readonly packageJson?: PackageManifest;
632
+ /** Layer 2: the extends-resolved tsconfig, when present. */
633
+ readonly tsconfig?: ResolvedTsconfig;
634
+ /** Layer 3: the tsdoctor.json sidecar manifest, when present. */
635
+ readonly manifest?: BundleManifest;
636
+ /** The `manifest.platform` tier, from platform options. */
637
+ readonly platform?: PlatformOverrides;
638
+ }
639
+ /**
640
+ * Resolve a bundle's layers into a {@link ResolvedBundle}, pure.
641
+ *
642
+ * @remarks
643
+ * Highest tier wins per FIELD: `manifest.platform` → `manifest.leaf` →
644
+ * `manifest.project` → `packageJson` → `apiModel` → `inferred`. The project
645
+ * tier participates only in the fields it carries site/project identity for
646
+ * (tagline); the display `name` chain deliberately skips it — a project name
647
+ * outranking every leaf's own name would render each package in a monorepo
648
+ * under the same title, and the spec's `og:title` derivation reads
649
+ * `leaf name/tagline ← package name`. Inference (image `alt` and MIME
650
+ * `type`) runs on the RESOLVED tagline/description, so a tagline change at
651
+ * any tier propagates into inferred alt text.
652
+ *
653
+ * @public
654
+ */
655
+ declare function resolveBundle(input: ResolveBundleInput): ResolvedBundle;
656
+ /**
657
+ * Resolve a read {@link Bundle}, unwrapping its `Option` layers.
658
+ *
659
+ * @public
660
+ */
661
+ declare function resolveBundleFrom(bundle: Bundle, platform?: PlatformOverrides): ResolvedBundle;
662
+ //#endregion
663
+ //#region src/BundleAssets.d.ts
664
+ declare const BundleAssetError_base: Schema.Class<BundleAssetError, Schema.TaggedStruct<"BundleAssetError", {
665
+ /** The source or destination path at fault. */
666
+ readonly path: Schema.String;
667
+ readonly cause: Schema.Defect;
668
+ }>, import("effect/Cause").YieldableError>;
669
+ /**
670
+ * A bundle-relative Open Graph image could not be published: the source file
671
+ * could not be read, or the destination could not be written.
672
+ *
673
+ * @public
674
+ */
675
+ declare class BundleAssetError extends BundleAssetError_base {
676
+ get message(): string;
677
+ }
678
+ /**
679
+ * One Open Graph image after publication: an absolute URL a `<head>` tag can
680
+ * use directly, plus whatever facts are known about it.
681
+ *
682
+ * @public
683
+ */
684
+ interface PublishedOpenGraphImage {
685
+ /** Absolute (or, when `siteUrl` is `""`, root-relative) URL. */
686
+ readonly url: string;
687
+ readonly type?: string;
688
+ readonly width?: number;
689
+ readonly height?: number;
690
+ readonly alt: string;
691
+ }
692
+ /**
693
+ * Input to {@link publishBundleAssets}.
694
+ *
695
+ * @public
696
+ */
697
+ interface PublishBundleAssetsInput {
698
+ /** The bundle directory a resolved image's `path` is relative to. */
699
+ readonly bundleDir: string;
700
+ readonly images: ReadonlyArray<ResolvedOpenGraphImage>;
701
+ /** The site's static-asset root (e.g. `docs/public`). */
702
+ readonly publicDir: string;
703
+ /** The site's absolute origin; `""` yields root-relative URLs. */
704
+ readonly siteUrl: string;
705
+ /** The route segment a package's published assets are namespaced under. */
706
+ readonly unscopedName: string;
707
+ /**
708
+ * An additional route segment inserted after `unscopedName`, for a site
709
+ * that publishes more than one build of the same package (e.g. one
710
+ * version per `VersionConfig`) under one public directory.
711
+ *
712
+ * @remarks
713
+ * Without it, two versions of the same package that both carry an
714
+ * `openGraph` image publish to the identical
715
+ * `tsdoctor/<unscopedName>/<basename>` route and overwrite each other on
716
+ * every build — differing bytes defeat the identical-bytes skip, and
717
+ * whichever version built last wins for every version's pages.
718
+ */
719
+ readonly subdir?: string;
720
+ }
721
+ /**
722
+ * Copy every bundle-relative image into `<publicDir>/tsdoctor/<unscopedName>/`
723
+ * and return every image — bundle-relative or external — as a
724
+ * {@link PublishedOpenGraphImage} carrying an absolute URL.
725
+ *
726
+ * @remarks
727
+ * Identical bytes are not rewritten, so a rebuild over an unchanged image
728
+ * leaves the published file's own metadata (mtime and any framework cache
729
+ * keyed on it) untouched. Width and height are read from the resolved image
730
+ * when the manifest declared them, and measured from the file's bytes only
731
+ * when it did not.
732
+ *
733
+ * @public
734
+ */
735
+ declare const publishBundleAssets: (input: PublishBundleAssetsInput) => Effect.Effect<readonly PublishedOpenGraphImage[], BundleAssetError, FileSystem.FileSystem | Path.Path>;
736
+ //#endregion
460
737
  //#region src/BundleDiscovery.d.ts
461
738
  declare const BundleDiscoveryError_base: Schema.Class<BundleDiscoveryError, Schema.TaggedStruct<"BundleDiscoveryError", {
462
739
  /** The directory (or file) the failure is about. */
@@ -563,7 +840,7 @@ declare const BundleFetchError_base: Schema.Class<BundleFetchError, Schema.Tagge
563
840
  /** Which fetcher failed. */
564
841
  readonly source: Schema.Literals<readonly ["npm", "github"]>;
565
842
  /** What went wrong, structurally. */
566
- readonly reason: Schema.Literals<readonly ["invalidRef", "versionNotFound", "releaseNotFound", "assetNotFound", "assetAmbiguous", "download", "notABundle", "cache"]>;
843
+ readonly reason: Schema.Literals<readonly ["invalidRef", "versionNotFound", "releaseNotFound", "assetNotFound", "assetAmbiguous", "download", "notABundle", "missingAsset", "cache"]>;
567
844
  /** The remote coordinate: `name@version` or `owner/repo@tag#asset`. */
568
845
  readonly ref: Schema.String;
569
846
  /** Human context for the failure. */
@@ -676,209 +953,6 @@ declare function fetchNpmBundle(options: FetchNpmBundleOptions): Effect.Effect<B
676
953
  */
677
954
  declare function fetchGitHubReleaseBundle(options: FetchGitHubReleaseBundleOptions): Effect.Effect<Bundle, BundleFetchError | BundleLayerError | BundleManifestError, GitHubRelease | PackageTarball | Cache | AppDirs | FileSystem.FileSystem | Path.Path>;
678
955
  //#endregion
679
- //#region src/PlatformOverrides.d.ts
680
- /**
681
- * The `manifest.platform` tier: a data-override object a consumer passes
682
- * through platform options (e.g. `ApiExtractorPlugin(options)`), sitting at
683
- * the TOP of the tier ranking.
684
- *
685
- * @remarks
686
- * Same field surface as the authored manifest tiers — name, tagline,
687
- * description, openGraph, sbom, registries — with no `spec` field (it is not
688
- * a file with an independent version) and no `project` block (it is a single
689
- * tier, not a flattened hierarchy). Lets a user with ONLY an api.json declare
690
- * identity/OG/registries declaratively; the resolver does the merging.
691
- *
692
- * @public
693
- */
694
- declare const PlatformOverrides: Schema.Struct<{
695
- /** Human display name override. */
696
- readonly name: Schema.optionalKey<Schema.String>;
697
- /** Tagline override. */
698
- readonly tagline: Schema.optionalKey<Schema.String>;
699
- /** Description override. */
700
- readonly description: Schema.optionalKey<Schema.String>;
701
- /** Open Graph override. */
702
- readonly openGraph: Schema.optionalKey<Schema.Struct<{
703
- readonly images: Schema.optionalKey<Schema.$Array<Schema.Struct<{
704
- readonly path: Schema.optionalKey<Schema.String>;
705
- readonly url: Schema.optionalKey<Schema.String>;
706
- readonly type: Schema.optionalKey<Schema.String>;
707
- readonly width: Schema.optionalKey<Schema.Int>;
708
- readonly height: Schema.optionalKey<Schema.Int>;
709
- readonly alt: Schema.optionalKey<Schema.String>;
710
- }>>>;
711
- readonly themeColor: Schema.optionalKey<Schema.String>;
712
- }>>;
713
- /** SBOM pointer override. */
714
- readonly sbom: Schema.optionalKey<Schema.Struct<{
715
- readonly path: Schema.String;
716
- readonly format: Schema.optionalKey<Schema.String>;
717
- }>>;
718
- /** Registries override. */
719
- readonly registries: Schema.optionalKey<Schema.$Array<Schema.Struct<{
720
- readonly type: Schema.String;
721
- readonly name: Schema.String;
722
- readonly url: Schema.String;
723
- }>>>;
724
- }>;
725
- /**
726
- * The decoded type of {@link (PlatformOverrides:variable)}.
727
- *
728
- * @public
729
- */
730
- type PlatformOverrides = typeof PlatformOverrides.Type;
731
- /**
732
- * Decode an unknown value into a {@link (PlatformOverrides:type)}.
733
- *
734
- * @remarks
735
- * For adapters decoding raw platform options. Failures share
736
- * {@link BundleManifestError} — the platform tier is manifest data by another
737
- * route, and a caller handles both boundaries with one tag.
738
- *
739
- * @public
740
- */
741
- declare function decodePlatformOverrides(input: unknown): Effect.Effect<PlatformOverrides, BundleManifestError>;
742
- //#endregion
743
- //#region src/BundleResolver.d.ts
744
- /**
745
- * Where a resolved field's value came from, highest-ranked tier first.
746
- *
747
- * @remarks
748
- * The first six values are the spec's tier ladder. `"tsconfig"` is this
749
- * package's one addition: the spec passes tsconfig compiler options through
750
- * as a resolved field but its ladder has no source that names the tsconfig
751
- * layer, so the union carries one.
752
- *
753
- * @public
754
- */
755
- type ProvenanceSource = "manifest.platform" | "manifest.leaf" | "manifest.project" | "packageJson" | "apiModel" | "tsconfig" | "inferred";
756
- /**
757
- * A resolved value carrying its provenance.
758
- *
759
- * @remarks
760
- * Provenance is load-bearing: a field is user-overridden iff its source
761
- * outranks the derivation that would otherwise supply it, an `inferred`
762
- * field tracks upstream changes while an authored field is pinned, and the
763
- * change-detection fingerprints hash value AND source together so an
764
- * override flip is a visible diff.
765
- *
766
- * @public
767
- */
768
- interface Provenanced<A> {
769
- /** The resolved value. */
770
- readonly value: A;
771
- /** The tier that supplied it. */
772
- readonly source: ProvenanceSource;
773
- }
774
- /**
775
- * One Open Graph image after resolution: authored fields passed through,
776
- * `type` and `alt` filled by the documented inference rules when absent.
777
- *
778
- * @public
779
- */
780
- interface ResolvedOpenGraphImage {
781
- /** Bundle-relative asset path, when the image is bundle-supplied. */
782
- readonly path?: string;
783
- /** Absolute external URL, when the image is external. */
784
- readonly url?: string;
785
- /** MIME type — authored, or inferred from the file extension. */
786
- readonly type?: Provenanced<string>;
787
- /** Pixel width, as authored. */
788
- readonly width?: number;
789
- /** Pixel height, as authored. */
790
- readonly height?: number;
791
- /** Alt text — authored, or inferred (tagline → description → `"<name> API documentation"`); never empty. */
792
- readonly alt: Provenanced<string>;
793
- }
794
- /**
795
- * The Open Graph block after resolution.
796
- *
797
- * @public
798
- */
799
- interface ResolvedOpenGraph {
800
- /** Resolved images, first-declared-wins per OG array semantics. */
801
- readonly images: ReadonlyArray<ResolvedOpenGraphImage>;
802
- /** Embed accent color, when authored. */
803
- readonly themeColor?: string;
804
- }
805
- /**
806
- * A bundle's manifest data resolved across the six tiers, every field
807
- * carrying value + provenance.
808
- *
809
- * @remarks
810
- * Fields that no tier supplies are absent — with two floors: `name` always
811
- * resolves (the api.json model always has one) and every resolved image's
812
- * `alt` always resolves (the inference chain bottoms out on `name`).
813
- *
814
- * @public
815
- */
816
- interface ResolvedBundle {
817
- /** Display name: platform → leaf manifest → package.json → api.json model. */
818
- readonly name: Provenanced<string>;
819
- /** Package version, from package.json. */
820
- readonly version?: Provenanced<string>;
821
- /** Tagline: platform → leaf manifest → project tier. */
822
- readonly tagline?: Provenanced<string>;
823
- /** Description: platform → leaf manifest → package.json. */
824
- readonly description?: Provenanced<string>;
825
- /** The project identity block, when the manifest carries one. */
826
- readonly project?: Provenanced<ProjectIdentity>;
827
- /** Open Graph block: platform → leaf manifest, with per-image inference applied. */
828
- readonly openGraph?: Provenanced<ResolvedOpenGraph>;
829
- /** SBOM pointer: platform → leaf manifest. */
830
- readonly sbom?: Provenanced<SbomRef>;
831
- /** Registries: platform → leaf manifest. */
832
- readonly registries?: Provenanced<ReadonlyArray<RegistryRef>>;
833
- /** Runtime dependencies from package.json, key-sorted. Feeds the type registry's rendering scope. */
834
- readonly dependencies?: Provenanced<Readonly<Record<string, string>>>;
835
- /** Peer dependencies from package.json, key-sorted. Feeds the type registry's rendering scope. */
836
- readonly peerDependencies?: Provenanced<Readonly<Record<string, string>>>;
837
- /** Extends-resolved compiler options from tsconfig.json. Feeds the Twoslash environment. */
838
- readonly compilerOptions?: Provenanced<CompilerOptions.Type>;
839
- }
840
- /**
841
- * The parsed layers {@link resolveBundle} resolves — plain optional fields,
842
- * so pure call sites (and tests) need no `Option` wrapping.
843
- *
844
- * @public
845
- */
846
- interface ResolveBundleInput {
847
- /** Layer 0: the model header (required — the one layer a bundle must have). */
848
- readonly apiModel: ApiModelInfo;
849
- /** Layer 1: the package.json manifest, when present. */
850
- readonly packageJson?: PackageManifest;
851
- /** Layer 2: the extends-resolved tsconfig, when present. */
852
- readonly tsconfig?: ResolvedTsconfig;
853
- /** Layer 3: the tsdoctor.json sidecar manifest, when present. */
854
- readonly manifest?: BundleManifest;
855
- /** The `manifest.platform` tier, from platform options. */
856
- readonly platform?: PlatformOverrides;
857
- }
858
- /**
859
- * Resolve a bundle's layers into a {@link ResolvedBundle}, pure.
860
- *
861
- * @remarks
862
- * Highest tier wins per FIELD: `manifest.platform` → `manifest.leaf` →
863
- * `manifest.project` → `packageJson` → `apiModel` → `inferred`. The project
864
- * tier participates only in the fields it carries site/project identity for
865
- * (tagline); the display `name` chain deliberately skips it — a project name
866
- * outranking every leaf's own name would render each package in a monorepo
867
- * under the same title, and the spec's `og:title` derivation reads
868
- * `leaf name/tagline ← package name`. Inference (image `alt` and MIME
869
- * `type`) runs on the RESOLVED tagline/description, so a tagline change at
870
- * any tier propagates into inferred alt text.
871
- *
872
- * @public
873
- */
874
- declare function resolveBundle(input: ResolveBundleInput): ResolvedBundle;
875
- /**
876
- * Resolve a read {@link Bundle}, unwrapping its `Option` layers.
877
- *
878
- * @public
879
- */
880
- declare function resolveBundleFrom(bundle: Bundle, platform?: PlatformOverrides): ResolvedBundle;
881
- //#endregion
882
956
  //#region src/BundleHash.d.ts
883
957
  /**
884
958
  * Normalize text for hashing: CRLF/CR line endings become LF and trailing
@@ -955,5 +1029,5 @@ declare function hashLayerText(text: string): Effect.Effect<string, PlatformErro
955
1029
  */
956
1030
  declare function fingerprintResolvedBundle(resolved: ResolvedBundle): Effect.Effect<Readonly<Record<string, string>>, PlatformError.PlatformError, Crypto.Crypto>;
957
1031
  //#endregion
958
- export { type ApiModelInfo, type Bundle, type BundleDescriptor, BundleDiscoveryError, BundleFetchError, BundleLayerError, BundleManifest, BundleManifestError, type BundleOverrides, type DiscoverBundleOptions, type DiscoverBundlesOptions, type FetchGitHubReleaseBundleOptions, type FetchNpmBundleOptions, KNOWN_REGISTRY_TYPES, type KnownRegistryType, MANIFEST_SPEC, ManifestSource, OpenGraphConfig, OpenGraphImage, PlatformOverrides, ProjectIdentity, type ProvenanceSource, type Provenanced, RegistryRef, type ResolveBundleInput, type ResolvedBundle, type ResolvedOpenGraph, type ResolvedOpenGraphImage, SbomRef, TSDOCTOR_MANIFEST_FILENAME, decodeBundleManifest, decodeManifestSource, decodePlatformOverrides, discoverBundle, discoverBundles, encodeBundleManifest, fetchGitHubReleaseBundle, fetchNpmBundle, fingerprintResolvedBundle, hashJsonValue, hashLayerText, hashText, isKnownRegistryType, loadBundle, loadBundles, normalizeText, readApiModelInfo, readBundle, resolveBundle, resolveBundleFrom };
1032
+ export { type ApiModelInfo, type Bundle, BundleAssetError, type BundleDescriptor, BundleDiscoveryError, BundleFetchError, BundleLayerError, BundleManifest, BundleManifestError, type BundleOverrides, type DiscoverBundleOptions, type DiscoverBundlesOptions, type FetchGitHubReleaseBundleOptions, type FetchNpmBundleOptions, KNOWN_REGISTRY_TYPES, type KnownRegistryType, MANIFEST_SPEC, ManifestSource, OpenGraphConfig, OpenGraphImage, PlatformOverrides, ProjectIdentity, type ProvenanceSource, type Provenanced, type PublishBundleAssetsInput, type PublishedOpenGraphImage, RegistryRef, type ResolveBundleInput, type ResolvedBundle, type ResolvedOpenGraph, type ResolvedOpenGraphImage, SbomRef, TSDOCTOR_MANIFEST_FILENAME, decodeBundleManifest, decodeManifestSource, decodePlatformOverrides, discoverBundle, discoverBundles, encodeBundleManifest, fetchGitHubReleaseBundle, fetchNpmBundle, fingerprintResolvedBundle, hashJsonValue, hashLayerText, hashText, isKnownRegistryType, loadBundle, loadBundles, normalizeText, publishBundleAssets, readApiModelInfo, readBundle, resolveBundle, resolveBundleFrom };
959
1033
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { BundleLayerError, readApiModelInfo, readBundle } from "./Bundle.js";
2
+ import { BundleAssetError, publishBundleAssets } from "./BundleAssets.js";
2
3
  import { BundleDiscoveryError, discoverBundle, discoverBundles, loadBundle, loadBundles } from "./BundleDiscovery.js";
3
4
  import { BundleFetchError, fetchGitHubReleaseBundle, fetchNpmBundle } from "./BundleFetch.js";
4
5
  import { fingerprintResolvedBundle, hashJsonValue, hashLayerText, hashText, normalizeText } from "./BundleHash.js";
@@ -6,4 +7,4 @@ import { resolveBundle, resolveBundleFrom } from "./BundleResolver.js";
6
7
  import { PlatformOverrides, decodePlatformOverrides } from "./PlatformOverrides.js";
7
8
  import { BundleManifest, BundleManifestError, KNOWN_REGISTRY_TYPES, MANIFEST_SPEC, ManifestSource, OpenGraphConfig, OpenGraphImage, ProjectIdentity, RegistryRef, SbomRef, TSDOCTOR_MANIFEST_FILENAME, decodeBundleManifest, decodeManifestSource, encodeBundleManifest, isKnownRegistryType } from "@tsdoctor/manifest";
8
9
 
9
- export { BundleDiscoveryError, BundleFetchError, BundleLayerError, BundleManifest, BundleManifestError, KNOWN_REGISTRY_TYPES, MANIFEST_SPEC, ManifestSource, OpenGraphConfig, OpenGraphImage, PlatformOverrides, ProjectIdentity, RegistryRef, SbomRef, TSDOCTOR_MANIFEST_FILENAME, decodeBundleManifest, decodeManifestSource, decodePlatformOverrides, discoverBundle, discoverBundles, encodeBundleManifest, fetchGitHubReleaseBundle, fetchNpmBundle, fingerprintResolvedBundle, hashJsonValue, hashLayerText, hashText, isKnownRegistryType, loadBundle, loadBundles, normalizeText, readApiModelInfo, readBundle, resolveBundle, resolveBundleFrom };
10
+ export { BundleAssetError, BundleDiscoveryError, BundleFetchError, BundleLayerError, BundleManifest, BundleManifestError, KNOWN_REGISTRY_TYPES, MANIFEST_SPEC, ManifestSource, OpenGraphConfig, OpenGraphImage, PlatformOverrides, ProjectIdentity, RegistryRef, SbomRef, TSDOCTOR_MANIFEST_FILENAME, decodeBundleManifest, decodeManifestSource, decodePlatformOverrides, discoverBundle, discoverBundles, encodeBundleManifest, fetchGitHubReleaseBundle, fetchNpmBundle, fingerprintResolvedBundle, hashJsonValue, hashLayerText, hashText, isKnownRegistryType, loadBundle, loadBundles, normalizeText, publishBundleAssets, readApiModelInfo, readBundle, resolveBundle, resolveBundleFrom };
@@ -0,0 +1,29 @@
1
+ //#region src/internal/asset-path.ts
2
+ /**
3
+ * Shared traversal guard for bundle-relative asset paths.
4
+ *
5
+ * @remarks
6
+ * Not exported from `index.ts` — internal to the package. Both the fetch
7
+ * plane ({@link "../BundleFetch.js"}) and the publish plane
8
+ * ({@link "../BundleAssets.js"}) read a manifest-declared, bundle-relative
9
+ * `openGraph.images[].path` and must reject the same shapes before touching
10
+ * the filesystem: no absolute paths, no `.`/`..` traversal segments, no
11
+ * separators hiding inside a segment.
12
+ *
13
+ * @internal
14
+ */
15
+ const SAFE_SEGMENT = /^(?!\.{1,2}$)[^/\\\s]+$/;
16
+ /**
17
+ * Whether `assetPath` is a relative path confined to the bundle directory —
18
+ * no leading slash, no `.`/`..` segment, no unsafe characters in any
19
+ * segment.
20
+ *
21
+ * @internal
22
+ */
23
+ function isSafeAssetPath(assetPath) {
24
+ if (assetPath.length === 0 || assetPath.startsWith("/") || assetPath.startsWith("\\")) return false;
25
+ return assetPath.split(/[/\\]/).every((segment) => SAFE_SEGMENT.test(segment));
26
+ }
27
+
28
+ //#endregion
29
+ export { isSafeAssetPath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsdoctor/bundle",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "The tsdoctor bundle spec: layered bundle discovery, the versioned tsdoctor.json sidecar manifest, provenance-carrying resolution and canonical input hashing for API documentation bundles.",
6
6
  "keywords": [
@@ -49,7 +49,8 @@
49
49
  "@effected/walker": "^0.5.0",
50
50
  "@effected/xdg": "^0.3.0",
51
51
  "@tsdoctor/manifest": "0.1.0",
52
- "effect": "4.0.0-rc.109"
52
+ "effect": "4.0.0-rc.109",
53
+ "image-size": "^2.0.2"
53
54
  },
54
55
  "engines": {
55
56
  "node": ">=24.11.0"