11import { statSync } from 'node:fs'
22import { posix } from 'node:path'
3- import { fileURLToPath } from 'node:url'
3+ import { fileURLToPath , pathToFileURL } from 'node:url'
44
55import type { StarlightUserConfig as StarlightUserConfigWithPlugins } from '@astrojs/starlight/types'
66import type { AstroConfig , AstroIntegrationLogger } from 'astro'
77import { bgGreen , black , blue , dim , green , red } from 'kleur/colors'
88import picomatch from 'picomatch'
9+ import terminalLink from 'terminal-link'
910
1011import type { StarlightLinksValidatorOptions } from '..'
1112
@@ -49,13 +50,14 @@ export function validateLinks(
4950
5051 const errors : ValidationErrors = new Map ( )
5152
52- for ( const [ filePath , { links : fileLinks } ] of validationData ) {
53+ for ( const [ id , { links : fileLinks , file } ] of validationData ) {
5354 for ( const link of fileLinks ) {
5455 const validationContext : ValidationContext = {
5556 astroConfig,
5657 customPages,
5758 errors,
58- filePath,
59+ file,
60+ id,
5961 link,
6062 localeConfig,
6163 options,
@@ -85,7 +87,10 @@ export function logErrors(pluginLogger: AstroIntegrationLogger, errors: Validati
8587 return
8688 }
8789
88- const errorCount = [ ...errors . values ( ) ] . reduce ( ( acc , links ) => acc + links . length , 0 )
90+ const errorCount = [ ...errors . values ( ) ] . reduce (
91+ ( acc , { errors : validationErrors } ) => acc + validationErrors . length ,
92+ 0 ,
93+ )
8994
9095 logger . error (
9196 red (
@@ -98,8 +103,8 @@ export function logErrors(pluginLogger: AstroIntegrationLogger, errors: Validati
98103
99104 let hasInvalidLinkToCustomPage = false
100105
101- for ( const [ file , validationErrors ] of errors ) {
102- logger . info ( `${ red ( '▶' ) } ${ blue ( file ) } ` )
106+ for ( const [ id , { errors : validationErrors , file } ] of errors ) {
107+ logger . info ( `${ red ( '▶' ) } ${ blue ( terminalLink ( id , pathToFileURL ( file ) . toString ( ) , { fallback : false } ) ) } ` )
103108
104109 for ( const [ index , validationError ] of validationErrors . entries ( ) ) {
105110 logger . info (
@@ -120,14 +125,14 @@ export function logErrors(pluginLogger: AstroIntegrationLogger, errors: Validati
120125 * Validate a link to another internal page that may or may not have a hash.
121126 */
122127function validateLink ( context : ValidationContext ) {
123- const { astroConfig, customPages, errors, filePath , link, localeConfig, options, pages } = context
128+ const { astroConfig, customPages, errors, id , file , link, localeConfig, options, pages } = context
124129
125130 if ( isExcludedLink ( link , context ) ) {
126131 return
127132 }
128133
129134 if ( link . error ) {
130- addError ( errors , filePath , link , link . error )
135+ addError ( errors , id , file , link , link . error )
131136 return
132137 }
133138
@@ -144,7 +149,7 @@ function validateLink(context: ValidationContext) {
144149
145150 if ( path . startsWith ( '.' ) || ( ! linkToValidate . startsWith ( '/' ) && ! linkToValidate . startsWith ( '?' ) ) ) {
146151 if ( options . errorOnRelativeLinks ) {
147- addError ( errors , filePath , link , ValidationErrorType . RelativeLink )
152+ addError ( errors , id , file , link , ValidationErrorType . RelativeLink )
148153 }
149154
150155 return
@@ -162,7 +167,8 @@ function validateLink(context: ValidationContext) {
162167 if ( ! isValidPage || ! fileHeadings ) {
163168 addError (
164169 errors ,
165- filePath ,
170+ id ,
171+ file ,
166172 link ,
167173 customPages . has ( stripTrailingSlash ( sanitizedPath ) )
168174 ? ValidationErrorType . InvalidLinkToCustomPage
@@ -171,24 +177,24 @@ function validateLink(context: ValidationContext) {
171177 return
172178 }
173179
174- if ( options . errorOnInconsistentLocale && localeConfig && isInconsistentLocaleLink ( filePath , link . raw , localeConfig ) ) {
175- addError ( errors , filePath , link , ValidationErrorType . InconsistentLocale )
180+ if ( options . errorOnInconsistentLocale && localeConfig && isInconsistentLocaleLink ( id , link . raw , localeConfig ) ) {
181+ addError ( errors , id , file , link , ValidationErrorType . InconsistentLocale )
176182 return
177183 }
178184
179185 if ( hash && ! fileHeadings . includes ( hash ) ) {
180186 if ( options . errorOnInvalidHashes ) {
181- addError ( errors , filePath , link , ValidationErrorType . InvalidHash )
187+ addError ( errors , id , file , link , ValidationErrorType . InvalidHash )
182188 }
183189 return
184190 }
185191
186192 if ( path . length > 0 ) {
187193 if ( astroConfig . trailingSlash === 'always' && ! path . endsWith ( '/' ) ) {
188- addError ( errors , filePath , link , ValidationErrorType . TrailingSlashMissing )
194+ addError ( errors , id , file , link , ValidationErrorType . TrailingSlashMissing )
189195 return
190196 } else if ( astroConfig . trailingSlash === 'never' && path . endsWith ( '/' ) ) {
191- addError ( errors , filePath , link , ValidationErrorType . TrailingSlashForbidden )
197+ addError ( errors , id , file , link , ValidationErrorType . TrailingSlashForbidden )
192198 return
193199 }
194200 }
@@ -208,22 +214,22 @@ function getFileHeadings(path: string, { astroConfig, localeConfig, options, val
208214 * Validate a link to an hash in the same page.
209215 */
210216function validateSelfHash ( context : ValidationContext ) {
211- const { errors, link, filePath , validationData } = context
217+ const { errors, link, id , file , validationData } = context
212218
213219 if ( isExcludedLink ( link , context ) ) {
214220 return
215221 }
216222
217223 const hash = link . raw . split ( '#' ) [ 1 ] ?? link . raw
218224 const sanitizedHash = hash . replace ( / ^ # / , '' )
219- const fileHeadings = validationData . get ( filePath ) ?. headings
225+ const fileHeadings = validationData . get ( id ) ?. headings
220226
221227 if ( ! fileHeadings ) {
222- throw new Error ( `Failed to find headings for the file at '${ filePath } '.` )
228+ throw new Error ( `Failed to find headings for the file at '${ id } '.` )
223229 }
224230
225231 if ( ! fileHeadings . includes ( sanitizedHash ) ) {
226- addError ( errors , filePath , link , ValidationErrorType . InvalidHash )
232+ addError ( errors , id , file , link , ValidationErrorType . InvalidHash )
227233 }
228234}
229235
@@ -254,28 +260,28 @@ function isValidAsset(path: string, context: ValidationContext) {
254260/**
255261 * Check if a link is excluded from validation by the user.
256262 */
257- function isExcludedLink ( link : Link , { filePath , options, validationData } : ValidationContext ) {
263+ function isExcludedLink ( link : Link , { id , options, validationData } : ValidationContext ) {
258264 if ( Array . isArray ( options . exclude ) ) return picomatch ( options . exclude ) ( stripQueryString ( link . raw ) )
259265
260- const file = validationData . get ( filePath ) ?. file
266+ const file = validationData . get ( id ) ?. file
261267 if ( ! file ) throw new Error ( 'Missing file path to check exclusion.' )
262268
263269 return options . exclude ( {
264270 file,
265271 link : link . raw ,
266- slug : stripTrailingSlash ( filePath ) ,
272+ slug : stripTrailingSlash ( id ) ,
267273 } )
268274}
269275
270276function stripQueryString ( path : string ) : string {
271277 return path . split ( '?' ) [ 0 ] ?? path
272278}
273279
274- function addError ( errors : ValidationErrors , filePath : string , link : Link , type : ValidationErrorType ) {
275- const fileErrors = errors . get ( filePath ) ?? [ ]
276- fileErrors . push ( { link : link . raw , type } )
280+ function addError ( errors : ValidationErrors , id : string , file : string , link : Link , type : ValidationErrorType ) {
281+ const fileErrors = errors . get ( id ) ?? { errors : [ ] , file }
282+ fileErrors . errors . push ( { link : link . raw , type } )
277283
278- errors . set ( filePath , fileErrors )
284+ errors . set ( id , fileErrors )
279285}
280286
281287function pluralize ( count : number , singular : string ) {
@@ -289,7 +295,7 @@ function formatValidationError(error: ValidationError, site: AstroConfig['site']
289295}
290296
291297// The validation errors keyed by file path.
292- type ValidationErrors = Map < string , ValidationError [ ] >
298+ type ValidationErrors = Map < string , { errors : ValidationError [ ] ; file : string } >
293299
294300export type ValidationErrorType = ( typeof ValidationErrorType ) [ keyof typeof ValidationErrorType ]
295301
@@ -308,7 +314,8 @@ interface ValidationContext {
308314 astroConfig : AstroConfig
309315 customPages : Set < string >
310316 errors : ValidationErrors
311- filePath : string
317+ id : string
318+ file : string
312319 link : Link
313320 localeConfig : LocaleConfig | undefined
314321 options : StarlightLinksValidatorOptions
0 commit comments