diff --git a/packages/starlight-links-validator/index.ts b/packages/starlight-links-validator/index.ts index f50bd7e..aa31ef2 100644 --- a/packages/starlight-links-validator/index.ts +++ b/packages/starlight-links-validator/index.ts @@ -48,7 +48,7 @@ export default function starlightLinksValidatorPlugin( return { name: 'starlight-links-validator-plugin', hooks: { - setup({ addIntegration, config: starlightConfig, logger }) { + setup({ addIntegration, astroConfig, config: starlightConfig, logger }) { addIntegration({ name: 'starlight-links-validator-integration', hooks: { @@ -59,12 +59,12 @@ export default function starlightLinksValidatorPlugin( updateConfig({ markdown: { - remarkPlugins: [remarkStarlightLinksValidator], + remarkPlugins: [[remarkStarlightLinksValidator, astroConfig.base]], }, }) }, 'astro:build:done': ({ dir, pages }) => { - const errors = validateLinks(pages, dir, starlightConfig, options.data) + const errors = validateLinks(pages, dir, astroConfig.base, starlightConfig, options.data) logErrors(logger, errors) diff --git a/packages/starlight-links-validator/libs/path.ts b/packages/starlight-links-validator/libs/path.ts index 70f523b..071427d 100644 --- a/packages/starlight-links-validator/libs/path.ts +++ b/packages/starlight-links-validator/libs/path.ts @@ -5,3 +5,7 @@ export function ensureLeadingSlash(path: string): string { export function ensureTrailingSlash(path: string): string { return path.endsWith('/') ? path : `${path}/` } + +export function stripLeadingSlash(path: string) { + return path.replace(/^\//, '') +} diff --git a/packages/starlight-links-validator/libs/remark.ts b/packages/starlight-links-validator/libs/remark.ts index 738995a..45f300f 100644 --- a/packages/starlight-links-validator/libs/remark.ts +++ b/packages/starlight-links-validator/libs/remark.ts @@ -12,15 +12,17 @@ import { toString } from 'mdast-util-to-string' import type { Plugin } from 'unified' import { visit } from 'unist-util-visit' +import { stripLeadingSlash } from './path' + // All the headings keyed by file path. const headings: Headings = new Map() // All the internal links keyed by file path. const links: Links = new Map() -export const remarkStarlightLinksValidator: Plugin<[], Root> = function () { +export const remarkStarlightLinksValidator: Plugin<[base: string], Root> = function (base) { return (tree, file) => { const slugger = new GitHubSlugger() - const filePath = normalizeFilePath(file.history[0]) + const filePath = normalizeFilePath(base, file.history[0]) const fileHeadings: string[] = [] const fileLinks: string[] = [] @@ -134,12 +136,12 @@ function isInternalLink(link: string) { return nodePath.isAbsolute(link) || link.startsWith('#') || link.startsWith('.') } -function normalizeFilePath(filePath?: string) { +function normalizeFilePath(base: string, filePath?: string) { if (!filePath) { throw new Error('Missing file path to validate links.') } - return nodePath + const path = nodePath .relative(nodePath.join(process.cwd(), 'src/content/docs'), filePath) .replace(/\.\w+$/, '') .replace(/index$/, '') @@ -147,6 +149,12 @@ function normalizeFilePath(filePath?: string) { .split(/[/\\]/) .map((segment) => slug(segment)) .join('/') + + if (base !== '/') { + return nodePath.join(stripLeadingSlash(base), path) + } + + return path } function isMdxIdAttribute(attribute: MdxJsxAttribute | MdxJsxExpressionAttribute): attribute is MdxIdAttribute { diff --git a/packages/starlight-links-validator/libs/validation.ts b/packages/starlight-links-validator/libs/validation.ts index 1361abf..85d3a8f 100644 --- a/packages/starlight-links-validator/libs/validation.ts +++ b/packages/starlight-links-validator/libs/validation.ts @@ -1,4 +1,5 @@ import { statSync } from 'node:fs' +import { join } from 'node:path' import { fileURLToPath } from 'node:url' import type { StarlightPlugin } from '@astrojs/starlight/types' @@ -8,7 +9,7 @@ import { bgGreen, black, blue, dim, green, red } from 'kleur/colors' import type { StarlightLinksValidatorOptions } from '..' import { getFallbackHeadings, getLocaleConfig, isInconsistentLocaleLink, type LocaleConfig } from './i18n' -import { ensureTrailingSlash } from './path' +import { ensureTrailingSlash, stripLeadingSlash } from './path' import { getValidationData, type Headings } from './remark' export const ValidationErrorType = { @@ -21,6 +22,7 @@ export const ValidationErrorType = { export function validateLinks( pages: PageData[], outputDir: URL, + base: string, starlightConfig: StarlightUserConfig, options: StarlightLinksValidatorOptions, ): ValidationErrors { @@ -28,7 +30,11 @@ export function validateLinks( const localeConfig = getLocaleConfig(starlightConfig) const { headings, links } = getValidationData() - const allPages: Pages = new Set(pages.map((page) => ensureTrailingSlash(page.pathname))) + const allPages: Pages = new Set( + pages.map((page) => + ensureTrailingSlash(base === '/' ? page.pathname : join(stripLeadingSlash(base), page.pathname)), + ), + ) const errors: ValidationErrors = new Map() diff --git a/packages/starlight-links-validator/tests/base-path.test.ts b/packages/starlight-links-validator/tests/base-path.test.ts new file mode 100644 index 0000000..eb26253 --- /dev/null +++ b/packages/starlight-links-validator/tests/base-path.test.ts @@ -0,0 +1,26 @@ +import { expect, test } from 'vitest' + +import { ValidationErrorType } from '../libs/validation' + +import { expectValidationErrorCount, expectValidationErrors, loadFixture } from './utils' + +test('should validate links when the `base` Astro option is set', async () => { + expect.assertions(2) + + try { + await loadFixture('base-path') + } catch (error) { + expectValidationErrorCount(error, 8, 1) + + expectValidationErrors(error, 'test/test/', [ + ['/guides/example', ValidationErrorType.InvalidLink], + ['/guides/example/', ValidationErrorType.InvalidLink], + ['/guides/example#description', ValidationErrorType.InvalidLink], + ['/guides/example/#description', ValidationErrorType.InvalidLink], + ['/unknown', ValidationErrorType.InvalidLink], + ['/unknown/', ValidationErrorType.InvalidLink], + ['/test/guides/example#unknown', ValidationErrorType.InvalidAnchor], + ['/test/guides/example/#unknown', ValidationErrorType.InvalidAnchor], + ]) + } +}) diff --git a/packages/starlight-links-validator/tests/fixtures/base-path/astro.config.ts b/packages/starlight-links-validator/tests/fixtures/base-path/astro.config.ts new file mode 100644 index 0000000..936b5cd --- /dev/null +++ b/packages/starlight-links-validator/tests/fixtures/base-path/astro.config.ts @@ -0,0 +1,14 @@ +import starlight from '@astrojs/starlight' +import { defineConfig } from 'astro/config' + +import starlightLinksValidator from '../..' + +export default defineConfig({ + base: '/test', + integrations: [ + starlight({ + plugins: [starlightLinksValidator()], + title: 'Starlight Links Validator Tests - trailing always', + }), + ], +}) diff --git a/packages/starlight-links-validator/tests/fixtures/base-path/src/content/docs/guides/example.md b/packages/starlight-links-validator/tests/fixtures/base-path/src/content/docs/guides/example.md new file mode 100644 index 0000000..e78ca68 --- /dev/null +++ b/packages/starlight-links-validator/tests/fixtures/base-path/src/content/docs/guides/example.md @@ -0,0 +1,7 @@ +--- +title: Example +--- + +## Description + +This is an example page. diff --git a/packages/starlight-links-validator/tests/fixtures/base-path/src/content/docs/test.md b/packages/starlight-links-validator/tests/fixtures/base-path/src/content/docs/test.md new file mode 100644 index 0000000..e433187 --- /dev/null +++ b/packages/starlight-links-validator/tests/fixtures/base-path/src/content/docs/test.md @@ -0,0 +1,25 @@ +--- +title: Test +--- + +# Some links + +- [External link](https://starlight.astro.build/) + +- [Example page](/test/guides/example) +- [Example page](/test/guides/example/) + +- [Example page with hash](/test/guides/example#description) +- [Example page with hash](/test/guides/example/#description) + +- [Example page with missing base](/guides/example) +- [Example page with missing base](/guides/example/) + +- [Example page with missing base and hash](/guides/example#description) +- [Example page with missing base and hash](/guides/example/#description) + +- [Unknown page](/unknown) +- [Unknown page](/unknown/) + +- [Example page with unknown hash](/test/guides/example#unknown) +- [Example page with unknown hash](/test/guides/example/#unknown)