starlight-links-validator 0.14.0 → 0.14.2

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,21 @@
1
1
  # starlight-links-validator
2
2
 
3
+ ## 0.14.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#85](https://github.com/HiDeoo/starlight-links-validator/pull/85) [`57fdb1b`](https://github.com/HiDeoo/starlight-links-validator/commit/57fdb1b2f85f023e4b053480fd9ea5adb69a9e2a) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Improves error message for invalid links to custom pages.
8
+
9
+ ## 0.14.1
10
+
11
+ ### Patch Changes
12
+
13
+ - [#82](https://github.com/HiDeoo/starlight-links-validator/pull/82) [`b3cbee8`](https://github.com/HiDeoo/starlight-links-validator/commit/b3cbee83fb54f5bd6dd06b01bb8397758c081752) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fixes regresion introduced in version [`0.14.0`](https://github.com/HiDeoo/starlight-links-validator/releases/tag/starlight-links-validator%400.14.0) of the plugin regarding validation of links to pages with [custom IDs/slugs](https://docs.astro.build/en/guides/content-collections/#defining-custom-ids).
14
+
15
+ Note that you must use at least Astro version [`5.1.1`](https://github.com/withastro/astro/releases/tag/astro%405.1.1) to benefit from this fix.
16
+
17
+ - [#80](https://github.com/HiDeoo/starlight-links-validator/pull/80) [`876cb50`](https://github.com/HiDeoo/starlight-links-validator/commit/876cb5094d10a56a1be04b7cdc27e4f89fb1b681) Thanks [@lukekarrys](https://github.com/lukekarrys)! - Fixes validation issues for pages ending in `index`, e.g. `module_index`.
18
+
3
19
  ## 0.14.0
4
20
 
5
21
  ### Minor Changes
package/index.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import type { StarlightPlugin } from '@astrojs/starlight/types'
2
+ import type { IntegrationResolvedRoute } from 'astro'
2
3
  import { AstroError } from 'astro/errors'
3
4
  import { z } from 'astro/zod'
4
5
 
5
6
  import { clearContentLayerCache } from './libs/astro'
7
+ import { pathnameToSlug } from './libs/path'
6
8
  import { remarkStarlightLinksValidator } from './libs/remark'
7
9
  import { logErrors, validateLinks } from './libs/validation'
8
10
 
@@ -72,6 +74,8 @@ export default function starlightLinksValidatorPlugin(
72
74
  name: 'starlight-links-validator-plugin',
73
75
  hooks: {
74
76
  setup({ addIntegration, astroConfig, config: starlightConfig, logger }) {
77
+ let routes: IntegrationResolvedRoute[] = []
78
+
75
79
  addIntegration({
76
80
  name: 'starlight-links-validator-integration',
77
81
  hooks: {
@@ -90,13 +94,32 @@ export default function starlightLinksValidatorPlugin(
90
94
  },
91
95
  })
92
96
  },
93
- 'astro:build:done': ({ dir, pages }) => {
94
- const errors = validateLinks(pages, dir, astroConfig, starlightConfig, options.data)
97
+ 'astro:routes:resolved': (params) => {
98
+ routes = params.routes
99
+ },
100
+ 'astro:build:done': ({ dir, pages, assets }) => {
101
+ const customPages = new Set<string>()
95
102
 
96
- logErrors(logger, errors)
103
+ for (const [pattern, urls] of assets) {
104
+ const route = routes.find((route) => route.pattern === pattern)
105
+ if (!route || route.origin !== 'project') continue
106
+
107
+ for (const url of urls) {
108
+ customPages.add(pathnameToSlug(url.pathname.replace(astroConfig.outDir.pathname, '')))
109
+ }
110
+ }
111
+
112
+ const errors = validateLinks(pages, customPages, dir, astroConfig, starlightConfig, options.data)
113
+
114
+ const hasInvalidLinkToCustomPage = logErrors(logger, errors)
97
115
 
98
116
  if (errors.size > 0) {
99
- throwPluginError('Links validation failed.')
117
+ throwPluginError(
118
+ 'Links validation failed.',
119
+ hasInvalidLinkToCustomPage
120
+ ? 'Some invalid links point to custom pages which cannot be validated, see the `exclude` option for more informations at https://starlight-links-validator.vercel.app/configuration#exclude'
121
+ : undefined,
122
+ )
100
123
  }
101
124
  },
102
125
  },
@@ -106,11 +129,13 @@ export default function starlightLinksValidatorPlugin(
106
129
  }
107
130
  }
108
131
 
109
- function throwPluginError(message: string): never {
110
- throw new AstroError(
111
- message,
112
- `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-links-validator/issues/new/choose`,
113
- )
132
+ function throwPluginError(message: string, additionalHint?: string): never {
133
+ let hint = 'See the error report above for more informations.\n\n'
134
+ if (additionalHint) hint += `${additionalHint}\n\n`
135
+ hint +=
136
+ 'If you believe this is a bug, please file an issue at https://github.com/HiDeoo/starlight-links-validator/issues/new/choose'
137
+
138
+ throw new AstroError(message, hint)
114
139
  }
115
140
 
116
141
  type StarlightLinksValidatorUserOptions = z.input<typeof starlightLinksValidatorOptionsSchema>
package/libs/path.ts CHANGED
@@ -1,3 +1,5 @@
1
+ const htmlExtension = '.html'
2
+
1
3
  export function ensureLeadingSlash(path: string): string {
2
4
  return path.startsWith('/') ? path : `/${path}`
3
5
  }
@@ -9,3 +11,28 @@ export function ensureTrailingSlash(path: string): string {
9
11
  export function stripLeadingSlash(path: string) {
10
12
  return path.replace(/^\//, '')
11
13
  }
14
+
15
+ export function stripTrailingSlash(path: string) {
16
+ return path.replace(/\/$/, '')
17
+ }
18
+
19
+ export function pathnameToSlug(pathname: string): string {
20
+ const base = stripTrailingSlash(import.meta.env.BASE_URL)
21
+
22
+ if (pathname.startsWith(base)) {
23
+ pathname = pathname.replace(base, '')
24
+ }
25
+
26
+ const segments = pathname.split('/')
27
+
28
+ if (segments.at(-1) === 'index.html') {
29
+ segments.pop()
30
+ } else if (segments.at(-1)?.endsWith(htmlExtension)) {
31
+ const last = segments.pop()
32
+ if (last) {
33
+ segments.push(last.slice(0, -1 * htmlExtension.length))
34
+ }
35
+ }
36
+
37
+ return segments.filter(Boolean).join('/')
38
+ }
package/libs/remark.ts CHANGED
@@ -178,7 +178,7 @@ function normalizeFilePath(base: string, srcDir: URL, filePath?: string) {
178
178
  const path = nodePath
179
179
  .relative(nodePath.join(fileURLToPath(srcDir), 'content/docs'), filePath)
180
180
  .replace(/\.\w+$/, '')
181
- .replace(/index$/, '')
181
+ .replace(/(^|[/\\])index$/, '')
182
182
  .replace(/[/\\]?$/, '/')
183
183
  .split(/[/\\]/)
184
184
  .map((segment) => slug(segment))
@@ -10,13 +10,14 @@ import picomatch from 'picomatch'
10
10
  import type { StarlightLinksValidatorOptions } from '..'
11
11
 
12
12
  import { getFallbackHeadings, getLocaleConfig, isInconsistentLocaleLink, type LocaleConfig } from './i18n'
13
- import { ensureTrailingSlash, stripLeadingSlash } from './path'
13
+ import { ensureTrailingSlash, stripLeadingSlash, stripTrailingSlash } from './path'
14
14
  import { getValidationData, type Headings } from './remark'
15
15
 
16
16
  export const ValidationErrorType = {
17
17
  InconsistentLocale: 'inconsistent locale',
18
18
  InvalidHash: 'invalid hash',
19
19
  InvalidLink: 'invalid link',
20
+ InvalidLinkToCustomPage: 'invalid link to custom page',
20
21
  LocalLink: 'local link',
21
22
  RelativeLink: 'relative link',
22
23
  TrailingSlashMissing: 'missing trailing slash',
@@ -25,6 +26,7 @@ export const ValidationErrorType = {
25
26
 
26
27
  export function validateLinks(
27
28
  pages: PageData[],
29
+ customPages: Set<string>,
28
30
  outputDir: URL,
29
31
  astroConfig: AstroConfig,
30
32
  starlightConfig: StarlightUserConfig,
@@ -50,6 +52,7 @@ export function validateLinks(
50
52
  for (const link of fileLinks) {
51
53
  const validationContext: ValidationContext = {
52
54
  astroConfig,
55
+ customPages,
53
56
  errors,
54
57
  filePath,
55
58
  headings,
@@ -92,6 +95,8 @@ export function logErrors(pluginLogger: AstroIntegrationLogger, errors: Validati
92
95
  ),
93
96
  )
94
97
 
98
+ let hasInvalidLinkToCustomPage = false
99
+
95
100
  for (const [file, validationErrors] of errors) {
96
101
  logger.info(`${red('▶')} ${blue(file)}`)
97
102
 
@@ -101,17 +106,20 @@ export function logErrors(pluginLogger: AstroIntegrationLogger, errors: Validati
101
106
  ` - ${validationError.type}`,
102
107
  )}`,
103
108
  )
109
+ hasInvalidLinkToCustomPage = validationError.type === ValidationErrorType.InvalidLinkToCustomPage
104
110
  }
105
111
  }
106
112
 
107
113
  process.stdout.write('\n')
114
+
115
+ return hasInvalidLinkToCustomPage
108
116
  }
109
117
 
110
118
  /**
111
119
  * Validate a link to another internal page that may or may not have a hash.
112
120
  */
113
121
  function validateLink(context: ValidationContext) {
114
- const { astroConfig, errors, filePath, link, localeConfig, options, pages } = context
122
+ const { astroConfig, customPages, errors, filePath, link, localeConfig, options, pages } = context
115
123
 
116
124
  if (isExcludedLink(link, context)) {
117
125
  return
@@ -153,7 +161,14 @@ function validateLink(context: ValidationContext) {
153
161
  const fileHeadings = getFileHeadings(sanitizedPath, context)
154
162
 
155
163
  if (!isValidPage || !fileHeadings) {
156
- addError(errors, filePath, link, ValidationErrorType.InvalidLink)
164
+ addError(
165
+ errors,
166
+ filePath,
167
+ link,
168
+ customPages.has(stripTrailingSlash(sanitizedPath))
169
+ ? ValidationErrorType.InvalidLinkToCustomPage
170
+ : ValidationErrorType.InvalidLink,
171
+ )
157
172
  return
158
173
  }
159
174
 
@@ -271,6 +286,7 @@ type Pages = Set<PageData['pathname']>
271
286
 
272
287
  interface ValidationContext {
273
288
  astroConfig: AstroConfig
289
+ customPages: Set<string>
274
290
  errors: ValidationErrors
275
291
  filePath: string
276
292
  headings: Headings
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starlight-links-validator",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "license": "MIT",
5
5
  "description": "Starlight plugin to validate internal links.",
6
6
  "author": "HiDeoo <github@hideoo.dev> (https://hideoo.dev)",