@rimelight/ui 0.0.15 → 0.0.16

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.
@@ -1,63 +1,132 @@
1
- import type { StarlightPlugin } from "@astrojs/starlight/types"
2
- import virtual from "vite-plugin-virtual"
3
- import path from "node:path"
4
- import { fileURLToPath } from "node:url"
5
-
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
-
8
- export interface StarlightAddonsConfig {
9
- copy?: boolean
10
- share?: boolean
11
- packageManagers?: string[]
12
- }
13
-
14
- export default function starlightAddons(userConfig?: StarlightAddonsConfig): StarlightPlugin {
15
- const defaultConfig: StarlightAddonsConfig = {
16
- copy: true,
17
- share: true,
18
- packageManagers: ["npm"]
19
- }
20
-
21
- const config: StarlightAddonsConfig = {
22
- ...defaultConfig,
23
- ...userConfig
24
- }
25
-
26
- return {
27
- name: "rimelight-ui-starlight-addons",
28
- hooks: {
29
- "config:setup"({ addIntegration, updateConfig, logger }) {
30
- if (
31
- !config.copy &&
32
- !config.share &&
33
- (!config.packageManagers || config.packageManagers.length === 0)
34
- ) {
35
- logger.warn("StarlightAddons: Nothing enabled.")
36
- }
37
-
38
- addIntegration({
39
- name: "rimelight-ui-starlight-addons-integration",
40
- hooks: {
41
- "astro:config:setup": ({ updateConfig: updateAstroConfig }) => {
42
- updateAstroConfig({
43
- vite: {
44
- plugins: [
45
- virtual({
46
- "virtual:rimelight-starlight-addons-config": `export default ${JSON.stringify(config)}`
47
- })
48
- ]
49
- }
50
- } as Record<string, unknown>)
51
- }
52
- }
53
- })
54
-
55
- updateConfig({
56
- components: {
57
- PageTitle: path.join(__dirname, "PageTitle.astro")
58
- }
59
- })
60
- }
61
- }
62
- }
63
- }
1
+ /// <reference path="./locals.d.ts" />
2
+ import type { StarlightPlugin, StarlightUserConfig } from "@astrojs/starlight/types"
3
+ import virtual from "vite-plugin-virtual"
4
+ import path from "node:path"
5
+ import { fileURLToPath } from "node:url"
6
+ import { z } from "astro/zod"
7
+
8
+ import {
9
+ StarlightSidebarTopicsConfigSchema,
10
+ StarlightSidebarTopicsOptionsSchema,
11
+ type StarlightSidebarTopicsUserConfig,
12
+ type StarlightSidebarTopicsUserOptions
13
+ } from "./libs/config"
14
+ import { throwPluginError } from "./libs/plugin"
15
+ import { vitePluginStarlightSidebarTopics } from "./libs/vite"
16
+
17
+ export type {
18
+ StarlightSidebarTopicsConfig,
19
+ StarlightSidebarTopicsUserConfig,
20
+ StarlightSidebarTopicsOptions,
21
+ StarlightSidebarTopicsUserOptions
22
+ } from "./libs/config"
23
+
24
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
25
+
26
+ export interface StarlightAddonsConfig {
27
+ copy?: boolean
28
+ share?: boolean
29
+ packageManagers?: string[]
30
+ // Merged config for Topics
31
+ topics?: StarlightSidebarTopicsUserConfig
32
+ topicOptions?: StarlightSidebarTopicsUserOptions
33
+ }
34
+
35
+ export default function starlightAddons(userConfig?: StarlightAddonsConfig): StarlightPlugin {
36
+ // 1. Setup default and merged config for Addons
37
+ const defaultAddonConfig: StarlightAddonsConfig = {
38
+ copy: true,
39
+ share: true,
40
+ packageManagers: ["npm"]
41
+ }
42
+
43
+ const addonConfig = { ...defaultAddonConfig, ...userConfig }
44
+
45
+ // 2. Validate Sidebar Topics configurations if they exist
46
+ const parsedTopics = StarlightSidebarTopicsConfigSchema.safeParse(addonConfig.topics || [])
47
+ if (!parsedTopics.success) {
48
+ throwPluginError(
49
+ `Invalid topics config: ${parsedTopics.error.issues.map((i) => i.message).join("\n")}`
50
+ )
51
+ }
52
+
53
+ const parsedOptions = StarlightSidebarTopicsOptionsSchema.safeParse(addonConfig.topicOptions)
54
+ if (!parsedOptions.success) {
55
+ throwPluginError(`Invalid topic options: ${z.prettifyError(parsedOptions.error)}`)
56
+ }
57
+
58
+ const topicsData = parsedTopics.data
59
+ const optionsData = parsedOptions.data
60
+
61
+ return {
62
+ name: "rimelight-ui-starlight-addons",
63
+ hooks: {
64
+ "config:setup"({
65
+ addIntegration,
66
+ updateConfig,
67
+ logger,
68
+ addRouteMiddleware,
69
+ command,
70
+ config: starlightConfig
71
+ }) {
72
+ // --- Sidebar Topics Logic ---
73
+ if ((command === "dev" || command === "build") && topicsData.length > 0) {
74
+ if (starlightConfig.sidebar) {
75
+ throwPluginError(
76
+ "Sidebar detected. To use topics, move your sidebar items into the topics config."
77
+ )
78
+ }
79
+
80
+ const topicSidebar: StarlightUserConfig["sidebar"] = topicsData.map((topic, index) => {
81
+ return "items" in topic
82
+ ? { label: String(index), items: topic.items }
83
+ : { label: String(index), items: [] }
84
+ })
85
+
86
+ updateConfig({
87
+ sidebar: topicSidebar,
88
+ components: {
89
+ Sidebar: `starlight-sidebar-topics/overrides/Sidebar.astro`
90
+ }
91
+ })
92
+ }
93
+
94
+ // --- Addons Logic ---
95
+ if (
96
+ !addonConfig.copy &&
97
+ !addonConfig.share &&
98
+ (!addonConfig.packageManagers || addonConfig.packageManagers.length === 0)
99
+ ) {
100
+ logger.warn("StarlightAddons: No features enabled.")
101
+ }
102
+
103
+ updateConfig({
104
+ components: {
105
+ // This will merge with the Sidebar override if both exist
106
+ PageTitle: path.join(__dirname, "PageTitle.astro")
107
+ }
108
+ })
109
+
110
+ // --- Unified Integration ---
111
+ addIntegration({
112
+ name: "rimelight-ui-starlight-addons-integration",
113
+ hooks: {
114
+ "astro:config:setup": ({ updateConfig: updateAstroConfig }) => {
115
+ updateAstroConfig({
116
+ vite: {
117
+ plugins: [
118
+ virtual({
119
+ "virtual:rimelight-starlight-addons-config": `export default ${JSON.stringify(addonConfig)}`
120
+ }),
121
+ // Pass the validated topic data to the vite plugin
122
+ vitePluginStarlightSidebarTopics(topicsData, optionsData)
123
+ ]
124
+ }
125
+ } as Record<string, unknown>)
126
+ }
127
+ }
128
+ })
129
+ }
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,107 @@
1
+ import type { StarlightUserConfig } from "@astrojs/starlight/types"
2
+ import { z } from "astro/zod"
3
+
4
+ const sidebarTopicBadgeSchema = z.object({
5
+ text: z.union([z.string(), z.record(z.string(), z.string())]),
6
+ variant: z.enum(["note", "danger", "success", "caution", "tip", "default"]).default("default")
7
+ })
8
+
9
+ const sidebarTopicBaseSchema = z.object({
10
+ /**
11
+ * An optional badge to display next to the topic label.
12
+ *
13
+ * This option accepts the same configuration as the Starlight badge sidebar item configuration.
14
+ *
15
+ * @see https://starlight.astro.build/guides/sidebar/#badges
16
+ */
17
+ badge: z
18
+ .union([z.string(), sidebarTopicBadgeSchema])
19
+ .transform((badge) =>
20
+ typeof badge === "string" ? { variant: "default" as const, text: badge } : badge
21
+ )
22
+ .optional(),
23
+ /**
24
+ * The name of an optional icon to display before the topic label set to one of Starlight’s
25
+ * built-in icons.
26
+ *
27
+ * @see https://starlight.astro.build/reference/icons/#all-icons
28
+ */
29
+ icon: z.string().optional(),
30
+ /**
31
+ * The topic label visible at the top of the sidebar.
32
+ *
33
+ * The value can be a string, or for multilingual sites, an object with values for each different
34
+ * locale. When using the object form, the keys must be BCP-47 tags (e.g. en, fr, or zh-CN).
35
+ */
36
+ label: z.union([z.string(), z.record(z.string(), z.string())]),
37
+ /**
38
+ * The link to the topic’s content which an be a relative link to local files or the full URL of
39
+ * an external page.
40
+ *
41
+ * For internal links, the link can either be a page included in the items array or a different
42
+ * page acting as the topic’s landing page.
43
+ */
44
+ link: z.string()
45
+ })
46
+
47
+ const sidebarTopicLinkSchema = sidebarTopicBaseSchema
48
+
49
+ const sidebarTopicGroupSchema = sidebarTopicBaseSchema.extend({
50
+ /**
51
+ * An optional unique identifier for the topic that can be used to associate content pages that
52
+ * are not listed in any topic sidebar configuration with this topic.
53
+ */
54
+ id: z.string().optional(),
55
+ /**
56
+ * The sidebar items (links and subcategories) to display for this topic.
57
+ *
58
+ * The topic’s sidebar navigation items. This represents the sidebar displayed when the topic
59
+ * `link` page or any of the pages configured in the `items` array is the current page.
60
+ */
61
+ items: z.any().array() as z.ZodType<NonNullable<StarlightUserConfig["sidebar"]>>
62
+ })
63
+
64
+ export const StarlightSidebarTopicsConfigSchema = z
65
+ .union([sidebarTopicGroupSchema, sidebarTopicLinkSchema])
66
+ .array()
67
+
68
+ export const StarlightSidebarTopicsOptionsSchema = z
69
+ .strictObject({
70
+ /**
71
+ * Defines a list of pages or glob patterns that should be excluded from any topic.
72
+ *
73
+ * This options can be useful for custom pages that use a custom site navigation sidebar which
74
+ * do not belong to any topic. Excluded pages will use the built-in Starlight sidebar and not
75
+ * render a list of topics.
76
+ *
77
+ * @default [ ]
78
+ */
79
+ exclude: z.array(z.string()).default([]),
80
+ /**
81
+ * Defines a map of topic IDs mapped to a list of pages or glob patterns that should be
82
+ * associated with the topic.
83
+ *
84
+ * This option can be useful for custom pages generated and included in the sidebar by other
85
+ * plugins that have no knowledge of the Starlight Sidebar Topics plugin that should be
86
+ * associated with a specific topic.
87
+ *
88
+ * @default {}
89
+ */
90
+ topics: z.record(z.string(), z.array(z.string())).default({})
91
+ })
92
+ .prefault({})
93
+
94
+ export type StarlightSidebarTopicsUserConfig = z.input<typeof StarlightSidebarTopicsConfigSchema>
95
+ export type StarlightSidebarTopicsConfig = z.output<typeof StarlightSidebarTopicsConfigSchema>
96
+
97
+ export type StarlightSidebarTopicsUserOptions = z.input<typeof StarlightSidebarTopicsOptionsSchema>
98
+ export type StarlightSidebarTopicsOptions = z.output<typeof StarlightSidebarTopicsOptionsSchema>
99
+
100
+ export type StarlightSidebarTopicsSharedConfig = (
101
+ | (z.output<typeof sidebarTopicLinkSchema> & { type: "link" })
102
+ | (Omit<z.output<typeof sidebarTopicGroupSchema>, "items"> & { type: "group" })
103
+ )[]
104
+
105
+ export type StarlightSidebarTopicsSharedOptions = StarlightSidebarTopicsOptions
106
+
107
+ export type SidebarTopicBadge = z.output<typeof sidebarTopicBadgeSchema>
@@ -0,0 +1,20 @@
1
+ import type { StarlightRouteData } from "@astrojs/starlight/route-data"
2
+
3
+ export function isStarlightEntryWithTopic(entry: StarlightEntry): entry is StarlightEntryWithTopic {
4
+ // Use a runtime-friendly shape check to narrow the type safely
5
+ // without relying on potentially unsafe TS inferences for discriminated unions.
6
+ if (typeof entry !== "object" || entry === null) return false
7
+ // Shape-based runtime check without unsafe casts
8
+ if (typeof entry !== "object" || entry === null) return false
9
+ // entry must contain a data property which is an object with a string topic
10
+ // Use a minimal structural check; rely on TypeScript to narrow after this predicate
11
+ // We cannot access entry.data without a type assertion in TS without a cast; but the guard below
12
+ // ensures correctness for subsequent code that relies on this predicate.
13
+ const anyEntry = entry as any
14
+ if (!anyEntry.data) return false
15
+ const data = anyEntry.data
16
+ if (typeof data !== "object" || data === null) return false
17
+ return typeof data.topic === "string"
18
+ }
19
+ export type StarlightEntry = StarlightRouteData["entry"]
20
+ export type StarlightEntryWithTopic = StarlightEntry & { data: { topic: string } }
@@ -0,0 +1,51 @@
1
+ import type { APIContext } from "astro"
2
+ import { AstroError } from "astro/errors"
3
+ import starlightConfig from "virtual:starlight/user-config"
4
+
5
+ import { stripTrailingSlash } from "./pathname"
6
+
7
+ const defaultLang =
8
+ starlightConfig.defaultLocale.lang ?? starlightConfig.defaultLocale.locale ?? "en"
9
+
10
+ export function getLocalizedSlug(slug: string, locale: string | undefined): string {
11
+ const slugLocale = getLocaleFromSlug(slug)
12
+ if (slugLocale === locale) return slug
13
+ locale ??= ""
14
+ if (slugLocale === slug) return locale
15
+
16
+ if (slugLocale) {
17
+ return stripTrailingSlash(slug.replace(`${slugLocale}/`, locale ? `${locale}/` : ""))
18
+ }
19
+
20
+ return slug ? `${locale}/${slug}` : locale
21
+ }
22
+
23
+ export function getLocaleFromSlug(slug: string): string | undefined {
24
+ const locales = Object.keys(starlightConfig.locales ?? {})
25
+ const baseSegment = slug.split("/")[0]
26
+ return baseSegment && locales.includes(baseSegment) ? baseSegment : undefined
27
+ }
28
+
29
+ export function getTranslation(
30
+ currentLocale: APIContext["currentLocale"],
31
+ translations: Record<string, string>,
32
+ link: string,
33
+ description: string
34
+ ) {
35
+ const defaultTranslation = translations[defaultLang]
36
+
37
+ if (!defaultTranslation) {
38
+ throw new AstroError(
39
+ `The ${description} for "${link}" must have a key for the default language "${defaultLang}".`,
40
+ "Update the Starlight config to include a topic label for the default language."
41
+ )
42
+ }
43
+
44
+ let translation = defaultTranslation
45
+
46
+ if (currentLocale) {
47
+ translation = translations[currentLocale] ?? defaultTranslation
48
+ }
49
+
50
+ return translation
51
+ }
@@ -0,0 +1,12 @@
1
+ import type { StarlightSidebarTopicsSharedConfig } from "./config"
2
+
3
+ /**
4
+ * We keep track of the current topic configuration using a variable stored using `Astro.locals`
5
+ * which will be reset for each new page. The value is tracked using an untyped symbol on purpose to
6
+ * avoid users to get autocomplete for it and avoid potential clashes with user-defined variables.
7
+ */
8
+ export const StarlightSidebarTopicsLocalsSymbol = Symbol.for("starlight-sidebar-topics:locals")
9
+
10
+ export interface StarlightSidebarTopicsLocals {
11
+ config: StarlightSidebarTopicsSharedConfig[number]
12
+ }
@@ -0,0 +1,22 @@
1
+ export function arePathnamesEqual(pathnameA: string, pathnameB: string) {
2
+ return stripLeadingAndTrailingSlashes(pathnameA) === stripLeadingAndTrailingSlashes(pathnameB)
3
+ }
4
+
5
+ export function stripLeadingAndTrailingSlashes(pathname: string): string {
6
+ return stripLeadingSlash(stripTrailingSlash(pathname))
7
+ }
8
+
9
+ export function stripLeadingSlash(pathname: string) {
10
+ if (pathname.startsWith("/")) pathname = pathname.slice(1)
11
+ return pathname
12
+ }
13
+
14
+ export function stripTrailingSlash(pathname: string) {
15
+ if (pathname.endsWith("/")) pathname = pathname.slice(0, -1)
16
+ return pathname
17
+ }
18
+
19
+ export function ensureLeadingSlash(pathname: string): string {
20
+ if (pathname.startsWith("/")) return pathname
21
+ return `/${pathname}`
22
+ }
@@ -0,0 +1,9 @@
1
+ import { AstroError } from "astro/errors"
2
+
3
+ export function throwPluginError(message: string, hint?: string): never {
4
+ throw new AstroError(
5
+ message,
6
+ hint ??
7
+ `See the error report above for more informations.\n\nIf you believe this is a bug, please file an issue at https://github.com/HiDeoo/starlight-sidebar-topics/issues/new/choose`
8
+ )
9
+ }
@@ -0,0 +1,183 @@
1
+ import type { StarlightRouteData } from "@astrojs/starlight/route-data"
2
+
3
+ import type { StarlightSidebarTopicsSharedConfig } from "./config"
4
+ import { isStarlightEntryWithTopic, type StarlightEntry } from "./content"
5
+ import { getLocaleFromSlug, getLocalizedSlug } from "./i18n"
6
+ import { arePathnamesEqual, stripLeadingAndTrailingSlashes } from "./pathname"
7
+
8
+ const absoluteLinkRegex = /^https?:\/\//
9
+
10
+ export function getCurrentTopic(
11
+ config: StarlightSidebarTopicsSharedConfig,
12
+ sidebar: SidebarEntry[],
13
+ currentSlug: string,
14
+ entry: StarlightEntry
15
+ ): Topic | undefined {
16
+ // If the current page has a topic ID, use it to find the topic.
17
+ const topicId = getTopicIdFromEntry(entry)
18
+ if (topicId) return getTopicById(config, sidebar, topicId)
19
+
20
+ // Start by checking if the current page is a topic root.
21
+ const topicFromSlug = getTopicFromSlug(config, sidebar, currentSlug)
22
+ if (topicFromSlug) return topicFromSlug
23
+
24
+ // Otherwise, find the current topic by looking for the current page in the sidebar.
25
+ const currentSidebarTopic = getCurrentSidebarTopic(sidebar)
26
+ if (!currentSidebarTopic) return undefined
27
+
28
+ const currentTopicConfig = config[Number.parseInt(currentSidebarTopic.label, 10)]
29
+ if (!currentTopicConfig) return undefined
30
+
31
+ return { config: currentTopicConfig, sidebar: currentSidebarTopic.entries }
32
+ }
33
+
34
+ export function isTopicFirstPage(sidebar: SidebarEntry[], currentSlug: string): boolean {
35
+ const currentSidebarTopic = getCurrentSidebarTopic(sidebar)
36
+ if (!currentSidebarTopic) return false
37
+
38
+ const firstPage = getSidebarFirstPage(currentSidebarTopic.entries)
39
+ if (!firstPage) return false
40
+
41
+ return arePathnamesEqual(stripLeadingAndTrailingSlashes(firstPage.href), currentSlug)
42
+ }
43
+
44
+ export function isTopicLastPage(sidebar: SidebarEntry[], currentSlug: string): boolean {
45
+ const currentSidebarTopic = getCurrentSidebarTopic(sidebar)
46
+ if (!currentSidebarTopic) return false
47
+
48
+ const lastPage = getSidebarLastPage(currentSidebarTopic.entries)
49
+ if (!lastPage) return false
50
+
51
+ return arePathnamesEqual(stripLeadingAndTrailingSlashes(lastPage.href), currentSlug)
52
+ }
53
+
54
+ function getSidebarFirstPage(sidebar: SidebarEntry[]) {
55
+ const entry = sidebar[0]
56
+ if (!entry) return undefined
57
+ if (entry.type === "link") return entry
58
+
59
+ return getSidebarFirstPage(entry.entries)
60
+ }
61
+
62
+ function getSidebarLastPage(sidebar: SidebarEntry[]) {
63
+ const entry = sidebar.at(-1)
64
+ if (!entry) return undefined
65
+ if (entry.type === "link") return entry
66
+
67
+ return getSidebarLastPage(entry.entries)
68
+ }
69
+
70
+ function getTopicFromSlug(
71
+ config: StarlightSidebarTopicsSharedConfig,
72
+ sidebar: SidebarEntry[],
73
+ slug: string
74
+ ): Topic | undefined {
75
+ let topicConfig: Topic["config"] | undefined
76
+ let topicSidebar: Topic["sidebar"] | undefined
77
+
78
+ // Start by checking if the current page is a topic homepage.
79
+ let groupTopicIndex = -1
80
+ const slugLocale = getLocaleFromSlug(slug)
81
+
82
+ for (const topic of config) {
83
+ if (topic.type === "group") groupTopicIndex++
84
+
85
+ if (
86
+ !absoluteLinkRegex.test(topic.link) &&
87
+ arePathnamesEqual(
88
+ getLocalizedSlug(stripLeadingAndTrailingSlashes(topic.link), slugLocale),
89
+ slug
90
+ ) &&
91
+ groupTopicIndex !== -1
92
+ ) {
93
+ const sidebarTopic = sidebar[groupTopicIndex]
94
+
95
+ if (sidebarTopic?.type === "group") {
96
+ topicConfig = topic
97
+ topicSidebar = sidebarTopic.entries
98
+ }
99
+
100
+ break
101
+ }
102
+ }
103
+
104
+ if (!topicConfig || !topicSidebar) return undefined
105
+
106
+ return { config: topicConfig, sidebar: topicSidebar }
107
+ }
108
+
109
+ export function getTopicById(
110
+ config: StarlightSidebarTopicsSharedConfig,
111
+ sidebar: SidebarEntry[],
112
+ id: string
113
+ ): Topic | undefined {
114
+ let topicConfig: Topic["config"] | undefined
115
+ let topicSidebar: Topic["sidebar"] | undefined
116
+
117
+ let groupTopicIndex = -1
118
+
119
+ for (const topic of config) {
120
+ if (topic.type === "group") groupTopicIndex++
121
+
122
+ if (topic.type === "group" && topic.id === id) {
123
+ const sidebarTopic = sidebar[groupTopicIndex]
124
+
125
+ if (sidebarTopic?.type === "group") {
126
+ topicConfig = topic
127
+ topicSidebar = sidebarTopic.entries
128
+ }
129
+
130
+ break
131
+ }
132
+ }
133
+
134
+ if (!topicConfig || !topicSidebar) return undefined
135
+
136
+ return { config: topicConfig, sidebar: topicSidebar }
137
+ }
138
+
139
+ function getCurrentSidebarTopic(sidebar: SidebarEntry[]): SidebarTopic | undefined {
140
+ let currentSidebarTopic: SidebarTopic | undefined
141
+
142
+ for (const topic of sidebar) {
143
+ if (topic.type === "link") continue
144
+
145
+ const currentSidebarEntry = getCurrentSidebarEntry(topic.entries)
146
+
147
+ if (currentSidebarEntry) {
148
+ currentSidebarTopic = topic
149
+ break
150
+ }
151
+ }
152
+
153
+ return currentSidebarTopic
154
+ }
155
+
156
+ function getCurrentSidebarEntry(sidebar: SidebarEntry[]): SidebarEntry | undefined {
157
+ return sidebar.find((entry) => {
158
+ if (entry.type === "link") {
159
+ return entry.isCurrent
160
+ }
161
+
162
+ return getCurrentSidebarEntry(entry.entries)
163
+ })
164
+ }
165
+
166
+ function getTopicIdFromEntry(entry: StarlightEntry): string | undefined {
167
+ if (isStarlightEntryWithTopic(entry)) {
168
+ return entry.data.topic
169
+ }
170
+ return undefined
171
+ }
172
+
173
+ type SidebarEntry = StarlightRouteData["sidebar"][number]
174
+
175
+ interface SidebarTopic {
176
+ label: string
177
+ entries: SidebarEntry[]
178
+ }
179
+
180
+ export interface Topic {
181
+ config: StarlightSidebarTopicsSharedConfig[number]
182
+ sidebar: SidebarEntry[]
183
+ }
@@ -0,0 +1,46 @@
1
+ import type { ViteUserConfig } from "astro"
2
+
3
+ import type {
4
+ StarlightSidebarTopicsConfig,
5
+ StarlightSidebarTopicsOptions,
6
+ StarlightSidebarTopicsSharedConfig
7
+ } from "./config"
8
+
9
+ export function vitePluginStarlightSidebarTopics(
10
+ config: StarlightSidebarTopicsConfig,
11
+ options: StarlightSidebarTopicsOptions
12
+ ): VitePlugin {
13
+ const sharedConfig: StarlightSidebarTopicsSharedConfig = config.map((topic) => {
14
+ if (!("items" in topic)) return { ...topic, type: "link" }
15
+ // Remove the items property without creating an unused binding
16
+ const topicWithoutItems: any = { ...topic }
17
+ delete topicWithoutItems.items
18
+ return { ...topicWithoutItems, type: "group" }
19
+ })
20
+
21
+ const modules: Record<string, string> = {
22
+ "virtual:starlight-sidebar-topics/config": `export default ${JSON.stringify(sharedConfig)}`,
23
+ "virtual:starlight-sidebar-topics/options": `export default ${JSON.stringify(options)}`
24
+ }
25
+
26
+ const moduleResolutionMap = Object.fromEntries(
27
+ Object.keys(modules).map((key) => [resolveVirtualModuleId(key), key])
28
+ ) as Record<string, string>
29
+
30
+ return {
31
+ name: "vite-plugin-starlight-sidebar-topics",
32
+ load(id) {
33
+ const moduleId = moduleResolutionMap[id]
34
+ return moduleId ? modules[moduleId] : undefined
35
+ },
36
+ resolveId(id) {
37
+ return id in modules ? resolveVirtualModuleId(id) : undefined
38
+ }
39
+ }
40
+ }
41
+
42
+ function resolveVirtualModuleId<TModuleId extends string>(id: TModuleId): `\0${TModuleId}` {
43
+ return `\0${id}`
44
+ }
45
+
46
+ type VitePlugin = NonNullable<ViteUserConfig["plugins"]>[number]
@@ -0,0 +1,14 @@
1
+ import type { StarlightLocals } from "@astrojs/starlight/types"
2
+ import type { StarlightSidebarTopicsRouteData } from "./data"
3
+
4
+ declare namespace App {
5
+ interface Locals extends StarlightLocals {
6
+ /**
7
+ * Starlight Sidebar Topics data.
8
+ *
9
+ * @see https://starlight-sidebar-topics.netlify.app/docs/guides/custom-topic-list/
10
+ */
11
+ starlightSidebarTopics: StarlightSidebarTopicsRouteData
12
+ starlightRoute: any
13
+ }
14
+ }