@grove-dev/astro 0.3.1 → 0.3.4

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 (163) hide show
  1. package/README.md +9 -75
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +11 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/display.d.ts +1 -26
  6. package/dist/lib/display.d.ts.map +1 -1
  7. package/dist/lib/display.js +1 -103
  8. package/dist/lib/display.js.map +1 -1
  9. package/dist/lib/format.d.ts +1 -28
  10. package/dist/lib/format.d.ts.map +1 -1
  11. package/dist/lib/format.js +1 -71
  12. package/dist/lib/format.js.map +1 -1
  13. package/dist/lib/lenses.d.ts +1 -57
  14. package/dist/lib/lenses.d.ts.map +1 -1
  15. package/dist/lib/lenses.js +1 -198
  16. package/dist/lib/lenses.js.map +1 -1
  17. package/dist/lib/repo.d.ts +1 -19
  18. package/dist/lib/repo.d.ts.map +1 -1
  19. package/dist/lib/repo.js +1 -30
  20. package/dist/lib/repo.js.map +1 -1
  21. package/dist/lib/scores.d.ts +1 -36
  22. package/dist/lib/scores.d.ts.map +1 -1
  23. package/dist/lib/scores.js +1 -75
  24. package/dist/lib/scores.js.map +1 -1
  25. package/dist/lib/search.d.ts +1 -113
  26. package/dist/lib/search.d.ts.map +1 -1
  27. package/dist/lib/search.js +1 -379
  28. package/dist/lib/search.js.map +1 -1
  29. package/dist/lib/taxonomy-counts.d.ts +1 -10
  30. package/dist/lib/taxonomy-counts.d.ts.map +1 -1
  31. package/dist/lib/taxonomy-counts.js +1 -21
  32. package/dist/lib/taxonomy-counts.js.map +1 -1
  33. package/package.json +23 -11
  34. package/src/components/ContributorsGrid.astro +3 -3
  35. package/src/components/CurationGrid.astro +2 -2
  36. package/src/components/DirectoryIndexClient.astro +240 -0
  37. package/src/components/FilterGroupMenu.astro +8 -4
  38. package/src/components/FinalCta.astro +41 -0
  39. package/src/components/Hero.astro +15 -18
  40. package/src/components/Icon.astro +35 -7
  41. package/src/components/IndexRow.astro +2 -2
  42. package/src/components/ItemCard.astro +2 -7
  43. package/src/components/RefinePanel.astro +7 -7
  44. package/src/components/SmartLensTabs.astro +9 -5
  45. package/src/components/StackGrid.astro +1 -1
  46. package/src/components/SubmissionClient.astro +133 -0
  47. package/src/index.ts +11 -1
  48. package/src/layouts/BaseLayout.astro +30 -8
  49. package/src/layouts/Footer.astro +25 -56
  50. package/src/layouts/Header.astro +33 -15
  51. package/src/layouts/ThemeToggle.astro +7 -3
  52. package/src/lib/display.ts +1 -111
  53. package/src/lib/format.ts +1 -64
  54. package/src/lib/lenses.ts +1 -220
  55. package/src/lib/repo.ts +1 -32
  56. package/src/lib/scores.ts +1 -90
  57. package/src/lib/search.ts +1 -418
  58. package/src/lib/taxonomy-counts.ts +1 -26
  59. package/{templates/default/src/data/records.ts → src/server/directory.ts} +12 -8
  60. package/src/server/index.ts +2 -0
  61. package/src/server/models.ts +436 -0
  62. package/dist/template-routes.test.d.ts +0 -2
  63. package/dist/template-routes.test.d.ts.map +0 -1
  64. package/dist/template-routes.test.js +0 -63
  65. package/dist/template-routes.test.js.map +0 -1
  66. package/dist/theme.test.d.ts +0 -2
  67. package/dist/theme.test.d.ts.map +0 -1
  68. package/dist/theme.test.js +0 -60
  69. package/dist/theme.test.js.map +0 -1
  70. package/src/template-routes.test.ts +0 -103
  71. package/src/theme.test.ts +0 -76
  72. package/templates/default/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
  73. package/templates/default/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  74. package/templates/default/.github/ISSUE_TEMPLATE/record_submission.md +0 -48
  75. package/templates/default/.github/pull_request_template.md +0 -27
  76. package/templates/default/.github/workflows/build.yml +0 -53
  77. package/templates/default/.github/workflows/cleanup-stale-records.yml +0 -46
  78. package/templates/default/.github/workflows/daily-refresh.yml +0 -51
  79. package/templates/default/.github/workflows/sync-contributors.yml +0 -51
  80. package/templates/default/.github/workflows/sync-github-metadata.yml +0 -56
  81. package/templates/default/.github/workflows/validate-data.yml +0 -33
  82. package/templates/default/astro.config.mjs +0 -25
  83. package/templates/default/content/pages/about.md +0 -66
  84. package/templates/default/content/pages/methodology.md +0 -103
  85. package/templates/default/data/records/.gitkeep +0 -0
  86. package/templates/default/data/records/coolify.yml +0 -46
  87. package/templates/default/data/records/gitea.yml +0 -46
  88. package/templates/default/data/records/hoppscotch.yml +0 -47
  89. package/templates/default/data/records/inventree.yml +0 -47
  90. package/templates/default/data/records/label-studio.yml +0 -46
  91. package/templates/default/data/records/logseq.yml +0 -48
  92. package/templates/default/data/records/private-gpt.yml +0 -47
  93. package/templates/default/data/records/wakapi.yml +0 -44
  94. package/templates/default/data/taxonomy/categories.yml +0 -47
  95. package/templates/default/data/taxonomy/distribution-channels.yml +0 -43
  96. package/templates/default/data/taxonomy/platforms.yml +0 -27
  97. package/templates/default/data/taxonomy/stacks.yml +0 -41
  98. package/templates/default/grove.config.ts +0 -36
  99. package/templates/default/package.json +0 -43
  100. package/templates/default/public/icons/platforms/android.svg +0 -1
  101. package/templates/default/public/icons/platforms/apple-dark.svg +0 -1
  102. package/templates/default/public/icons/platforms/apple-light.svg +0 -1
  103. package/templates/default/public/icons/platforms/chrome.svg +0 -1
  104. package/templates/default/public/icons/platforms/chromeos.svg +0 -5
  105. package/templates/default/public/icons/platforms/desktop.svg +0 -4
  106. package/templates/default/public/icons/platforms/embedded.svg +0 -5
  107. package/templates/default/public/icons/platforms/ios.svg +0 -1
  108. package/templates/default/public/icons/platforms/linux.svg +0 -1
  109. package/templates/default/public/icons/platforms/macos.svg +0 -1
  110. package/templates/default/public/icons/platforms/ubuntu.svg +0 -1
  111. package/templates/default/public/icons/platforms/web.svg +0 -5
  112. package/templates/default/public/icons/platforms/windows.svg +0 -6
  113. package/templates/default/public/icons/stacks/android.svg +0 -1
  114. package/templates/default/public/icons/stacks/apple-dark.svg +0 -1
  115. package/templates/default/public/icons/stacks/apple-light.svg +0 -1
  116. package/templates/default/public/icons/stacks/bun.svg +0 -1
  117. package/templates/default/public/icons/stacks/capacitor.svg +0 -14
  118. package/templates/default/public/icons/stacks/clojure.svg +0 -1
  119. package/templates/default/public/icons/stacks/clojurescript.svg +0 -1
  120. package/templates/default/public/icons/stacks/dart.svg +0 -1
  121. package/templates/default/public/icons/stacks/deno.svg +0 -1
  122. package/templates/default/public/icons/stacks/django.svg +0 -1
  123. package/templates/default/public/icons/stacks/docker.svg +0 -1
  124. package/templates/default/public/icons/stacks/firebase.svg +0 -1
  125. package/templates/default/public/icons/stacks/flutter.svg +0 -1
  126. package/templates/default/public/icons/stacks/go.svg +0 -1
  127. package/templates/default/public/icons/stacks/graphql.svg +0 -1
  128. package/templates/default/public/icons/stacks/ionic.svg +0 -4
  129. package/templates/default/public/icons/stacks/java.svg +0 -1
  130. package/templates/default/public/icons/stacks/javascript.svg +0 -1
  131. package/templates/default/public/icons/stacks/kotlin.svg +0 -1
  132. package/templates/default/public/icons/stacks/llm.svg +0 -1
  133. package/templates/default/public/icons/stacks/mongodb.svg +0 -1
  134. package/templates/default/public/icons/stacks/node.js.svg +0 -1
  135. package/templates/default/public/icons/stacks/nodejs.svg +0 -1
  136. package/templates/default/public/icons/stacks/python.svg +0 -1
  137. package/templates/default/public/icons/stacks/rag.svg +0 -7
  138. package/templates/default/public/icons/stacks/react-native.svg +0 -1
  139. package/templates/default/public/icons/stacks/react.svg +0 -1
  140. package/templates/default/public/icons/stacks/rust.svg +0 -1
  141. package/templates/default/public/icons/stacks/solidity.svg +0 -1
  142. package/templates/default/public/icons/stacks/sveltekit.svg +0 -1
  143. package/templates/default/public/icons/stacks/swift.svg +0 -1
  144. package/templates/default/public/icons/stacks/tensorflow.svg +0 -1
  145. package/templates/default/public/icons/stacks/typescript.svg +0 -1
  146. package/templates/default/public/icons/stacks/vue.svg +0 -1
  147. package/templates/default/public/llms-full.txt +0 -83
  148. package/templates/default/public/llms.txt +0 -11
  149. package/templates/default/public/og-image.svg +0 -48
  150. package/templates/default/public/robots.txt +0 -53
  151. package/templates/default/public/sitemap.xml +0 -63
  152. package/templates/default/src/pages/404.astro +0 -64
  153. package/templates/default/src/pages/[slug]/[recordSlug].astro +0 -691
  154. package/templates/default/src/pages/[slug]/index.astro +0 -482
  155. package/templates/default/src/pages/about.astro +0 -189
  156. package/templates/default/src/pages/apps/[recordSlug].astro +0 -34
  157. package/templates/default/src/pages/contributors.astro +0 -175
  158. package/templates/default/src/pages/index.astro +0 -233
  159. package/templates/default/src/pages/sitemap.xml.ts +0 -89
  160. package/templates/default/src/pages/submit.astro +0 -281
  161. package/templates/default/src/styles/global.css +0 -33
  162. package/templates/default/tsconfig.json +0 -9
  163. package/templates/default/wrangler.jsonc +0 -28
@@ -0,0 +1,436 @@
1
+ /**
2
+ * Page view-models for Grove's Astro adapter.
3
+ *
4
+ * These functions contain directory decisions and generated-data access,
5
+ * while consumer-owned Astro pages keep control of markup and composition.
6
+ */
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { resolve } from "node:path";
9
+ import type { IndexFilters, IndexRecord, ProjectRecord } from "@grove-dev/core";
10
+ import {
11
+ activeFilterChips,
12
+ applySort,
13
+ buildFacets,
14
+ effectivePage,
15
+ effectiveSort,
16
+ filterRecords,
17
+ filtersFromSearchParams,
18
+ formatRelative,
19
+ formatStars,
20
+ getOwnerAndRepoFromRepoUrl,
21
+ getOwnerAvatarUrl,
22
+ projectStackIds,
23
+ statusDisplay,
24
+ totalPages,
25
+ } from "@grove-dev/core";
26
+ import {
27
+ entityBySlug,
28
+ fullItems,
29
+ fullProjects,
30
+ getContentHtml,
31
+ itemLabel,
32
+ itemLabelPlural,
33
+ items,
34
+ projectBySlug,
35
+ resourceBySlug,
36
+ taxonomyLabel,
37
+ } from "./directory.js";
38
+
39
+ export interface DirectorySiteConfig {
40
+ name: string;
41
+ tagline?: string;
42
+ description?: string;
43
+ repoUrl?: string;
44
+ nav?: Array<{ label: string; href: string }>;
45
+ facets?: string[];
46
+ analytics?: {
47
+ googleAnalyticsId?: string;
48
+ };
49
+ footer?: {
50
+ columns?: Array<{
51
+ heading: string;
52
+ items: Array<{ label: string; href: string; external?: boolean }>;
53
+ }>;
54
+ copyright?: string;
55
+ license?: string;
56
+ };
57
+ submission?: {
58
+ eyebrow?: string;
59
+ title?: string;
60
+ description?: string;
61
+ good?: string[];
62
+ avoid?: string[];
63
+ };
64
+ blueprintConfig?: {
65
+ routeSlug?: string;
66
+ labelSingular?: string;
67
+ labelPlural?: string;
68
+ };
69
+ taxonomy?: {
70
+ categories?: Array<{ id: string; name: string }>;
71
+ stacks?: Array<{ id: string; name: string }>;
72
+ platforms?: Array<{ id: string; name: string }>;
73
+ };
74
+ stats?: {
75
+ originalRepo?: string;
76
+ originalStars?: number;
77
+ originalForks?: number;
78
+ originalContributors?: number;
79
+ totalApps?: number;
80
+ totalCategories?: number;
81
+ totalStacks?: number;
82
+ totalOwners?: number;
83
+ totalStars?: number;
84
+ };
85
+ }
86
+
87
+ export interface DirectoryContributor {
88
+ username: string;
89
+ name?: string;
90
+ avatarUrl?: string;
91
+ profileUrl?: string;
92
+ contributions?: number;
93
+ }
94
+
95
+ export function loadDirectoryContributors(root = process.cwd()): DirectoryContributor[] {
96
+ const path = resolve(root, "data", "generated", "contributors.json");
97
+ if (!existsSync(path)) return [];
98
+ try {
99
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as {
100
+ contributors?: DirectoryContributor[];
101
+ };
102
+ return parsed.contributors ?? [];
103
+ } catch {
104
+ return [];
105
+ }
106
+ }
107
+
108
+ export function getHomePageModel(site: DirectorySiteConfig) {
109
+ const slug = site.blueprintConfig?.routeSlug ?? "projects";
110
+ const singular = site.blueprintConfig?.labelSingular ?? itemLabel();
111
+ const plural = site.blueprintConfig?.labelPlural ?? itemLabelPlural();
112
+ const projects = items.filter((record) => record.kind === "project");
113
+ const hot = filterRecords(projects, { labels: ["hot"] }).slice(0, 6);
114
+ const recentlyAdded = applySort(projects, "recently-added").slice(0, 6);
115
+ const established = filterRecords(projects, { labels: ["mature"] }).slice(0, 6);
116
+
117
+ const stackCounts = new Map<string, number>();
118
+ const categoryCounts = new Map<string, number>();
119
+ for (const record of fullItems) {
120
+ if (record.category) categoryCounts.set(record.category, (categoryCounts.get(record.category) ?? 0) + 1);
121
+ if (record.kind === "project" && record.stack) {
122
+ stackCounts.set(record.stack, (stackCounts.get(record.stack) ?? 0) + 1);
123
+ }
124
+ }
125
+ const stacks = [...stackCounts]
126
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
127
+ .slice(0, 12)
128
+ .map(([id, count]) => ({ name: taxonomyLabel("stacks", id), slug: id, count }));
129
+ const categories = [...categoryCounts]
130
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
131
+ .map(([id, count]) => ({ name: taxonomyLabel("categories", id), slug: id, count }));
132
+ const contributors = loadDirectoryContributors();
133
+ const description = site.description ??
134
+ `A searchable directory of real ${plural} — organized by stack, category, platform, license, activity, and maturity.`;
135
+
136
+ return {
137
+ slug,
138
+ itemSingular: singular,
139
+ itemPlural: plural,
140
+ hot,
141
+ recentlyAdded,
142
+ established,
143
+ stacks,
144
+ categories,
145
+ contributors,
146
+ title: `${site.name} — ${site.tagline ?? ""}`.trim(),
147
+ description,
148
+ stats: {
149
+ originalRepo: site.stats?.originalRepo ?? "",
150
+ apps: site.stats?.totalApps ?? 0,
151
+ categories: site.stats?.totalCategories ?? 0,
152
+ stacks: site.stats?.totalStacks ?? 0,
153
+ owners: site.stats?.totalOwners ?? 0,
154
+ stars: site.stats?.totalStars ?? 0,
155
+ },
156
+ };
157
+ }
158
+
159
+ const FACET_ALIASES: Record<string, "stacks" | "platforms" | "categories" | "tags" | "licenses"> = {
160
+ stack: "stacks",
161
+ stacks: "stacks",
162
+ platform: "platforms",
163
+ platforms: "platforms",
164
+ category: "categories",
165
+ categories: "categories",
166
+ tag: "tags",
167
+ tags: "tags",
168
+ license: "licenses",
169
+ licenses: "licenses",
170
+ };
171
+
172
+ function configuredFacets(site?: DirectorySiteConfig) {
173
+ return new Set((site?.facets ?? ["category", "tags"])
174
+ .map((facet) => FACET_ALIASES[facet])
175
+ .filter((facet): facet is NonNullable<typeof facet> => Boolean(facet)));
176
+ }
177
+
178
+ export function getDirectoryIndexModel(searchParams: URLSearchParams, site?: DirectorySiteConfig) {
179
+ const filters = filtersFromSearchParams(searchParams);
180
+ const rawFacets = buildFacets(items);
181
+ const enabled = configuredFacets(site);
182
+ const facets = {
183
+ stacks: enabled.has("stacks") ? rawFacets.stacks.map((option) => ({ ...option, label: taxonomyLabel("stacks", option.value) })) : [],
184
+ platforms: enabled.has("platforms") ? rawFacets.platforms.map((option) => ({ ...option, label: taxonomyLabel("platforms", option.value) })) : [],
185
+ categories: enabled.has("categories") ? rawFacets.categories.map((option) => ({ ...option, label: taxonomyLabel("categories", option.value) })) : [],
186
+ tags: enabled.has("tags") ? rawFacets.tags : [],
187
+ licenses: enabled.has("licenses") ? rawFacets.licenses : [],
188
+ };
189
+ const sort = effectiveSort(filters);
190
+ const sorted = applySort(filterRecords(items, filters), sort);
191
+ const pageCount = totalPages(sorted.length);
192
+ const page = Math.min(effectivePage(filters), pageCount);
193
+ return {
194
+ items,
195
+ total: items.length,
196
+ filters,
197
+ facets,
198
+ sort,
199
+ sorted,
200
+ pages: pageCount,
201
+ page,
202
+ chips: activeFilterChips(filters),
203
+ clientItemsJson: JSON.stringify(items).replace(/</g, "\\u003c"),
204
+ };
205
+ }
206
+
207
+ export function getContributorsPageModel(site: DirectorySiteConfig) {
208
+ const contributors = loadDirectoryContributors();
209
+ const sorted = [...contributors].sort((a, b) =>
210
+ (b.contributions ?? 0) - (a.contributions ?? 0) || a.username.localeCompare(b.username)
211
+ );
212
+ const repo = site.repoUrl?.replace(/\/$/, "");
213
+ return {
214
+ contributors: sorted,
215
+ total: sorted.length,
216
+ title: `Community — ${site.name}`,
217
+ description: `The people who have contributed to ${site.name} through code, curation, and review.`,
218
+ contributorsGraphHref: repo ? `${repo}/graphs/contributors` : null,
219
+ };
220
+ }
221
+
222
+ export function getSubmissionPageModel(site: DirectorySiteConfig) {
223
+ const singular = site.blueprintConfig?.labelSingular ?? itemLabel();
224
+ const enabled = configuredFacets(site);
225
+ const values = (key: "category" | "stack") =>
226
+ [...new Set(fullProjects.map((record) => String(record[key] ?? "")).filter(Boolean))]
227
+ .sort((a, b) => a.localeCompare(b));
228
+ const categories = (site.taxonomy?.categories?.length
229
+ ? site.taxonomy.categories.map(({ id }) => id)
230
+ : values("category"))
231
+ .map((id) => ({ id, label: taxonomyLabel("categories", id) }));
232
+ const stacks = (site.taxonomy?.stacks?.length
233
+ ? site.taxonomy.stacks.map(({ id }) => id)
234
+ : values("stack"))
235
+ .map((id) => ({ id, label: taxonomyLabel("stacks", id) }));
236
+ const existingFullNames = fullProjects.map((record) => {
237
+ const url = String(record.repoUrl ?? record.links?.github ?? "");
238
+ const match = url.match(/github\.com\/([^/]+)\/([^/?#]+)/i);
239
+ return match ? `${match[1]}/${match[2].replace(/\.git$/, "")}`.toLowerCase() : null;
240
+ }).filter((value): value is string => Boolean(value));
241
+
242
+ return {
243
+ singular,
244
+ title: `Submit ${singular} - ${site.name}`,
245
+ description: `Submit a new ${singular} to ${site.name}.`,
246
+ repoUrl: site.repoUrl ?? "https://github.com/tortuvshin/grove",
247
+ copy: site.submission ?? {},
248
+ fields: {
249
+ category: enabled.has("categories"),
250
+ stack: enabled.has("stacks"),
251
+ platforms: enabled.has("platforms"),
252
+ tags: enabled.has("tags"),
253
+ },
254
+ categories,
255
+ stacks,
256
+ existingSlugs: fullProjects.map((record) => record.slug).filter(Boolean),
257
+ existingFullNames,
258
+ platformOptions: site.taxonomy?.platforms?.map(({ id, name }) => ({ id, label: name })) ??
259
+ ["ios", "android", "web", "macos", "windows", "linux"].map((id) => ({ id, label: taxonomyLabel("platforms", id) })),
260
+ };
261
+ }
262
+
263
+ type MonthlyCommit = number | { month: string; commits: number };
264
+ type ProjectExtras = {
265
+ github?: {
266
+ activity?: { monthlyCommits?: MonthlyCommit[] };
267
+ files?: Record<string, boolean>;
268
+ languages?: Record<string, number>;
269
+ };
270
+ health?: { status?: string; tier?: string };
271
+ };
272
+
273
+ export function getRecordDetailModel(
274
+ recordSlug: string,
275
+ site: DirectorySiteConfig,
276
+ routeSlug = site.blueprintConfig?.routeSlug ?? "projects",
277
+ ) {
278
+ const project = projectBySlug(recordSlug);
279
+ const resource = resourceBySlug(recordSlug);
280
+ const entity = entityBySlug(recordSlug);
281
+ const record = project ?? resource ?? entity;
282
+ if (!record) return null;
283
+
284
+ const isProject = record.kind === "project";
285
+ const proj: ProjectRecord | null = project ?? null;
286
+ const name = record.kind === "resource" ? record.title : record.name;
287
+ const description = record.description ?? `${name} on ${site.name}`;
288
+ const repoUrl = proj?.repoUrl ?? record.links?.github ?? "";
289
+ const homepageUrl = record.links?.website ?? "";
290
+ const stacks = projectStackIds(proj);
291
+ const platforms = proj?.platforms ?? [];
292
+ const github = proj?.github?.repository;
293
+ const stars = github?.stargazers_count ?? 0;
294
+ const forks = github?.forks_count ?? 0;
295
+ const language = github?.language ?? null;
296
+ const licenseSpdx = github?.license?.spdx_id ?? null;
297
+ const pushedAt = github?.pushed_at ?? null;
298
+ const ownerRepo = repoUrl ? getOwnerAndRepoFromRepoUrl(repoUrl) : null;
299
+ const avatarSrc = proj?.logoUrl ?? (ownerRepo ? getOwnerAvatarUrl(ownerRepo.owner, 80) : null);
300
+ const rawScores = proj?.scores;
301
+ const scores = rawScores && Object.values(rawScores).some((value) => typeof value === "number")
302
+ ? rawScores
303
+ : null;
304
+ const healthStatus = proj?.health?.status;
305
+ const extras = record as ProjectExtras;
306
+ const monthlyCommits = (extras.github?.activity?.monthlyCommits ?? []).map(
307
+ (value): { month: string; commits: number } => typeof value === "number"
308
+ ? { month: "", commits: value }
309
+ : { month: value.month ?? "", commits: value.commits ?? 0 },
310
+ );
311
+ const contributionLabels: Record<string, string> = {
312
+ readme: "README",
313
+ contributing: "Contributing",
314
+ security: "Security",
315
+ issueTemplates: "Issue templates",
316
+ pullRequestTemplate: "PR template",
317
+ };
318
+ const contributionFiles = extras.github?.files ?? {};
319
+ const contributionSignals = Object.keys(contributionLabels)
320
+ .map((key) => ({ key, label: contributionLabels[key], ok: contributionFiles[key] }))
321
+ .filter((signal) => signal.ok !== undefined);
322
+ const languages = extras.github?.languages
323
+ ? Object.entries(extras.github.languages).sort((a, b) => b[1] - a[1]).slice(0, 5)
324
+ : [];
325
+
326
+ let jsonLd: Record<string, unknown>;
327
+ if (isProject && proj) {
328
+ const sameAs = [repoUrl, homepageUrl].filter(Boolean);
329
+ const dateCreated = record.curation?.reviewedAt;
330
+ jsonLd = {
331
+ "@context": "https://schema.org",
332
+ "@type": "SoftwareSourceCode",
333
+ name,
334
+ headline: name,
335
+ description: record.description ?? undefined,
336
+ url: repoUrl || homepageUrl || undefined,
337
+ codeRepository: repoUrl || undefined,
338
+ sameAs: sameAs.length ? sameAs : undefined,
339
+ programmingLanguage: language ?? undefined,
340
+ license: licenseSpdx ?? undefined,
341
+ applicationCategory: record.category || undefined,
342
+ operatingSystem: platforms.length ? platforms.join(", ") : undefined,
343
+ keywords: record.tags?.length ? record.tags.join(", ") : undefined,
344
+ dateCreated: dateCreated ?? undefined,
345
+ dateModified: pushedAt ?? dateCreated ?? undefined,
346
+ interactionStatistic: stars > 0 ? {
347
+ "@type": "InteractionCounter",
348
+ interactionType: { "@type": "LikeAction" },
349
+ userInteractionCount: stars,
350
+ } : undefined,
351
+ author: ownerRepo ? {
352
+ "@type": "Organization",
353
+ name: ownerRepo.owner,
354
+ url: `https://github.com/${ownerRepo.owner}`,
355
+ } : undefined,
356
+ isAccessibleForFree: true,
357
+ };
358
+ } else if (record.kind === "resource") {
359
+ const schemaTypes: Record<string, string> = {
360
+ article: "Article", book: "Book", course: "Course", podcast: "PodcastSeries", video: "VideoObject",
361
+ };
362
+ jsonLd = {
363
+ "@context": "https://schema.org",
364
+ "@type": schemaTypes[record.type] ?? "CreativeWork",
365
+ name,
366
+ headline: name,
367
+ description: record.description || undefined,
368
+ url: homepageUrl || undefined,
369
+ author: record.author ? { "@type": "Person", name: record.author } : undefined,
370
+ datePublished: record.publishedAt || undefined,
371
+ keywords: record.tags?.length ? record.tags.join(", ") : undefined,
372
+ isAccessibleForFree: true,
373
+ };
374
+ } else {
375
+ jsonLd = {
376
+ "@context": "https://schema.org",
377
+ "@type": entity?.type === "person" ? "Person" : "Organization",
378
+ name,
379
+ headline: name,
380
+ description: entity?.description || undefined,
381
+ url: homepageUrl || undefined,
382
+ foundingDate: entity?.founded || undefined,
383
+ location: entity?.location || undefined,
384
+ keywords: entity?.tags?.length ? entity.tags.join(", ") : undefined,
385
+ };
386
+ }
387
+
388
+ return {
389
+ slug: routeSlug,
390
+ record,
391
+ project: proj,
392
+ entity,
393
+ isProject,
394
+ name,
395
+ title: `${name} — ${site.name}`,
396
+ description,
397
+ itemSingular: site.blueprintConfig?.labelSingular ?? record.kind,
398
+ repoUrl,
399
+ homepageUrl,
400
+ stacks,
401
+ platforms,
402
+ github,
403
+ stars,
404
+ forks,
405
+ language,
406
+ licenseSpdx,
407
+ pushedAt,
408
+ starsLabel: formatStars(stars),
409
+ pushedLabel: formatRelative(pushedAt),
410
+ ownerRepo,
411
+ avatarSrc,
412
+ initials: name.replace(/[^a-z0-9]/gi, "").slice(0, 2).toUpperCase(),
413
+ bestFor: proj?.bestFor ?? [],
414
+ whyListed: proj?.whyListed ?? [],
415
+ caveats: proj?.caveats ?? [],
416
+ distribution: proj?.distribution?.channels ?? [],
417
+ scores,
418
+ curationLabels: record.curation?.labels ?? [],
419
+ tags: record.tags ?? [],
420
+ healthLabel: healthStatus ? statusDisplay(healthStatus) : null,
421
+ contentHtml: isProject ? getContentHtml(recordSlug) : null,
422
+ monthlyCommits,
423
+ maxMonthlyCommits: Math.max(1, ...monthlyCommits.map((item) => item.commits)),
424
+ contributionSignals,
425
+ languages,
426
+ totalLanguageBytes: Math.max(1, languages.reduce((sum, [, bytes]) => sum + bytes, 0)),
427
+ jsonLd,
428
+ };
429
+ }
430
+
431
+ export type DirectoryIndexModel = ReturnType<typeof getDirectoryIndexModel>;
432
+ export type DirectoryHomeModel = ReturnType<typeof getHomePageModel>;
433
+ export type SubmissionPageModel = ReturnType<typeof getSubmissionPageModel>;
434
+ export type ContributorsPageModel = ReturnType<typeof getContributorsPageModel>;
435
+ export type RecordDetailModel = NonNullable<ReturnType<typeof getRecordDetailModel>>;
436
+ export type { IndexFilters };
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=template-routes.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"template-routes.test.d.ts","sourceRoot":"","sources":["../src/template-routes.test.ts"],"names":[],"mappings":""}
@@ -1,63 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { readFile, stat } from "node:fs/promises";
3
- import { resolve } from "node:path";
4
- const pagesDir = resolve(import.meta.dirname, "../templates/default/src/pages");
5
- describe("default Astro route configuration", () => {
6
- it("ships TypeScript settings that resolve package exports and Node built-ins", async () => {
7
- const templateRoot = resolve(pagesDir, "../..");
8
- const tsconfig = JSON.parse(await readFile(resolve(templateRoot, "tsconfig.json"), "utf8"));
9
- const manifest = JSON.parse(await readFile(resolve(templateRoot, "package.json"), "utf8"));
10
- expect(tsconfig.extends).toBe("astro/tsconfigs/base");
11
- expect(tsconfig.compilerOptions?.moduleResolution).toBe("Bundler");
12
- expect(tsconfig.compilerOptions?.types).toContain("node");
13
- expect(manifest.devDependencies?.["@types/node"]).toMatch(/^\^/);
14
- });
15
- it("treats generated JSON as an untyped boundary before applying payload types", async () => {
16
- const recordsModule = await readFile(resolve(pagesDir, "../data/records.ts"), "utf8");
17
- expect(recordsModule).toContain("fullPayload as unknown as FullPayload");
18
- });
19
- it("derives the directory route from generated site config", async () => {
20
- const listPage = await readFile(resolve(pagesDir, "[slug]/index.astro"), "utf8");
21
- expect(listPage).toContain("siteConfig.blueprintConfig?.routeSlug");
22
- });
23
- it("does not generate the legacy apps alias when apps is canonical", async () => {
24
- const aliasPage = await readFile(resolve(pagesDir, "apps/[recordSlug].astro"), "utf8");
25
- expect(aliasPage).toContain('if (indexSlug() === "apps")');
26
- expect(aliasPage).toContain("return []");
27
- });
28
- it("renders consumer-authored about Markdown through the default page", async () => {
29
- const aboutPage = await readFile(resolve(pagesDir, "about.astro"), "utf8");
30
- expect(aboutPage).toContain('getPageContentHtml("about")');
31
- });
32
- it("generates submission drafts accepted by the Grove record schema", async () => {
33
- const submitPage = await readFile(resolve(pagesDir, "submit.astro"), "utf8");
34
- expect(submitPage).toContain('" type: manual"');
35
- expect(submitPage).not.toContain('" type: github"');
36
- });
37
- it("keeps generic maintenance behavior in Grove instead of consumer scripts", async () => {
38
- const templateRoot = resolve(pagesDir, "../..");
39
- const manifest = JSON.parse(await readFile(resolve(templateRoot, "package.json"), "utf8"));
40
- const scriptsDirExists = await stat(resolve(templateRoot, "scripts"))
41
- .then(() => true)
42
- .catch(() => false);
43
- expect(manifest.scripts?.["build:llms"]).toBe("grove llms");
44
- expect(manifest.scripts?.["sync:contributors"]).toBe("grove sync contributors");
45
- expect(scriptsDirExists).toBe(false);
46
- });
47
- it("hydrates static list pages with URL-driven search and pagination", async () => {
48
- const listPage = await readFile(resolve(pagesDir, "[slug]/index.astro"), "utf8");
49
- expect(listPage).toContain('id="grove-index-data"');
50
- expect(listPage).toContain("function applyClientFilters()");
51
- expect(listPage).toContain("const PAGE_SIZE = 20");
52
- });
53
- it("uses generated taxonomy names as display labels", async () => {
54
- const recordsModule = await readFile(resolve(pagesDir, "../data/records.ts"), "utf8");
55
- const homePage = await readFile(resolve(pagesDir, "index.astro"), "utf8");
56
- const listPage = await readFile(resolve(pagesDir, "[slug]/index.astro"), "utf8");
57
- expect(recordsModule).toContain("export function taxonomyLabel");
58
- expect(recordsModule).toContain("?? prettySlug(id)");
59
- expect(homePage).toContain('taxonomyLabel("categories"');
60
- expect(listPage).toContain("labelFacetsWithTaxonomy");
61
- });
62
- });
63
- //# sourceMappingURL=template-routes.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"template-routes.test.js","sourceRoot":"","sources":["../src/template-routes.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=theme.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"theme.test.d.ts","sourceRoot":"","sources":["../src/theme.test.ts"],"names":[],"mappings":""}
@@ -1,60 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- import { describe, expect, it } from "vitest";
4
- const astroTheme = readFileSync(resolve(import.meta.dirname, "styles.css"), "utf8");
5
- const scaffoldTheme = readFileSync(resolve(import.meta.dirname, "../templates/default/src/styles/global.css"), "utf8");
6
- const tailwindMarkup = [
7
- "components/ItemCard.astro",
8
- "layouts/Header.astro",
9
- "../templates/default/src/pages/submit.astro",
10
- ].map((file) => readFileSync(resolve(import.meta.dirname, file), "utf8")).join("\n");
11
- const iconMarkup = readFileSync(resolve(import.meta.dirname, "components/Icon.astro"), "utf8");
12
- describe("Astro theme contract", () => {
13
- it("keeps Starlight's light and dark foundation values", () => {
14
- expect(astroTheme).toContain("--grove-background: oklch(100% 0 0)");
15
- expect(astroTheme).toContain("--grove-foreground: var(--color-ink-950)");
16
- expect(astroTheme).toContain("--grove-background: var(--color-ink-950)");
17
- expect(astroTheme).toContain("--grove-foreground: var(--color-ink-50)");
18
- expect(astroTheme).toContain("--color-ink-50: oklch(98.5% 0 0)");
19
- expect(astroTheme).toContain("--color-ink-950: oklch(14.5% 0 0)");
20
- });
21
- it("exposes runtime semantic colors to Tailwind", () => {
22
- for (const token of [
23
- "background",
24
- "foreground",
25
- "primary",
26
- "secondary",
27
- "muted",
28
- "accent",
29
- "border",
30
- "card",
31
- "popover",
32
- ]) {
33
- expect(astroTheme).toContain(`--color-${token}: var(--grove-${token})`);
34
- }
35
- });
36
- it("uses the same system and mono font stacks as the docs", () => {
37
- expect(astroTheme).toContain('--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,');
38
- expect(astroTheme).toContain("--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,");
39
- expect(astroTheme).toContain("font-family: var(--font-sans)");
40
- });
41
- it("does not duplicate package-owned foundation tokens in the scaffold", () => {
42
- expect(scaffoldTheme).not.toContain("--color-ink-");
43
- expect(scaffoldTheme).not.toContain("font-family:");
44
- expect(scaffoldTheme).not.toContain("color-scheme:");
45
- });
46
- it("styles components in HTML with Starlight-aligned Tailwind utilities", () => {
47
- expect(tailwindMarkup).toContain("rounded-[calc(var(--radius)+0.25rem)]");
48
- expect(tailwindMarkup).toContain("rounded-full");
49
- expect(tailwindMarkup).toContain("min-h-5");
50
- expect(tailwindMarkup).toContain("h-8");
51
- expect(tailwindMarkup).toContain("focus-visible:ring-3");
52
- expect(astroTheme).not.toContain(".grove-card");
53
- expect(astroTheme).not.toContain(".grove-btn");
54
- });
55
- it("falls back to initials when a consumer has no matching icon asset", () => {
56
- expect(iconMarkup).toContain("data-grove-icon-fallback-initials");
57
- expect(iconMarkup).toContain("onerror=");
58
- });
59
- });
60
- //# sourceMappingURL=theme.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"theme.test.js","sourceRoot":"","sources":["../src/theme.test.ts"],"names":[],"mappings":"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"}