starlight-versions 0.9.1 → 0.10.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # starlight-versions
2
2
 
3
+ ## 0.10.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#80](https://github.com/HiDeoo/starlight-versions/pull/80) [`dd385c8`](https://github.com/HiDeoo/starlight-versions/commit/dd385c88940cc36e5733d0f082a04907f44bb2f0) Thanks [@kozmagabor](https://github.com/kozmagabor)! - Fixes versioning of assets stored alongside documentation pages.
8
+
9
+ ## 0.10.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#76](https://github.com/HiDeoo/starlight-versions/pull/76) [`a4134e7`](https://github.com/HiDeoo/starlight-versions/commit/a4134e7ac0df8778367f4ada77c3b5ee8976cc06) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Adds support for excluding pages from versioning.
14
+
15
+ ### Patch Changes
16
+
17
+ - [#79](https://github.com/HiDeoo/starlight-versions/pull/79) [`4329441`](https://github.com/HiDeoo/starlight-versions/commit/4329441a853506584a2e9c43429b83a14a5152a6) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fixes pagination links when using Astro's `base` configuration option with multiple segments.
18
+
19
+ - [#79](https://github.com/HiDeoo/starlight-versions/pull/79) [`4329441`](https://github.com/HiDeoo/starlight-versions/commit/4329441a853506584a2e9c43429b83a14a5152a6) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fixes invalid versioned links for paths similar to Astro's `base` configuration option.
20
+
3
21
  ## 0.9.1
4
22
 
5
23
  ### Patch Changes
@@ -10,7 +10,7 @@ import project from 'virtual:starlight/project-context'
10
10
  import starlightConfig from 'virtual:starlight/user-config'
11
11
  import starlightVersionsConfig from 'virtual:starlight-versions-config'
12
12
 
13
- import { getVersionFromSlug, getVersionIdentifier, getVersionURL } from '../libs/versions'
13
+ import { getPagefindVersionIdentifiers, getVersionFromSlug, getVersionURL } from '../libs/versions'
14
14
 
15
15
  const { entry } = Astro.locals.starlightRoute
16
16
 
@@ -24,7 +24,11 @@ const pagefindTranslations = {
24
24
  }
25
25
 
26
26
  const pageVersion = getVersionFromSlug(starlightVersionsConfig, starlightConfig, entry.id)
27
- const pageVersionIdentifier = getVersionIdentifier(pageVersion)
27
+ const [pageVersionIdentifier, ...otherPageVersionsIdentifiers] = getPagefindVersionIdentifiers(
28
+ starlightVersionsConfig,
29
+ pageVersion,
30
+ entry.id,
31
+ )
28
32
 
29
33
  const dataAttributes: DOMStringMap = {
30
34
  'data-pagefind-filter': `version:${pageVersionIdentifier}`,
@@ -35,6 +39,7 @@ if (project.trailingSlash === 'never') dataAttributes['data-strip-trailing-slash
35
39
  ---
36
40
 
37
41
  <site-search class={Astro.props['class']} {...dataAttributes}>
42
+ {otherPageVersionsIdentifiers.map((version) => <span hidden data-pagefind-filter={`version:${version}`} />)}
38
43
  <button data-open-modal disabled aria-label={Astro.locals.t('search.label')} aria-keyshortcuts="Control+K">
39
44
  <Icon name="magnifier" />
40
45
  <span class="sl-hidden md:sl-block" aria-hidden="true">{Astro.locals.t('search.label')}</span>
@@ -3,37 +3,41 @@ import { Icon } from '@astrojs/starlight/components'
3
3
  import starlightConfig from 'virtual:starlight/user-config'
4
4
  import starlightVersionsConfig from 'virtual:starlight-versions-config'
5
5
 
6
- import { getVersionFromSlug, getVersionURL } from '../libs/versions'
6
+ import { getVersionFromSlug, getVersionURL, isExcludedDocPath } from '../libs/versions'
7
7
 
8
- const { id } = Astro.locals.starlightRoute
8
+ const { entry, id } = Astro.locals.starlightRoute
9
9
 
10
10
  const pageVersion = getVersionFromSlug(starlightVersionsConfig, starlightConfig, id)
11
+ const isExcluded = pageVersion === undefined && isExcludedDocPath(starlightVersionsConfig, entry.filePath)
11
12
  ---
12
13
 
13
- <starlight-version-select>
14
- <label>
15
- <span class="sr-only">{Astro.locals.t('starlightVersions.select.accessibleLabel')}</span>
16
- <Icon name="seti:clock" class="icon label-icon" />
17
- <select value={pageVersion?.label ?? 'current'} autocomplete="off">
18
- <option
19
- selected={pageVersion === undefined}
20
- value={getVersionURL(starlightVersionsConfig, starlightConfig, Astro.url, undefined).pathname}
21
- >{starlightVersionsConfig.current.label}</option
22
- >
23
- {
24
- starlightVersionsConfig.versions.map((version) => (
14
+ {
15
+ !isExcluded && (
16
+ <starlight-version-select>
17
+ <label>
18
+ <span class="sr-only">{Astro.locals.t('starlightVersions.select.accessibleLabel')}</span>
19
+ <Icon name="seti:clock" class="icon label-icon" />
20
+ <select value={pageVersion?.label ?? 'current'} autocomplete="off">
25
21
  <option
26
- selected={version.slug === pageVersion?.slug}
27
- value={getVersionURL(starlightVersionsConfig, starlightConfig, Astro.url, version).pathname}
22
+ selected={pageVersion === undefined}
23
+ value={getVersionURL(starlightVersionsConfig, starlightConfig, Astro.url, undefined).pathname}
28
24
  >
29
- {version.label ?? version.slug}
25
+ {starlightVersionsConfig.current.label}
30
26
  </option>
31
- ))
32
- }
33
- </select>
34
- <Icon name="down-caret" class="icon caret-icon" />
35
- </label>
36
- </starlight-version-select>
27
+ {starlightVersionsConfig.versions.map((version) => (
28
+ <option
29
+ selected={version.slug === pageVersion?.slug}
30
+ value={getVersionURL(starlightVersionsConfig, starlightConfig, Astro.url, version).pathname}
31
+ >
32
+ {version.label ?? version.slug}
33
+ </option>
34
+ ))}
35
+ </select>
36
+ <Icon name="down-caret" class="icon caret-icon" />
37
+ </label>
38
+ </starlight-version-select>
39
+ )
40
+ }
37
41
 
38
42
  <style>
39
43
  label {
package/libs/config.ts CHANGED
@@ -18,6 +18,14 @@ export const StarlightVersionsConfigSchema = z
18
18
  label: z.string().default('Latest'),
19
19
  })
20
20
  .prefault({}),
21
+ /**
22
+ * A list of source file glob patterns that should be excluded from versioning.
23
+ *
24
+ * Excluded pages always use the latest content across all versions.
25
+ *
26
+ * @default []
27
+ */
28
+ exclude: z.array(z.string()).default([]),
21
29
  /**
22
30
  * A list of all archived versions of the documentation.
23
31
  */
@@ -25,10 +33,16 @@ export const StarlightVersionsConfigSchema = z
25
33
  message: 'At least one version of the documentation must be defined.',
26
34
  }),
27
35
  })
28
- .transform((value) => ({
29
- ...value,
30
- versionsBySlug: Object.fromEntries(value.versions.map((version) => [version.slug, version])),
31
- }))
36
+ .transform((value) => {
37
+ // This gets populated later when generating versioned sidebars.
38
+ const excludedSlugsByVersion: Record<string, string[]> = {}
39
+
40
+ return {
41
+ ...value,
42
+ excludedSlugsByVersion,
43
+ versionsBySlug: Object.fromEntries(value.versions.map((version) => [version.slug, version])),
44
+ }
45
+ })
32
46
 
33
47
  export type StarlightVersionsUserConfig = z.input<typeof StarlightVersionsConfigSchema>
34
48
  export type StarlightVersionsConfig = z.output<typeof StarlightVersionsConfigSchema>
package/libs/fs.ts CHANGED
@@ -32,13 +32,17 @@ export async function copyDirectory(sourceDir: URL, destDir: URL, callback: Copy
32
32
  await copyDirectory(source, dest, callback, false)
33
33
  } else if (entry.isFile()) {
34
34
  const source = new URL(entry.name, sourceDir)
35
- const content = await fs.readFile(source, 'utf8')
36
35
 
37
- const updatedContent = await callback({ type: 'file', content, url: source })
36
+ const updatedContent = await callback({ type: 'file', url: source })
37
+ if (updatedContent === true) continue
38
38
 
39
- if (typeof updatedContent !== 'string') continue
39
+ const dest = new URL(entry.name, destDir)
40
40
 
41
- await fs.writeFile(new URL(entry.name, destDir), updatedContent)
41
+ if (updatedContent === undefined) {
42
+ await copyFile(source, dest)
43
+ } else if (typeof updatedContent === 'string') {
44
+ await fs.writeFile(dest, updatedContent)
45
+ }
42
46
  }
43
47
  }
44
48
  }
@@ -72,8 +76,12 @@ export async function isDirectoryEntry(entry: Dirent) {
72
76
  return stats.isDirectory()
73
77
  }
74
78
 
79
+ /**
80
+ * Defines how entries are handled when copying a directory with `copyDirectory()`.
81
+ *
82
+ * - For files, return `undefined` to copy as-is, a string to write transformed content, or `true` to skip.
83
+ * - For directories, return `undefined` to use the default destination, a `URL` to override it, or `true` to skip.
84
+ */
75
85
  export type CopyDirectoryCallback = (
76
- entry:
77
- | { type: 'file'; content: string; url: URL }
78
- | { type: 'directory'; name: string; isRoot: boolean; source: URL; dest: URL },
79
- ) => Promise<string | boolean | URL>
86
+ entry: { type: 'file'; url: URL } | { type: 'directory'; name: string; isRoot: boolean; source: URL; dest: URL },
87
+ ) => Promise<string | true | URL | undefined>
package/libs/markdown.ts CHANGED
@@ -10,7 +10,13 @@ import remarkMdx from 'remark-mdx'
10
10
  import { CONTINUE, SKIP, visit } from 'unist-util-visit'
11
11
  import type { VFile } from 'vfile'
12
12
 
13
- import { isAbsoluteLink, stripLeadingSlash, stripTrailingSlash } from './path'
13
+ import {
14
+ ensureTrailingSlash,
15
+ isAbsoluteLink,
16
+ stripLeadingAndTrailingSlashes,
17
+ stripLeadingSlash,
18
+ stripTrailingSlash,
19
+ } from './path'
14
20
  import { getFrontmatterNodeValue, parseFrontmatter } from './starlight'
15
21
  import type { Version, VersionAsset } from './versions'
16
22
 
@@ -51,6 +57,10 @@ export function remarkStarlightVersions() {
51
57
  return handleImports(node, file)
52
58
  }
53
59
  case 'mdxJsxFlowElement': {
60
+ if (node.name === 'a') {
61
+ return handleLinkElements(node, file)
62
+ }
63
+
54
64
  if (node.name && mediaElements.has(node.name)) {
55
65
  return handleMediaElements(node, file)
56
66
  }
@@ -99,19 +109,17 @@ function handleFrontmatter(tree: Root, file: VFile) {
99
109
  }
100
110
  }
101
111
 
102
- if (typeof frontmatter.prev === 'object' && frontmatter.prev.link?.startsWith('/')) {
103
- frontmatter.prev.link = addVersionToLink(frontmatter.prev.link, file)
112
+ if (typeof frontmatter.prev === 'object' && frontmatter.prev.link) {
113
+ frontmatter.prev.link = transformLink(frontmatter.prev.link, file)
104
114
  }
105
115
 
106
- if (typeof frontmatter.next === 'object' && frontmatter.next.link?.startsWith('/')) {
107
- frontmatter.next.link = addVersionToLink(frontmatter.next.link, file)
116
+ if (typeof frontmatter.next === 'object' && frontmatter.next.link) {
117
+ frontmatter.next.link = transformLink(frontmatter.next.link, file)
108
118
  }
109
119
 
110
120
  if (frontmatter.hero?.actions) {
111
121
  for (const action of frontmatter.hero.actions) {
112
- if (action.link.startsWith('/')) {
113
- action.link = addVersionToLink(action.link, file)
114
- }
122
+ action.link = transformLink(action.link, file)
115
123
  }
116
124
  }
117
125
 
@@ -134,23 +142,50 @@ function handleFrontmatter(tree: Root, file: VFile) {
134
142
  }
135
143
 
136
144
  function handleLinks(node: Link, file: VFile) {
137
- if (!isPublicAsset(node.url)) return SKIP
138
-
139
- node.url = addVersionToLink(node.url, file)
145
+ node.url = transformLink(node.url, file)
140
146
 
141
147
  return SKIP
142
148
  }
143
149
 
144
- function handleLinkElements(node: MdxJsxTextElement, file: VFile) {
150
+ function handleLinkElements(node: MdxJsxFlowElement | MdxJsxTextElement, file: VFile) {
145
151
  const href = node.attributes.find((attribute) => attribute.type === 'mdxJsxAttribute' && attribute.name === 'href')
146
152
 
147
- if (!href || typeof href.value !== 'string' || !isPublicAsset(href.value)) return CONTINUE
153
+ if (!href || typeof href.value !== 'string') return CONTINUE
148
154
 
149
- href.value = addVersionToLink(href.value, file)
155
+ href.value = transformLink(href.value, file)
150
156
 
151
157
  return CONTINUE
152
158
  }
153
159
 
160
+ function transformLink(link: string, file: VFile) {
161
+ if (isAbsoluteLink(link)) return link
162
+
163
+ const excludedLink = getExcludedLink(link, file)
164
+ if (excludedLink) return excludedLink
165
+
166
+ return isPublicAsset(link) ? addVersionToLink(link, file) : link
167
+ }
168
+
169
+ function getExcludedLink(link: string, file: VFile) {
170
+ const { excludedSlugs, slug } = file.data
171
+
172
+ if (!excludedSlugs || !slug || link.startsWith('#') || link.startsWith('?')) return undefined
173
+
174
+ const isAbsolute = link.startsWith('/')
175
+ const url = new URL(link, `https://example.com/${stripLeadingAndTrailingSlashes(slug)}/`)
176
+ let pathname = url.pathname
177
+ const base = file.data.base ?? ''
178
+
179
+ if (base && (pathname === base || pathname.startsWith(ensureTrailingSlash(base)))) {
180
+ pathname = pathname.slice(base.length)
181
+ }
182
+
183
+ if (!excludedSlugs.includes(stripLeadingAndTrailingSlashes(pathname))) return undefined
184
+ if (isAbsolute) return link
185
+
186
+ return `${base}${pathname}${url.search}${url.hash}`
187
+ }
188
+
154
189
  function handleImages(node: Image, file: VFile) {
155
190
  if (isAbsoluteLink(node.url)) return SKIP
156
191
 
@@ -189,10 +224,10 @@ function addVersionToLink(link: string, file: VFile) {
189
224
  assert.ok(file.data.version, 'A version must be provided to add a version to an Astro asset.')
190
225
 
191
226
  const base = file.data.base ?? ''
192
- const hasBase = file.data.base && link.startsWith(file.data.base)
227
+ const hasBase = base && (link === base || link.startsWith(ensureTrailingSlash(base)))
193
228
 
194
229
  if (hasBase) {
195
- link = link.replace(base, '')
230
+ link = link.slice(base.length)
196
231
  }
197
232
 
198
233
  const segments = link.split('/')
@@ -211,8 +246,10 @@ function addVersionToLink(link: string, file: VFile) {
211
246
 
212
247
  function addVersionToAstroAsset(asset: string, file: VFile) {
213
248
  assert.ok(file.data.version, 'A version must be provided to add a version to an Astro asset.')
249
+ assert.ok(file.data.docsDir, 'A docs directory must be provided to add a version to an Astro asset.')
214
250
 
215
251
  const source = new URL(asset, file.data.url)
252
+ if (source.href.startsWith(file.data.docsDir.href)) return asset
216
253
 
217
254
  const segments = asset.split('/')
218
255
  segments.splice(-1, 0, file.data.version.slug)
@@ -252,6 +289,8 @@ function isPublicAsset(asset: string) {
252
289
  export interface TransformContext {
253
290
  assets: VersionAsset[]
254
291
  base: string
292
+ docsDir: URL
293
+ excludedSlugs: string[]
255
294
  locale: string | undefined
256
295
  publicDir: URL
257
296
  slug: string
package/libs/path.ts CHANGED
@@ -2,7 +2,7 @@ import path from 'node:path'
2
2
 
3
3
  import { slug } from 'github-slugger'
4
4
 
5
- const absoluteLinkRegex = /^https?:\/\//
5
+ const absoluteLinkRegex = /^[a-z][a-z\d+.-]*:/i
6
6
 
7
7
  export function ensureTrailingSlash(filePath: string): string {
8
8
  if (filePath.endsWith('/')) {
@@ -28,6 +28,10 @@ export function stripTrailingSlash(filePath: string) {
28
28
  return filePath.slice(0, -1)
29
29
  }
30
30
 
31
+ export function stripLeadingAndTrailingSlashes(filePath: string) {
32
+ return stripLeadingSlash(stripTrailingSlash(filePath))
33
+ }
34
+
31
35
  export function slugifyPath(filePath: string): string {
32
36
  const segments = stripExtension(filePath).split('/')
33
37
 
package/libs/starlight.ts CHANGED
@@ -7,7 +7,16 @@ import type { HookParameters } from '@astrojs/starlight/types'
7
7
  import type { z } from 'astro/zod'
8
8
  import yaml from 'yaml'
9
9
 
10
- import { isAbsoluteLink, slugifyPath } from './path'
10
+ import {
11
+ ensureTrailingSlash,
12
+ isAbsoluteLink,
13
+ slugifyPath,
14
+ stripLeadingAndTrailingSlashes,
15
+ stripTrailingSlash,
16
+ } from './path'
17
+
18
+ const frontmatterRegex = /^---\r?\n(?<yaml>.*?)\r?\n---(?:\r?\n|$)/s
19
+ const queryOrFragmentRegex = /[?#]/
11
20
 
12
21
  export function parseFrontmatter(frontmatter: string) {
13
22
  return yaml.parse(frontmatter) as StarlightFrontmatter
@@ -17,13 +26,45 @@ export function getFrontmatterNodeValue(frontmatter: StarlightFrontmatter) {
17
26
  return yaml.stringify(frontmatter).trim()
18
27
  }
19
28
 
29
+ export function getDocPath(docsDir: URL, doc: URL) {
30
+ return path.relative(url.fileURLToPath(docsDir), url.fileURLToPath(doc)).replaceAll('\\', '/')
31
+ }
32
+
20
33
  export function getDocSlug(docsDir: URL, doc: URL) {
21
- const docPath = path.relative(url.fileURLToPath(docsDir), url.fileURLToPath(doc)).replaceAll('\\', '/')
22
- const slug = slugifyPath(docPath)
34
+ const slug = slugifyPath(getDocPath(docsDir, doc))
23
35
 
24
36
  return slug === 'index' ? '/' : slug.replace(/\/index$/, '')
25
37
  }
26
38
 
39
+ export function resolveDocSlug(docsDir: URL, doc: URL, content: string) {
40
+ const frontmatter = frontmatterRegex.exec(content)?.groups?.['yaml']
41
+ const customSlug = frontmatter ? parseFrontmatter(frontmatter).slug : undefined
42
+
43
+ return stripLeadingAndTrailingSlashes(typeof customSlug === 'string' ? customSlug : getDocSlug(docsDir, doc))
44
+ }
45
+
46
+ // https://github.com/withastro/starlight/blob/ff5090a8fd7ae068b29e3c5af7197c5e5afdfa4e/packages/starlight/utils/slugs.ts#L104
47
+ export function getURLSlug(url: URL) {
48
+ let pathname = decodeURI(url.pathname)
49
+
50
+ const base = stripTrailingSlash(import.meta.env.BASE_URL)
51
+ if (pathname === base || pathname.startsWith(ensureTrailingSlash(base))) pathname = pathname.slice(base.length)
52
+
53
+ const segments = pathname.split('/')
54
+ const htmlExt = '.html'
55
+
56
+ if (segments.at(-1) === 'index.html') {
57
+ // Remove trailing `index.html`.
58
+ segments.pop()
59
+ } else if (segments.at(-1)?.endsWith(htmlExt)) {
60
+ // Remove trailing `.html`.
61
+ const last = segments.pop()
62
+ if (last) segments.push(last.slice(0, -1 * htmlExt.length))
63
+ }
64
+
65
+ return segments.filter(Boolean).join('/')
66
+ }
67
+
27
68
  export function getDocLocale(slug: string, starlightConfig: StarlightUserConfig): string | undefined {
28
69
  const locales = Object.keys(starlightConfig.locales ?? {})
29
70
 
@@ -36,14 +77,22 @@ export function getDocLocale(slug: string, starlightConfig: StarlightUserConfig)
36
77
  return locales.find((locale) => locale === localeSegment)
37
78
  }
38
79
 
39
- export function addPrefixToSidebarConfig(prefix: string, sidebar: SidebarConfigInputItem[]): SidebarConfig {
80
+ export function getSidebarLinks(sidebar: StarlightSidebar): Extract<StarlightSidebar[number], { type: 'link' }>[] {
81
+ return sidebar.flatMap((entry) => (entry.type === 'group' ? getSidebarLinks(entry.entries) : entry))
82
+ }
83
+
84
+ export function addPrefixToSidebarConfig(
85
+ prefix: string,
86
+ sidebar: SidebarConfigInputItem[],
87
+ excludedSlugs: string[] = [],
88
+ ): SidebarConfig {
40
89
  return sidebar.map((item) => {
41
90
  if (typeof item === 'string') {
42
- return addPrefixToSlug(prefix, item)
91
+ return isExcludedInternalSidebarSlug(item, excludedSlugs) ? item : addPrefixToSlug(prefix, item)
43
92
  } else if ('items' in item) {
44
93
  return {
45
94
  ...item,
46
- items: addPrefixToSidebarConfig(prefix, item.items),
95
+ items: addPrefixToSidebarConfig(prefix, item.items, excludedSlugs),
47
96
  }
48
97
  } else if (isSidebarLegacyAutoGroup(item)) {
49
98
  return addPrefixToLegacyAutogeneratedGroup(prefix, item)
@@ -55,12 +104,15 @@ export function addPrefixToSidebarConfig(prefix: string, sidebar: SidebarConfigI
55
104
  } else if ('slug' in item) {
56
105
  return {
57
106
  ...item,
58
- slug: addPrefixToSlug(prefix, item.slug),
107
+ slug: isExcludedInternalSidebarSlug(item.slug, excludedSlugs) ? item.slug : addPrefixToSlug(prefix, item.slug),
59
108
  }
60
109
  } else if (isAbsoluteLink(item.link)) {
61
110
  return item
62
111
  }
63
112
 
113
+ const link = item.link.startsWith('./') ? item.link.slice(2) : item.link
114
+ if (isExcludedSidebarSlug(link, excludedSlugs)) return item
115
+
64
116
  const segments = item.link.split('/')
65
117
  segments.splice(/^[./]/.test(item.link) ? 1 : 0, 0, prefix)
66
118
 
@@ -71,6 +123,16 @@ export function addPrefixToSidebarConfig(prefix: string, sidebar: SidebarConfigI
71
123
  })
72
124
  }
73
125
 
126
+ function isExcludedSidebarSlug(value: string, excludedSlugs: string[]) {
127
+ const pathname = value.split(queryOrFragmentRegex)[0] ?? ''
128
+ const slug = stripLeadingAndTrailingSlashes(pathname)
129
+ return excludedSlugs.includes(slug)
130
+ }
131
+
132
+ function isExcludedInternalSidebarSlug(slug: string, excludedSlugs: string[]) {
133
+ return isExcludedSidebarSlug(slug === 'index' ? '' : slug, excludedSlugs)
134
+ }
135
+
74
136
  function isSidebarLegacyAutoGroup(item: SidebarConfigInputItem): item is SidebarLegacyAutoGroup {
75
137
  return typeof item === 'object' && 'autogenerate' in item && 'label' in item
76
138
  }
package/libs/versions.ts CHANGED
@@ -1,9 +1,12 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
1
4
  import type { StarlightConfig } from '@astrojs/starlight/types'
2
5
  import type { AstroConfig, AstroIntegrationLogger } from 'astro'
3
6
  import { z } from 'astro/zod'
4
7
 
5
8
  import type { StarlightVersionsConfig } from '..'
6
- import type { DocsVersionsConfig } from '../schema'
9
+ import { docsVersionsSchema, type DocsVersionsConfig } from '../schema'
7
10
 
8
11
  import {
9
12
  copyDirectory,
@@ -15,10 +18,19 @@ import {
15
18
  writeJSONFile,
16
19
  } from './fs'
17
20
  import { transformMarkdown } from './markdown'
18
- import { ensureTrailingSlash, getExtension, stripExtension, stripLeadingSlash, stripTrailingSlash } from './path'
21
+ import {
22
+ ensureTrailingSlash,
23
+ getExtension,
24
+ stripExtension,
25
+ stripLeadingAndTrailingSlashes,
26
+ stripTrailingSlash,
27
+ } from './path'
19
28
  import { throwPluginError } from './plugin'
20
29
  import {
21
30
  getDocSlug,
31
+ getDocPath,
32
+ getURLSlug,
33
+ resolveDocSlug,
22
34
  type StarlightSidebarUserConfig,
23
35
  type StarlightUserConfig,
24
36
  addPrefixToSidebarConfig,
@@ -28,6 +40,9 @@ import {
28
40
 
29
41
  const currentVersionSidebarGroupLabel = Symbol('StarlightVersionsCurrentVersionSidebarGroupLabel')
30
42
 
43
+ // https://github.com/withastro/starlight/blob/23207221169b31e0cc210451a39a19c80fa1f69e/packages/starlight/loaders.ts#L6
44
+ const docsExtensions = new Set(['.markdown', '.mdown', '.mkdn', '.mkd', '.mdwn', '.md', '.mdx'])
45
+
31
46
  export const VersionBaseSchema = z.object({
32
47
  /**
33
48
  * The version redirect strategy used when navigating to this version:
@@ -56,7 +71,7 @@ export const VersionSchema = z.object({
56
71
  * @example '2.0'
57
72
  * @example '3-1-2'
58
73
  */
59
- slug: z.string().refine((value) => stripLeadingSlash(stripTrailingSlash(value))),
74
+ slug: z.string().refine((value) => stripLeadingAndTrailingSlashes(value)),
60
75
  })
61
76
 
62
77
  export async function ensureNewVersion(
@@ -71,6 +86,13 @@ export async function ensureNewVersion(
71
86
 
72
87
  if (!newVersion) return
73
88
 
89
+ const excludedDocs = await getExcludedDocs(
90
+ config,
91
+ locales.filter((locale) => locale !== 'root'),
92
+ docsDir,
93
+ )
94
+ const excludedSlugs = [...excludedDocs.values()].toSorted()
95
+
74
96
  const assets: VersionAsset[] = []
75
97
 
76
98
  await copyDirectory(docsDir, new URL(ensureTrailingSlash(newVersion.slug), docsDir), async (entry) => {
@@ -86,7 +108,7 @@ export async function ensureNewVersion(
86
108
  }
87
109
 
88
110
  // Do not skip other non-root directories.
89
- return false
111
+ return
90
112
  }
91
113
 
92
114
  // Skip root version directories.
@@ -95,17 +117,22 @@ export async function ensureNewVersion(
95
117
  const localeDir = locales.find((locale) => locale === entry.name)
96
118
 
97
119
  // Copy root directories not matching any locale.
98
- if (!localeDir) return false
120
+ if (!localeDir) return
99
121
 
100
122
  // Otherwise, swap the locale and version directories.
101
123
  return new URL(`../../${localeDir}/${newVersion.slug}/`, entry.dest)
102
124
  }
103
125
 
126
+ if (!docsExtensions.has(getExtension(entry.url.pathname))) return
127
+ if (excludedDocs.has(getDocPath(docsDir, entry.url))) return true
128
+
104
129
  const slug = getDocSlug(docsDir, entry.url)
105
130
 
106
- const md = await transformMarkdown(entry.content, {
131
+ const md = await transformMarkdown(await fs.readFile(entry.url, 'utf8'), {
107
132
  assets: [],
108
133
  base: stripTrailingSlash(astroConfig.base),
134
+ docsDir,
135
+ excludedSlugs,
109
136
  locale: getDocLocale(slug, starlightConfig),
110
137
  publicDir: astroConfig.publicDir,
111
138
  slug,
@@ -122,7 +149,7 @@ export async function ensureNewVersion(
122
149
  await copyFile(asset.source, asset.dest)
123
150
  }
124
151
 
125
- await makeVersionConfig(newVersion, starlightConfig, astroConfig.srcDir)
152
+ await makeVersionConfig(newVersion, starlightConfig, astroConfig.srcDir, excludedSlugs)
126
153
 
127
154
  logger.info(`Created new version '${newVersion.slug}'.`)
128
155
  }
@@ -139,11 +166,24 @@ export async function getVersionedSidebar(
139
166
  },
140
167
  ]
141
168
 
169
+ const excludedSlugsByVersion: Record<string, string[]> = {}
170
+
142
171
  for (const version of config.versions) {
143
- const versionSidebar = await getSidebarVersionGroup(version, srcDir)
144
- sidebar.push(versionSidebar)
172
+ const versionConfig = await getVersionConfig(version, srcDir)
173
+ const excludedSlugs = versionConfig.excluded
174
+
175
+ excludedSlugsByVersion[version.slug] = excludedSlugs
176
+
177
+ sidebar.push({
178
+ label: version.slug,
179
+ items: versionConfig.sidebar
180
+ ? addPrefixToSidebarConfig(version.slug, versionConfig.sidebar, excludedSlugs)
181
+ : [{ autogenerate: { directory: version.slug } }],
182
+ })
145
183
  }
146
184
 
185
+ config.excludedSlugsByVersion = excludedSlugsByVersion
186
+
147
187
  return sidebar
148
188
  }
149
189
 
@@ -170,15 +210,26 @@ export function getVersionURL(
170
210
  url: URL,
171
211
  version: Version | undefined,
172
212
  ): URL {
213
+ const canonicalSlug = getCanonicalSlug(config, starlightConfig, url)
214
+
215
+ if (version && isSlugExcludedFromVersion(config, version, canonicalSlug)) {
216
+ return getVersionURL(
217
+ { ...config, current: { ...config.current, redirect: 'same-page' } },
218
+ starlightConfig,
219
+ url,
220
+ undefined,
221
+ )
222
+ }
223
+
173
224
  const versionURL = new URL(url)
174
225
  const versionSlug = version?.slug ?? ''
175
226
  const versionRedirect = version?.redirect ?? config.current.redirect
176
227
 
177
228
  const base = stripTrailingSlash(import.meta.env.BASE_URL)
178
- const hasBase = versionURL.pathname.startsWith(base)
229
+ const hasBase = versionURL.pathname === base || versionURL.pathname.startsWith(ensureTrailingSlash(base))
179
230
 
180
231
  if (hasBase) {
181
- versionURL.pathname = versionURL.pathname.replace(base, '')
232
+ versionURL.pathname = versionURL.pathname.slice(base.length)
182
233
  }
183
234
 
184
235
  let baseSegment: string | undefined
@@ -243,6 +294,42 @@ export function getVersionURL(
243
294
  return versionURL
244
295
  }
245
296
 
297
+ export function getCanonicalSlug(config: StarlightVersionsConfig, starlightConfig: StarlightConfig, url: URL) {
298
+ const slug = getURLSlug(url)
299
+ const version = getVersionFromSlug(config, starlightConfig, slug)
300
+ return version ? stripVersionSegment(slug, version.slug) : slug
301
+ }
302
+
303
+ export function isSlugExcludedFromVersion(config: StarlightVersionsConfig, version: Version | undefined, slug: string) {
304
+ return version ? (config.excludedSlugsByVersion[version.slug]?.includes(slug) ?? false) : false
305
+ }
306
+
307
+ export function isExcludedDocPath(config: StarlightVersionsConfig, filePath: string) {
308
+ const normalizedPath = filePath.replaceAll('\\', '/')
309
+ const docsRoot = 'content/docs/'
310
+ const docsRootIndex = normalizedPath.indexOf(docsRoot)
311
+ if (docsRootIndex === -1) return false
312
+
313
+ const docsPath = normalizedPath.slice(docsRootIndex + docsRoot.length)
314
+
315
+ return config.exclude.some((excludeGlob) => path.posix.matchesGlob(docsPath, excludeGlob))
316
+ }
317
+
318
+ export function getPagefindVersionIdentifiers(
319
+ config: StarlightVersionsConfig,
320
+ version: Version | undefined,
321
+ slug: string,
322
+ ): [string, ...string[]] {
323
+ if (version) return [version.slug]
324
+
325
+ return [
326
+ 'current',
327
+ ...config.versions
328
+ .filter((archivedVersion) => isSlugExcludedFromVersion(config, archivedVersion, slug))
329
+ .map((archivedVersion) => archivedVersion.slug),
330
+ ]
331
+ }
332
+
246
333
  // An undefined version is valid and represents the current version.
247
334
  export function getVersionFromSlug(
248
335
  config: StarlightVersionsConfig,
@@ -268,8 +355,95 @@ export function getVersionFromSlug(
268
355
  return config.versions.find((version) => version.slug === versionSegment)
269
356
  }
270
357
 
271
- export function getVersionIdentifier(version: Version | undefined): string {
272
- return version?.slug ?? 'current'
358
+ export function addExcludedLinksToVersionSidebar(
359
+ currentSidebar: StarlightSidebar,
360
+ archivedSidebar: StarlightSidebar,
361
+ version: Version,
362
+ excludedSlugs: string[],
363
+ baseURL: URL,
364
+ ) {
365
+ addExcludedLinksToVersionSidebarEntries(currentSidebar, archivedSidebar, version.slug, excludedSlugs, baseURL)
366
+
367
+ return archivedSidebar
368
+ }
369
+
370
+ function addExcludedLinksToVersionSidebarEntries(
371
+ currentSidebar: StarlightSidebar,
372
+ archivedSidebar: StarlightSidebar,
373
+ version: string,
374
+ excludedSlugs: string[],
375
+ baseURL: URL,
376
+ ) {
377
+ let cursor = 0
378
+
379
+ for (const entry of currentSidebar) {
380
+ const archivedIndex = archivedSidebar.findIndex(
381
+ (candidate, index) => index >= cursor && isMatchingSidebarEntry(entry, candidate, version, baseURL),
382
+ )
383
+
384
+ if (archivedIndex !== -1) {
385
+ const archivedEntry = archivedSidebar[archivedIndex]
386
+
387
+ if (entry.type === 'group' && archivedEntry?.type === 'group') {
388
+ addExcludedLinksToVersionSidebarEntries(entry.entries, archivedEntry.entries, version, excludedSlugs, baseURL)
389
+ }
390
+
391
+ cursor = archivedIndex + 1
392
+ continue
393
+ }
394
+
395
+ const clone = cloneExcludedSidebarEntry(entry, excludedSlugs, baseURL)
396
+ if (!clone) continue
397
+
398
+ archivedSidebar.splice(cursor, 0, clone)
399
+ cursor++
400
+ }
401
+ }
402
+
403
+ function cloneExcludedSidebarEntry(
404
+ entry: StarlightSidebar[number],
405
+ excludedSlugs: string[],
406
+ baseURL: URL,
407
+ ): StarlightSidebar[number] | undefined {
408
+ if (!('autogenerate' in entry)) return undefined
409
+
410
+ if (entry.type === 'link') {
411
+ const slug = getSidebarSlug(entry.href, baseURL)
412
+ if (!excludedSlugs.includes(slug)) return undefined
413
+ return { ...entry, isCurrent: false }
414
+ }
415
+
416
+ const entries = entry.entries.flatMap((child) => {
417
+ const clone = cloneExcludedSidebarEntry(child, excludedSlugs, baseURL)
418
+ return clone ? [clone] : []
419
+ })
420
+
421
+ return entries.length > 0 ? { ...entry, entries } : undefined
422
+ }
423
+
424
+ function isMatchingSidebarEntry(
425
+ current: StarlightSidebar[number],
426
+ archived: StarlightSidebar[number],
427
+ version: string,
428
+ baseURL: URL,
429
+ ) {
430
+ if (current.type !== archived.type) return false
431
+ if (current.type === 'link' && archived.type === 'link') {
432
+ return getSidebarSlug(current.href, baseURL) === getSidebarSlug(archived.href, baseURL, version)
433
+ }
434
+ if (current.type !== 'group' || archived.type !== 'group') return false
435
+
436
+ const currentAuto = 'autogenerate' in current
437
+ const archivedAuto = 'autogenerate' in archived
438
+ if (currentAuto !== archivedAuto || current.label !== archived.label) return false
439
+ if (!currentAuto || !archivedAuto) return true
440
+
441
+ return current.autogenerate.directory === stripVersionSegment(archived.autogenerate.directory, version)
442
+ }
443
+
444
+ function getSidebarSlug(href: string, baseURL: URL, version?: string) {
445
+ const slug = getURLSlug(new URL(href, baseURL))
446
+ return version ? stripVersionSegment(slug, version) : slug
273
447
  }
274
448
 
275
449
  // An undefined version is valid and represents the current version.
@@ -278,12 +452,9 @@ export function getVersionFromPaginationLink(
278
452
  link: string,
279
453
  locale: string | undefined,
280
454
  ): Version | undefined {
281
- const [, ...segments] = link.split('/')
282
-
283
- if (import.meta.env.BASE_URL !== '/') {
284
- // Remove the base segment if configured.
285
- segments.splice(0, 1)
286
- }
455
+ const base = stripTrailingSlash(import.meta.env.BASE_URL)
456
+ const pathname = link === base || link.startsWith(ensureTrailingSlash(base)) ? link.slice(base.length) : link
457
+ const segments = stripLeadingAndTrailingSlashes(pathname).split('/')
287
458
 
288
459
  if (locale) {
289
460
  // Remove the locale segment if the current locale is not a root locale.
@@ -297,30 +468,57 @@ export function getVersionFromPaginationLink(
297
468
  return config.versions.find((version) => version.slug === versionSegment)
298
469
  }
299
470
 
300
- async function getSidebarVersionGroup(version: Version, srcDir: URL) {
301
- const versionConfig = await getVersionConfig(version, srcDir)
302
-
303
- if (!versionConfig.sidebar) {
304
- return {
305
- label: version.slug,
306
- items: [{ autogenerate: { directory: version.slug } }],
307
- }
308
- }
309
-
310
- return {
311
- label: version.slug,
312
- items: addPrefixToSidebarConfig(version.slug, versionConfig.sidebar),
313
- }
314
- }
315
-
316
471
  async function getVersionConfig(version: Version, srcDir: URL) {
317
472
  try {
318
- return await readJSONFile<DocsVersionsConfig>(getVersionConfigURL(version, srcDir))
473
+ return docsVersionsSchema().parse(
474
+ await readJSONFile<Partial<DocsVersionsConfig>>(getVersionConfigURL(version, srcDir)),
475
+ )
319
476
  } catch (error) {
320
477
  throw new Error(`Failed to read the version '${version.slug}' configuration file.`, { cause: error })
321
478
  }
322
479
  }
323
480
 
481
+ async function getExcludedDocs(config: StarlightVersionsConfig, locales: string[], docsDir: URL) {
482
+ const excludedDocs = new Map<string, string>()
483
+
484
+ async function visitDirectory(directory: URL) {
485
+ for (const entry of await listDirectory(directory)) {
486
+ const isDirectory = await isDirectoryEntry(entry)
487
+ const entryURL = new URL(isDirectory ? ensureTrailingSlash(entry.name) : entry.name, directory)
488
+ const docPath = getDocPath(docsDir, entryURL)
489
+
490
+ if (isDirectory) {
491
+ const [firstSegment, secondSegment] = docPath.split('/')
492
+
493
+ if (
494
+ (firstSegment && firstSegment in config.versionsBySlug) ||
495
+ (firstSegment && locales.includes(firstSegment) && secondSegment && secondSegment in config.versionsBySlug)
496
+ ) {
497
+ continue
498
+ }
499
+
500
+ await visitDirectory(entryURL)
501
+
502
+ continue
503
+ }
504
+
505
+ if (
506
+ !entry.isFile() ||
507
+ !docsExtensions.has(getExtension(docPath)) ||
508
+ !config.exclude.some((excludeGlob) => path.posix.matchesGlob(docPath, excludeGlob))
509
+ ) {
510
+ continue
511
+ }
512
+
513
+ excludedDocs.set(docPath, resolveDocSlug(docsDir, entryURL, await fs.readFile(entryURL, 'utf8')))
514
+ }
515
+ }
516
+
517
+ await visitDirectory(docsDir)
518
+
519
+ return excludedDocs
520
+ }
521
+
324
522
  async function checkForNewVersion(config: StarlightVersionsConfig, docsDir: URL): Promise<Version | undefined> {
325
523
  let newVersion: Version | undefined
326
524
 
@@ -345,12 +543,18 @@ async function checkForNewVersion(config: StarlightVersionsConfig, docsDir: URL)
345
543
  return newVersion
346
544
  }
347
545
 
348
- async function makeVersionConfig(version: Version, starlightConfig: StarlightUserConfig, srcDir: URL) {
546
+ async function makeVersionConfig(
547
+ version: Version,
548
+ starlightConfig: StarlightUserConfig,
549
+ srcDir: URL,
550
+ excludedSlugs: string[],
551
+ ) {
349
552
  const versionsDir = getVersionContentCollectionURL(srcDir)
350
553
 
351
554
  await ensureDirectory(versionsDir)
352
555
  await writeJSONFile(getVersionConfigURL(version, srcDir), {
353
556
  sidebar: starlightConfig.sidebar,
557
+ excluded: excludedSlugs,
354
558
  } satisfies DocsVersionsConfig)
355
559
  }
356
560
 
@@ -362,6 +566,13 @@ function getVersionConfigURL(version: Version, srcDir: URL) {
362
566
  return new URL(`${version.slug}.json`, getVersionContentCollectionURL(srcDir))
363
567
  }
364
568
 
569
+ function stripVersionSegment(route: string, version: string) {
570
+ const segments = route.split('/')
571
+ const index = segments.indexOf(version)
572
+ if (index !== -1) segments.splice(index, 1)
573
+ return segments.join('/')
574
+ }
575
+
365
576
  export type Version = z.output<typeof VersionSchema>
366
577
 
367
578
  export interface VersionAsset {
package/middleware.ts CHANGED
@@ -2,32 +2,119 @@ import { defineRouteMiddleware, type StarlightRouteData } from '@astrojs/starlig
2
2
  import starlightConfig from 'virtual:starlight/user-config'
3
3
  import starlightVersionsConfig from 'virtual:starlight-versions-config'
4
4
 
5
- import { getVersionFromPaginationLink, getVersionFromSlug, getVersionSidebar, type Version } from './libs/versions'
5
+ import { getSidebarLinks } from './libs/starlight'
6
+ import {
7
+ addExcludedLinksToVersionSidebar,
8
+ getCanonicalSlug,
9
+ getVersionFromPaginationLink,
10
+ getVersionFromSlug,
11
+ getVersionSidebar,
12
+ isSlugExcludedFromVersion,
13
+ type Version,
14
+ } from './libs/versions'
6
15
 
7
16
  export const onRequest = defineRouteMiddleware((context) => {
8
17
  const { starlightRoute } = context.locals
9
- const { entry, locale, pagination, sidebar } = starlightRoute
10
-
11
- starlightRoute.sidebar = getVersionSidebar(
12
- getVersionFromSlug(starlightVersionsConfig, starlightConfig, entry.id),
13
- sidebar,
14
- )
18
+ const { entry, locale, sidebar } = starlightRoute
15
19
 
16
20
  const pageVersion = getVersionFromSlug(starlightVersionsConfig, starlightConfig, entry.id)
21
+ const versionSidebar = getVersionSidebar(pageVersion, sidebar)
22
+
23
+ if (pageVersion) {
24
+ const excludedSlugs = starlightVersionsConfig.excludedSlugsByVersion[pageVersion.slug] ?? []
25
+ if (excludedSlugs.length > 0) {
26
+ addExcludedLinksToVersionSidebar(
27
+ getVersionSidebar(undefined, sidebar),
28
+ versionSidebar,
29
+ pageVersion,
30
+ excludedSlugs,
31
+ context.url,
32
+ )
33
+ starlightRoute.pagination = getPaginationFromSidebar(starlightRoute, versionSidebar)
34
+ }
35
+ }
36
+
37
+ starlightRoute.sidebar = versionSidebar
17
38
 
18
- starlightRoute.pagination.prev = getPaginationLink(locale, pageVersion, pagination.prev)
19
- starlightRoute.pagination.next = getPaginationLink(locale, pageVersion, pagination.next)
39
+ starlightRoute.pagination.prev = getPaginationLink(context.url, locale, pageVersion, starlightRoute.pagination.prev)
40
+ starlightRoute.pagination.next = getPaginationLink(context.url, locale, pageVersion, starlightRoute.pagination.next)
20
41
  })
21
42
 
22
- function getPaginationLink(locale: string | undefined, currentVersion: Version | undefined, link: PaginationLink) {
43
+ function getPaginationLink(
44
+ baseURL: URL,
45
+ locale: string | undefined,
46
+ currentVersion: Version | undefined,
47
+ link: PaginationLink,
48
+ ) {
23
49
  if (!link) return undefined
24
50
 
25
51
  const linkVersion = getVersionFromPaginationLink(starlightVersionsConfig, link.href, locale)
52
+ const canonicalSlug = getCanonicalSlug(starlightVersionsConfig, starlightConfig, new URL(link.href, baseURL))
26
53
 
27
- // If the current version is not the same as the link version, remove the link.
28
- return (currentVersion === undefined && linkVersion === undefined) || currentVersion?.slug === linkVersion?.slug
54
+ // If the current version is not the same as the link version and it's not an excluded version link, remove it.
55
+ return (currentVersion === undefined && linkVersion === undefined) ||
56
+ currentVersion?.slug === linkVersion?.slug ||
57
+ (linkVersion === undefined && isSlugExcludedFromVersion(starlightVersionsConfig, currentVersion, canonicalSlug))
29
58
  ? link
30
59
  : undefined
31
60
  }
32
61
 
62
+ // https://github.com/withastro/starlight/blob/main/packages/starlight/utils/navigation.ts#L481
63
+ function getPaginationFromSidebar(
64
+ route: StarlightRouteData,
65
+ sidebar: StarlightRouteData['sidebar'],
66
+ ): StarlightRouteData['pagination'] {
67
+ const entries = getSidebarLinks(sidebar)
68
+ const currentIndex = entries.findIndex((entry) => entry.isCurrent)
69
+
70
+ const prev = applyPaginationConfig(
71
+ entries[currentIndex - 1],
72
+ starlightConfig.pagination,
73
+ route.entry.data.prev,
74
+ route.pagination.prev,
75
+ )
76
+ const next = applyPaginationConfig(
77
+ currentIndex > -1 ? entries[currentIndex + 1] : undefined,
78
+ starlightConfig.pagination,
79
+ route.entry.data.next,
80
+ route.pagination.next,
81
+ )
82
+
83
+ return { prev, next }
84
+ }
85
+
86
+ // https://github.com/withastro/starlight/blob/main/packages/starlight/utils/navigation.ts#L501
87
+ function applyPaginationConfig(
88
+ link: PaginationLink,
89
+ paginationEnabled: boolean,
90
+ config: StarlightRouteData['entry']['data']['prev'],
91
+ existing: PaginationLink,
92
+ ): PaginationLink {
93
+ // Explicitly remove the link.
94
+ if (config === false) return undefined
95
+ // Use the generated link if any.
96
+ else if (config === true) return link
97
+ // If a link exists, update its label if needed.
98
+ else if (typeof config === 'string' && link) {
99
+ return { ...link, label: config }
100
+ } else if (typeof config === 'object') {
101
+ if (link) {
102
+ // If a link exists, update both its label and href if needed.
103
+ return {
104
+ ...link,
105
+ label: config.label ?? link.label,
106
+ href: config.link ?? link.href,
107
+ // Explicitly remove sidebar link attributes for prev/next links.
108
+ attrs: {},
109
+ }
110
+ } else if (config.link && config.label) {
111
+ // If there is no link and the frontmatter contains both a URL and a label, use it.
112
+ return existing
113
+ }
114
+ }
115
+
116
+ // Otherwise, if the global config is enabled, return the generated link if any.
117
+ return paginationEnabled ? link : undefined
118
+ }
119
+
33
120
  type PaginationLink = StarlightRouteData['pagination']['next']
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starlight-versions",
3
- "version": "0.9.1",
3
+ "version": "0.10.1",
4
4
  "license": "MIT",
5
5
  "description": "Starlight plugin to version documentation pages.",
6
6
  "author": "HiDeoo <github@hideoo.dev> (https://hideoo.dev)",
package/schema.ts CHANGED
@@ -4,10 +4,12 @@ import type { StarlightSidebarUserConfig } from './libs/starlight'
4
4
 
5
5
  export function docsVersionsSchema() {
6
6
  return z.object({
7
- sidebar: z.any().optional(),
7
+ sidebar: z.custom<NonNullable<StarlightSidebarUserConfig>>().optional(),
8
+ excluded: z.array(z.string()).default([]),
8
9
  })
9
10
  }
10
11
 
11
12
  export interface DocsVersionsConfig {
12
13
  sidebar?: StarlightSidebarUserConfig
14
+ excluded: string[]
13
15
  }