@grimoire-rs/indexer 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +220 -9
  2. package/dist/ci.d.ts +6 -1
  3. package/dist/ci.d.ts.map +1 -1
  4. package/dist/ci.js +32 -1
  5. package/dist/ci.js.map +1 -1
  6. package/dist/cli/build.d.ts.map +1 -1
  7. package/dist/cli/build.js +3 -1
  8. package/dist/cli/build.js.map +1 -1
  9. package/dist/cli/ci.d.ts.map +1 -1
  10. package/dist/cli/ci.js +5 -1
  11. package/dist/cli/ci.js.map +1 -1
  12. package/dist/cli/enrich.d.ts +1 -0
  13. package/dist/cli/enrich.d.ts.map +1 -1
  14. package/dist/cli/enrich.js +38 -0
  15. package/dist/cli/enrich.js.map +1 -1
  16. package/dist/cli/init.d.ts.map +1 -1
  17. package/dist/cli/init.js +17 -3
  18. package/dist/cli/init.js.map +1 -1
  19. package/dist/cli/main.d.ts.map +1 -1
  20. package/dist/cli/main.js +9 -0
  21. package/dist/cli/main.js.map +1 -1
  22. package/dist/cli/ratings.d.ts +3 -0
  23. package/dist/cli/ratings.d.ts.map +1 -0
  24. package/dist/cli/ratings.js +160 -0
  25. package/dist/cli/ratings.js.map +1 -0
  26. package/dist/config.d.ts +19 -0
  27. package/dist/config.d.ts.map +1 -1
  28. package/dist/config.js +47 -1
  29. package/dist/config.js.map +1 -1
  30. package/dist/data/index.d.ts.map +1 -1
  31. package/dist/data/index.js +13 -0
  32. package/dist/data/index.js.map +1 -1
  33. package/dist/enrich/checkpoint.d.ts +53 -0
  34. package/dist/enrich/checkpoint.d.ts.map +1 -0
  35. package/dist/enrich/checkpoint.js +296 -0
  36. package/dist/enrich/checkpoint.js.map +1 -0
  37. package/dist/enrich/index.d.ts +14 -0
  38. package/dist/enrich/index.d.ts.map +1 -1
  39. package/dist/enrich/index.js +77 -4
  40. package/dist/enrich/index.js.map +1 -1
  41. package/dist/ratings/budget.d.ts +16 -0
  42. package/dist/ratings/budget.d.ts.map +1 -0
  43. package/dist/ratings/budget.js +7 -0
  44. package/dist/ratings/budget.js.map +1 -0
  45. package/dist/ratings/config.d.ts +56 -0
  46. package/dist/ratings/config.d.ts.map +1 -0
  47. package/dist/ratings/config.js +86 -0
  48. package/dist/ratings/config.js.map +1 -0
  49. package/dist/ratings/marker.d.ts +105 -0
  50. package/dist/ratings/marker.d.ts.map +1 -0
  51. package/dist/ratings/marker.js +141 -0
  52. package/dist/ratings/marker.js.map +1 -0
  53. package/dist/ratings/paging.d.ts +16 -0
  54. package/dist/ratings/paging.d.ts.map +1 -0
  55. package/dist/ratings/paging.js +36 -0
  56. package/dist/ratings/paging.js.map +1 -0
  57. package/dist/ratings/provider.d.ts +100 -0
  58. package/dist/ratings/provider.d.ts.map +1 -0
  59. package/dist/ratings/provider.js +203 -0
  60. package/dist/ratings/provider.js.map +1 -0
  61. package/dist/ratings/provider_github.d.ts +3 -0
  62. package/dist/ratings/provider_github.d.ts.map +1 -0
  63. package/dist/ratings/provider_github.js +135 -0
  64. package/dist/ratings/provider_github.js.map +1 -0
  65. package/dist/ratings/provider_gitlab.d.ts +3 -0
  66. package/dist/ratings/provider_gitlab.d.ts.map +1 -0
  67. package/dist/ratings/provider_gitlab.js +153 -0
  68. package/dist/ratings/provider_gitlab.js.map +1 -0
  69. package/dist/ratings/provider_memory.d.ts +28 -0
  70. package/dist/ratings/provider_memory.d.ts.map +1 -0
  71. package/dist/ratings/provider_memory.js +49 -0
  72. package/dist/ratings/provider_memory.js.map +1 -0
  73. package/dist/ratings/reconcile.d.ts +47 -0
  74. package/dist/ratings/reconcile.d.ts.map +1 -0
  75. package/dist/ratings/reconcile.js +133 -0
  76. package/dist/ratings/reconcile.js.map +1 -0
  77. package/dist/ratings/seed.d.ts +60 -0
  78. package/dist/ratings/seed.d.ts.map +1 -0
  79. package/dist/ratings/seed.js +132 -0
  80. package/dist/ratings/seed.js.map +1 -0
  81. package/dist/renderer/astro/components/Catalog.d.ts +2 -0
  82. package/dist/renderer/astro/components/Catalog.js +65 -16
  83. package/dist/renderer/astro/components/Catalog.tsx +119 -26
  84. package/dist/renderer/astro/components/CommandField.astro +1 -0
  85. package/dist/renderer/astro/components/VersionMenu.astro +31 -23
  86. package/dist/renderer/astro/layouts/Base.astro +218 -263
  87. package/dist/renderer/astro/lib/catalog.d.ts +25 -0
  88. package/dist/renderer/astro/lib/catalog.js +46 -0
  89. package/dist/renderer/astro/lib/catalog.ts +49 -0
  90. package/dist/renderer/astro/pages/p/[...slug].astro +238 -85
  91. package/dist/renderer/astro/styles/tokens.css +157 -0
  92. package/dist/renderer/index.d.ts.map +1 -1
  93. package/dist/renderer/index.js +101 -2
  94. package/dist/renderer/index.js.map +1 -1
  95. package/dist/renderer/types.d.ts +41 -0
  96. package/dist/renderer/types.d.ts.map +1 -1
  97. package/dist/validate/adapters/http.d.ts +29 -1
  98. package/dist/validate/adapters/http.d.ts.map +1 -1
  99. package/dist/validate/adapters/http.js +17 -2
  100. package/dist/validate/adapters/http.js.map +1 -1
  101. package/package.json +1 -1
  102. package/templates/ci/github-enrich.yml +9 -1
  103. package/templates/ci/github-pages.yml +11 -5
  104. package/templates/ci/github-ratings-seed.yml +88 -0
  105. package/templates/ci/github-ratings.yml +51 -0
  106. package/templates/ci/gitlab-ci.yml +3 -3
  107. package/templates/ci/gitlab-enrich.sh +10 -1
  108. package/templates/ci/gitlab-ratings-seed.sh +83 -0
  109. package/templates/ci/gitlab-ratings.yml +37 -0
  110. package/templates/gitignore +8 -0
@@ -1,11 +1,11 @@
1
1
  import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
2
2
  // Lucide (ISC) draws the UI; brand marks come from `@mdi/js`, which Lucide
3
3
  // deliberately does not carry. No SVG on this site is hand-written.
4
- import { Check, FolderRoot, Globe, Image, ImageOff } from "lucide-preact";
4
+ import { ArrowBigUp, Check, FolderRoot, Globe, Image, ImageOff } from "lucide-preact";
5
5
  import { mdiMicrosoftVisualStudioCode } from "@mdi/js";
6
6
  import { BrandMark } from "./BrandMark.js";
7
7
  import { withBase } from "../lib/base.js";
8
- import { timeAgo, vscodeUrl, type CatalogPackage } from "../lib/catalog.js";
8
+ import { lastUpdated, timeAgo, vscodeUrl, type CatalogPackage } from "../lib/catalog.js";
9
9
 
10
10
  // Known kinds get stable chip ordering + badge colors; unknown kinds
11
11
  // (future schema growth) still render with a neutral badge.
@@ -16,16 +16,70 @@ function kindOrder(kind: string): number {
16
16
  return i === -1 ? KNOWN_KINDS.length : i;
17
17
  }
18
18
 
19
- type Sort = "name" | "updated";
19
+ export type Sort = "name" | "updated" | "rating";
20
20
 
21
- // Deprecated packages sink to the bottom regardless of sort mode; the
22
- // chosen sort only orders within the two groups.
23
- function compare(a: CatalogPackage, b: CatalogPackage, sort: Sort): number {
24
- const dep = Number(!!a.deprecated) - Number(!!b.deprecated);
25
- if (dep !== 0) return dep;
26
- if (sort === "name") return a.name.localeCompare(b.name);
27
- if (!a.created || !b.created) return (a.created ? 0 : 1) - (b.created ? 0 : 1);
28
- return new Date(b.created).getTime() - new Date(a.created).getTime();
21
+ type Key = (a: CatalogPackage, b: CatalogPackage) => number;
22
+
23
+ /**
24
+ * Bigger first, with `null` as its own bucket underneath every number.
25
+ *
26
+ * The shared shape of the two ranked keys, and the reason both are written
27
+ * this way: "missing" is not a low value, it is the absence of one. Folding
28
+ * it into a number — 0 upvotes, epoch 0 — orders those rows against real
29
+ * data by accident, and ties them all with each other.
30
+ */
31
+ function descending(a: number | null, b: number | null): number {
32
+ if (a === null || b === null) return Number(a === null) - Number(b === null);
33
+ return b - a;
34
+ }
35
+
36
+ /** `updated` as epoch ms; null when absent, empty or not a date at all. */
37
+ function updatedAt(p: CatalogPackage): number | null {
38
+ const at = lastUpdated(p);
39
+ const ms = at ? new Date(at).getTime() : NaN;
40
+ return Number.isFinite(ms) ? ms : null;
41
+ }
42
+
43
+ /**
44
+ * Ascending, case-insensitive. The full ref breaks the last tie in every
45
+ * mode, and it is unique, so no two rows ever compare equal — a browse order
46
+ * that is not total is a browse order that reshuffles on rebuild.
47
+ */
48
+ const byName: Key = (a, b) =>
49
+ a.name.localeCompare(b.name, undefined, { sensitivity: "accent" }) || a.ref.localeCompare(b.ref);
50
+
51
+ /**
52
+ * Newest first. No usable date is *unknown*, not epoch 0: dating an undated
53
+ * package to 1970 sorts it below real packages by accident rather than by
54
+ * rule, so it goes into a bucket of its own at the bottom.
55
+ */
56
+ const byUpdated: Key = (a, b) => descending(updatedAt(a), updatedAt(b));
57
+
58
+ /**
59
+ * Most upvotes first. Unrated is its own bucket at the bottom, never a zero:
60
+ * a fresh index is all-unrated, and zeroes would leave every one of those
61
+ * rows comparing equal with nothing left to break the tie.
62
+ */
63
+ const byRating: Key = (a, b) => descending(a.rating?.up ?? null, b.rating?.up ?? null);
64
+
65
+ /** Each mode as a chain of keys, most significant first. */
66
+ const CHAINS: Record<Sort, Key[]> = {
67
+ name: [byName],
68
+ updated: [byUpdated, byName],
69
+ rating: [byRating, byUpdated, byName],
70
+ };
71
+
72
+ // Deprecated packages get no special ordering here — they are filtered out
73
+ // of the default browse entirely (see `shown` below) and interleave like
74
+ // any other row when the toggle brings them back. grim's own browse order
75
+ // (`browse_sort.rs`) has no deprecated key either; keeping this comparator
76
+ // silent on deprecation is what keeps the two in sync.
77
+ export function compare(a: CatalogPackage, b: CatalogPackage, sort: Sort): number {
78
+ for (const key of CHAINS[sort]) {
79
+ const d = key(a, b);
80
+ if (d !== 0) return d;
81
+ }
82
+ return 0;
29
83
  }
30
84
 
31
85
  // The two install scopes previously wore the VS Code extension's own
@@ -106,7 +160,7 @@ function CardLogo({ pkg }: { pkg: CatalogPackage }) {
106
160
  <span
107
161
  class="card-logo card-logo-fallback"
108
162
  aria-hidden="true"
109
- style={{ background: `var(--kind-${pkg.kind}, var(--muted))` }}
163
+ style={{ background: `var(--grim-color-kind-${pkg.kind}, var(--grim-color-muted))` }}
110
164
  >
111
165
  {pkg.name[0]?.toUpperCase()}
112
166
  </span>
@@ -449,16 +503,18 @@ export default function Catalog({
449
503
  );
450
504
 
451
505
  // A catalog with nothing deprecated gets no toggle — a control that can
452
- // only ever be a no-op is worse than its absence.
506
+ // only ever be a no-op is worse than its absence. An index that publishes
507
+ // no ratings gets no rating chip for the same reason.
453
508
  const hasDeprecated = packages.some((p) => p.deprecated);
509
+ const hasRatings = packages.some((p) => p.rating);
454
510
 
455
511
  // Chips leave the Tab order only while there is a grid to arrow up from.
456
512
  const chipTabIndex = shown.length === 0 ? 0 : -1;
457
513
 
458
514
  return (
459
- <section class="catalog">
460
- <div class="controls" ref={controlsRef}>
461
- <div class="search-field">
515
+ <section class="catalog" data-slot="catalog">
516
+ <div class="controls" data-slot="catalog-toolbar" ref={controlsRef}>
517
+ <div class="search-field" data-slot="catalog-search">
462
518
  <input
463
519
  ref={searchRef}
464
520
  type="search"
@@ -478,6 +534,7 @@ export default function Catalog({
478
534
  <button
479
535
  type="button"
480
536
  class={sort === "name" ? "chip active" : "chip"}
537
+ data-slot="filter-chip"
481
538
  tabIndex={chipTabIndex}
482
539
  onKeyDown={onChipKeyDown}
483
540
  onClick={() => setSort("name")}
@@ -487,12 +544,25 @@ export default function Catalog({
487
544
  <button
488
545
  type="button"
489
546
  class={sort === "updated" ? "chip active" : "chip"}
547
+ data-slot="filter-chip"
490
548
  tabIndex={chipTabIndex}
491
549
  onKeyDown={onChipKeyDown}
492
550
  onClick={() => setSort("updated")}
493
551
  >
494
552
  updated
495
553
  </button>
554
+ {hasRatings && (
555
+ <button
556
+ type="button"
557
+ class={sort === "rating" ? "chip active" : "chip"}
558
+ data-slot="filter-chip"
559
+ tabIndex={chipTabIndex}
560
+ onKeyDown={onChipKeyDown}
561
+ onClick={() => setSort("rating")}
562
+ >
563
+ rating
564
+ </button>
565
+ )}
496
566
  </div>
497
567
  {/* Divides sort from filter — two different questions sharing a row.
498
568
  Decorative only: each group already carries its own aria-label,
@@ -502,6 +572,7 @@ export default function Catalog({
502
572
  <button
503
573
  type="button"
504
574
  class={kind === null ? "chip active" : "chip"}
575
+ data-slot="filter-chip"
505
576
  tabIndex={chipTabIndex}
506
577
  onKeyDown={onChipKeyDown}
507
578
  onClick={() => setKind(null)}
@@ -513,6 +584,7 @@ export default function Catalog({
513
584
  key={k}
514
585
  type="button"
515
586
  class={kind === k ? `chip active kind-${k}` : `chip kind-${k}`}
587
+ data-slot="filter-chip"
516
588
  tabIndex={chipTabIndex}
517
589
  onKeyDown={onChipKeyDown}
518
590
  onClick={() => setKind(kind === k ? null : k)}
@@ -557,32 +629,53 @@ export default function Catalog({
557
629
  <li
558
630
  key={`${p.namespace}/${p.name}`}
559
631
  class="card"
632
+ data-slot="package-card"
560
633
  tabIndex={0}
561
634
  onKeyDown={onCardKeyDown}
562
635
  >
563
636
  <div class="card-head">
564
637
  <CardLogo pkg={p} />
565
- <h2>
638
+ <h2 data-slot="package-name">
566
639
  <a href={withBase(`/p/${p.namespace}/${p.name}/`)} tabIndex={-1}>
567
640
  {p.name}
568
641
  </a>
569
642
  </h2>
570
643
  {p.deprecated ? (
571
- <span class="badge deprecated">deprecated</span>
644
+ <span class="badge deprecated" data-slot="package-kind">deprecated</span>
572
645
  ) : (
573
- <span class={`badge kind-${p.kind}`}>{p.kind}</span>
646
+ <span class={`badge kind-${p.kind}`} data-slot="package-kind">{p.kind}</span>
574
647
  )}
575
648
  </div>
576
649
  <p class="namespace">{p.namespace}</p>
577
- {(p.version || p.license || p.created) && (
578
- <div class="meta-row">
650
+ {(p.version || p.license || lastUpdated(p) || p.rating) && (
651
+ <div class="meta-row" data-slot="package-meta">
579
652
  {p.version && <span class="pill version">v{p.version}</span>}
580
653
  {p.license && <span class="pill license">{p.license}</span>}
581
- {p.created && timeAgo(p.created) && (
582
- <time class="updated" datetime={p.created} title={p.created}>
583
- updated {timeAgo(p.created)}
584
- </time>
654
+ {/* A count and nothing more. The page is prerendered once
655
+ for everyone, so it cannot know whether *you* voted —
656
+ showing "not voted" to someone who has would be worse
657
+ than showing nothing. */}
658
+ {p.rating && (
659
+ <span
660
+ class="pill rating"
661
+ title={`${p.rating.up} upvote${p.rating.up === 1 ? "" : "s"}`}
662
+ >
663
+ <ArrowBigUp size={13} aria-hidden="true" />
664
+ {p.rating.up}
665
+ </span>
585
666
  )}
667
+ {/* The date the sidecar derived, not the artifact's own
668
+ `created`: a package republished from the same commit
669
+ keeps its date, and one with no commit date at all gets
670
+ the day this index first saw its current digest. */}
671
+ {(() => {
672
+ const at = lastUpdated(p);
673
+ return at && timeAgo(at) ? (
674
+ <time class="updated" datetime={at} title={at}>
675
+ updated {timeAgo(at)}
676
+ </time>
677
+ ) : null;
678
+ })()}
586
679
  </div>
587
680
  )}
588
681
  {p.deprecated && (
@@ -593,7 +686,7 @@ export default function Catalog({
593
686
  )}
594
687
  {p.description && <p class="description">{p.description}</p>}
595
688
  {p.keywords && p.keywords.length > 0 && (
596
- <div class="keywords">
689
+ <div class="keywords" data-slot="package-keywords">
597
690
  {p.keywords.slice(0, 5).map((kw) => (
598
691
  <button
599
692
  key={kw}
@@ -25,6 +25,7 @@ const { value, label, name } = Astro.props;
25
25
  <button
26
26
  type="button"
27
27
  class="cmd-field"
28
+ data-slot="install-command"
28
29
  data-copy={value}
29
30
  data-copy-name={name}
30
31
  aria-label={label}
@@ -28,6 +28,7 @@ const commands: [string, string][] = [
28
28
  <button
29
29
  type="button"
30
30
  class:list={["version", { current }]}
31
+ data-slot="version-pill"
31
32
  data-copy={pinned}
32
33
  data-copy-name={tag === "latest" ? "reference" : `pinned ${tag}`}
33
34
  data-menu={menuId}
@@ -46,33 +47,39 @@ const commands: [string, string][] = [
46
47
  </div>
47
48
 
48
49
  <style>
50
+ @layer grimoire {
51
+ /* Layered so a user CSS file (unlayered) outranks these rules. Astro
52
+ scopes a component style to `.foo[data-astro-cid-…]` — specificity
53
+ (0,2,0) — which beats a consumer's `.foo` in EITHER source order, so
54
+ source order alone does not make this overridable. Not reindented:
55
+ the wrapper stays a two-line diff. */
49
56
  /* No `span` inside: the shared copy script only swaps a label when it
50
57
  finds one, so the tag keeps its text and flashes the class instead. */
51
58
  .version {
52
- border: 1px solid var(--border);
53
- border-radius: 999px;
59
+ border: var(--grim-border-width) solid var(--grim-color-border);
60
+ border-radius: var(--grim-radius-pill);
54
61
  background: transparent;
55
- padding: 0.15rem 0.6rem;
62
+ padding: var(--grim-space-1) var(--grim-space-4);
56
63
  font-family: ui-monospace, "SFMono-Regular", monospace;
57
- font-size: 0.85rem;
58
- color: var(--muted);
64
+ font-size: var(--grim-text-sm);
65
+ color: var(--grim-color-muted);
59
66
  white-space: nowrap;
60
67
  cursor: pointer;
61
68
  }
62
69
  .version:hover,
63
70
  .version:focus-visible {
64
- color: var(--accent);
65
- border-color: var(--accent);
66
- background: var(--accent-soft);
71
+ color: var(--grim-color-accent);
72
+ border-color: var(--grim-color-accent);
73
+ background: var(--grim-color-accent-soft);
67
74
  }
68
75
  .version.current {
69
- color: var(--fg);
76
+ color: var(--grim-color-fg);
70
77
  font-weight: 600;
71
- border-color: var(--accent);
78
+ border-color: var(--grim-color-accent);
72
79
  }
73
80
  .version.copied {
74
- color: var(--accent);
75
- background: var(--accent-soft);
81
+ color: var(--grim-color-accent);
82
+ background: var(--grim-color-accent-soft);
76
83
  }
77
84
 
78
85
  .version-menu {
@@ -82,28 +89,29 @@ const commands: [string, string][] = [
82
89
  inset: auto;
83
90
  margin: 0;
84
91
  min-width: 11rem;
85
- padding: 0.3rem;
86
- background: var(--card);
87
- border: 1px solid var(--border);
88
- border-radius: 8px;
89
- box-shadow: 0 6px 20px rgb(0 0 0 / 0.15);
92
+ padding: var(--grim-space-2);
93
+ background: var(--grim-color-card);
94
+ border: var(--grim-border-width) solid var(--grim-color-border);
95
+ border-radius: var(--grim-radius-lg);
96
+ box-shadow: var(--grim-shadow-raised);
90
97
  }
91
98
  .cmd {
92
99
  display: block;
93
100
  width: 100%;
94
101
  border: 0;
95
- border-radius: 5px;
102
+ border-radius: var(--grim-radius-md);
96
103
  background: transparent;
97
- color: var(--muted);
104
+ color: var(--grim-color-muted);
98
105
  font: inherit;
99
- font-size: 0.82rem;
106
+ font-size: var(--grim-text-sm);
100
107
  text-align: left;
101
- padding: 0.3rem 0.5rem;
108
+ padding: var(--grim-space-2) var(--grim-space-4);
102
109
  cursor: pointer;
103
110
  }
104
111
  .cmd:hover,
105
112
  .cmd:focus-visible {
106
- background: var(--chip-bg);
107
- color: var(--fg);
113
+ background: var(--grim-color-chip-bg);
114
+ color: var(--grim-color-fg);
108
115
  }
116
+ }
109
117
  </style>