diff --git a/.github/workflows/superset-docs-verify.yml b/.github/workflows/superset-docs-verify.yml index d9892b1620b..82944f79c9b 100644 --- a/.github/workflows/superset-docs-verify.yml +++ b/.github/workflows/superset-docs-verify.yml @@ -78,6 +78,13 @@ jobs: - name: yarn install run: | yarn install --check-cache + - name: Lint docs links + # Fast source-level check for bare relative internal links + # like `[Foo](../foo)` that Docusaurus's onBrokenLinks + # setting can't catch. Runs in seconds; fails fast before + # the expensive build step. + run: | + yarn lint:docs-links - name: yarn typecheck run: | yarn typecheck diff --git a/docs/developer_docs/components/design-system/index.mdx b/docs/developer_docs/components/design-system/index.mdx index 2f4c4e30e84..6f856ca6187 100644 --- a/docs/developer_docs/components/design-system/index.mdx +++ b/docs/developer_docs/components/design-system/index.mdx @@ -29,10 +29,10 @@ sidebar_position: 1 ## Components -- [DropdownContainer](./dropdowncontainer) -- [Flex](./flex) -- [Grid](./grid) -- [Layout](./layout) -- [MetadataBar](./metadatabar) -- [Space](./space) -- [Table](./table) +- [DropdownContainer](./dropdowncontainer.md) +- [Flex](./flex.md) +- [Grid](./grid.md) +- [Layout](./layout.md) +- [MetadataBar](./metadatabar.md) +- [Space](./space.md) +- [Table](./table.md) diff --git a/docs/developer_docs/components/index.mdx b/docs/developer_docs/components/index.mdx index 270e24163c4..ee90cdcd45a 100644 --- a/docs/developer_docs/components/index.mdx +++ b/docs/developer_docs/components/index.mdx @@ -62,7 +62,7 @@ This documentation is auto-generated from Storybook stories. To add or update co 4. Run `yarn generate:superset-components` in the `docs/` directory :::info Work in Progress -This component library is actively being documented. See the [Components TODO](./TODO) page for a list of components awaiting documentation. +This component library is actively being documented. See the [Components TODO](./TODO.md) page for a list of components awaiting documentation. ::: --- diff --git a/docs/developer_docs/components/ui/index.mdx b/docs/developer_docs/components/ui/index.mdx index 2a0d1aa8d41..f78260823a2 100644 --- a/docs/developer_docs/components/ui/index.mdx +++ b/docs/developer_docs/components/ui/index.mdx @@ -29,49 +29,49 @@ sidebar_position: 1 ## Components -- [AutoComplete](./autocomplete) -- [Avatar](./avatar) -- [Badge](./badge) -- [Breadcrumb](./breadcrumb) -- [Button](./button) -- [ButtonGroup](./buttongroup) -- [CachedLabel](./cachedlabel) -- [Card](./card) -- [Checkbox](./checkbox) -- [Collapse](./collapse) -- [DatePicker](./datepicker) -- [Divider](./divider) -- [EditableTitle](./editabletitle) -- [EmptyState](./emptystate) -- [FaveStar](./favestar) -- [IconButton](./iconbutton) -- [Icons](./icons) -- [IconTooltip](./icontooltip) -- [InfoTooltip](./infotooltip) -- [Input](./input) -- [Label](./label) -- [List](./list) -- [ListViewCard](./listviewcard) -- [Loading](./loading) -- [Menu](./menu) -- [Modal](./modal) -- [ModalTrigger](./modaltrigger) -- [Popover](./popover) -- [ProgressBar](./progressbar) -- [Radio](./radio) -- [SafeMarkdown](./safemarkdown) -- [Select](./select) -- [Skeleton](./skeleton) -- [Slider](./slider) -- [Steps](./steps) -- [Switch](./switch) -- [TableCollection](./tablecollection) -- [TableView](./tableview) -- [Tabs](./tabs) -- [Timer](./timer) -- [Tooltip](./tooltip) -- [Tree](./tree) -- [TreeSelect](./treeselect) -- [Typography](./typography) -- [UnsavedChangesModal](./unsavedchangesmodal) -- [Upload](./upload) +- [AutoComplete](./autocomplete.md) +- [Avatar](./avatar.md) +- [Badge](./badge.md) +- [Breadcrumb](./breadcrumb.md) +- [Button](./button.md) +- [ButtonGroup](./buttongroup.md) +- [CachedLabel](./cachedlabel.md) +- [Card](./card.md) +- [Checkbox](./checkbox.md) +- [Collapse](./collapse.md) +- [DatePicker](./datepicker.md) +- [Divider](./divider.md) +- [EditableTitle](./editabletitle.md) +- [EmptyState](./emptystate.md) +- [FaveStar](./favestar.md) +- [IconButton](./iconbutton.md) +- [Icons](./icons.md) +- [IconTooltip](./icontooltip.md) +- [InfoTooltip](./infotooltip.md) +- [Input](./input.md) +- [Label](./label.md) +- [List](./list.md) +- [ListViewCard](./listviewcard.md) +- [Loading](./loading.md) +- [Menu](./menu.md) +- [Modal](./modal.md) +- [ModalTrigger](./modaltrigger.md) +- [Popover](./popover.md) +- [ProgressBar](./progressbar.md) +- [Radio](./radio.md) +- [SafeMarkdown](./safemarkdown.md) +- [Select](./select.md) +- [Skeleton](./skeleton.md) +- [Slider](./slider.md) +- [Steps](./steps.md) +- [Switch](./switch.md) +- [TableCollection](./tablecollection.md) +- [TableView](./tableview.md) +- [Tabs](./tabs.md) +- [Timer](./timer.md) +- [Tooltip](./tooltip.md) +- [Tree](./tree.md) +- [TreeSelect](./treeselect.md) +- [Typography](./typography.md) +- [UnsavedChangesModal](./unsavedchangesmodal.md) +- [Upload](./upload.md) diff --git a/docs/developer_docs/contributing/code-review.md b/docs/developer_docs/contributing/code-review.md index 3f1f9280942..f002c78d7ca 100644 --- a/docs/developer_docs/contributing/code-review.md +++ b/docs/developer_docs/contributing/code-review.md @@ -327,9 +327,9 @@ stats.sort_stats('cumulative').print_stats(10) ## Resources ### Internal -- [Coding Guidelines](../guidelines/design-guidelines) -- [Testing Guide](../testing/overview) -- [Extension Architecture](../extensions/architecture) +- [Coding Guidelines](../guidelines/design-guidelines.md) +- [Testing Guide](../testing/overview.md) +- [Extension Architecture](../extensions/architecture.md) ### External - [Google's Code Review Guide](https://google.github.io/eng-practices/review/) diff --git a/docs/developer_docs/contributing/development-setup.md b/docs/developer_docs/contributing/development-setup.md index 07d07eebb2b..6a8bca85fd2 100644 --- a/docs/developer_docs/contributing/development-setup.md +++ b/docs/developer_docs/contributing/development-setup.md @@ -668,7 +668,7 @@ A series of checks will now run when you make a git commit. ## Linting -See [how tos](./howtos#linting) +See [how tos](./howtos.md#linting) ## GitHub Actions and `act` diff --git a/docs/developer_docs/contributing/guidelines.md b/docs/developer_docs/contributing/guidelines.md index 63f808a5e7d..c62b6d4eca8 100644 --- a/docs/developer_docs/contributing/guidelines.md +++ b/docs/developer_docs/contributing/guidelines.md @@ -77,7 +77,7 @@ Finally, never submit a PR that will put master branch in broken state. If the P in `requirements.txt` pinned to a specific version which ensures that the application build is deterministic. - For TypeScript/JavaScript, include new libraries in `package.json` -- **Tests:** The pull request should include tests, either as doctests, unit tests, or both. Make sure to resolve all errors and test failures. See [Testing](./howtos#testing) for how to run tests. +- **Tests:** The pull request should include tests, either as doctests, unit tests, or both. Make sure to resolve all errors and test failures. See [Testing](./howtos.md#testing) for how to run tests. - **Documentation:** If the pull request adds functionality, the docs should be updated as part of the same PR. - **CI:** Reviewers will not review the code until all CI tests are passed. Sometimes there can be flaky tests. You can close and open PR to re-run CI test. Please report if the issue persists. After the CI fix has been deployed to `master`, please rebase your PR. - **Code coverage:** Please ensure that code coverage does not decrease. diff --git a/docs/developer_docs/contributing/howtos.md b/docs/developer_docs/contributing/howtos.md index e01d54de334..40ae473183f 100644 --- a/docs/developer_docs/contributing/howtos.md +++ b/docs/developer_docs/contributing/howtos.md @@ -282,7 +282,7 @@ You can now launch your VSCode debugger with the same config as above. VSCode wi ### Storybook -See the dedicated [Storybook documentation](../testing/storybook) for information on running Storybook locally and adding new stories. +See the dedicated [Storybook documentation](../testing/storybook.md) for information on running Storybook locally and adding new stories. ## Contributing Translations diff --git a/docs/developer_docs/contributing/overview.md b/docs/developer_docs/contributing/overview.md index b4d3bab085b..d0e6c5ba659 100644 --- a/docs/developer_docs/contributing/overview.md +++ b/docs/developer_docs/contributing/overview.md @@ -94,7 +94,7 @@ Look through the GitHub issues. Issues tagged with Superset could always use better documentation, whether as part of the official Superset docs, in docstrings, `docs/*.rst` or even on the web as blog posts or -articles. See [Documentation](./howtos#contributing-to-documentation) for more details. +articles. See [Documentation](./howtos.md#contributing-to-documentation) for more details. ### Add Translations @@ -103,7 +103,7 @@ text strings from Superset's UI. You can jump into the existing language dictionaries at `superset/translations//LC_MESSAGES/messages.po`, or even create a dictionary for a new language altogether. -See [Translating](./howtos#contributing-translations) for more details. +See [Translating](./howtos.md#contributing-translations) for more details. ### Ask Questions diff --git a/docs/developer_docs/contributing/submitting-pr.md b/docs/developer_docs/contributing/submitting-pr.md index 42b12a46ea6..4836aa47f63 100644 --- a/docs/developer_docs/contributing/submitting-pr.md +++ b/docs/developer_docs/contributing/submitting-pr.md @@ -35,7 +35,7 @@ Learn how to create and submit high-quality pull requests to Apache Superset. - [ ] You've found or created an issue to work on ### PR Readiness Checklist -- [ ] Code follows [coding guidelines](../guidelines/design-guidelines) +- [ ] Code follows [coding guidelines](../guidelines/design-guidelines.md) - [ ] Tests are passing locally - [ ] Linting passes (`pre-commit run --all-files`) - [ ] Documentation is updated if needed diff --git a/docs/developer_docs/extensions/contribution-types.md b/docs/developer_docs/extensions/contribution-types.md index 5033806df59..e765c5009c4 100644 --- a/docs/developer_docs/extensions/contribution-types.md +++ b/docs/developer_docs/extensions/contribution-types.md @@ -110,7 +110,7 @@ editors.registerEditor( ); ``` -See [Editors Extension Point](./extension-points/editors) for implementation details. +See [Editors Extension Point](./extension-points/editors.md) for implementation details. ## Backend diff --git a/docs/developer_docs/extensions/extension-points/editors.md b/docs/developer_docs/extensions/extension-points/editors.md index a2d05cc5204..aff1156b3ff 100644 --- a/docs/developer_docs/extensions/extension-points/editors.md +++ b/docs/developer_docs/extensions/extension-points/editors.md @@ -218,5 +218,5 @@ const disposable = handle.registerCompletionProvider(provider); ## Next Steps - **[SQL Lab Extension Points](./sqllab.md)** - Learn about other SQL Lab customizations -- **[Contribution Types](../contribution-types)** - Explore other contribution types -- **[Development](../development)** - Set up your development environment +- **[Contribution Types](../contribution-types.md)** - Explore other contribution types +- **[Development](../development.md)** - Set up your development environment diff --git a/docs/developer_docs/extensions/extension-points/sqllab.md b/docs/developer_docs/extensions/extension-points/sqllab.md index ef613cf12f0..959e89fa3ae 100644 --- a/docs/developer_docs/extensions/extension-points/sqllab.md +++ b/docs/developer_docs/extensions/extension-points/sqllab.md @@ -157,6 +157,6 @@ menus.registerMenuItem( ## Next Steps -- **[Contribution Types](../contribution-types)** - Learn about other contribution types (commands, menus) -- **[Development](../development)** - Set up your development environment -- **[Quick Start](../quick-start)** - Build a complete extension +- **[Contribution Types](../contribution-types.md)** - Learn about other contribution types (commands, menus) +- **[Development](../development.md)** - Set up your development environment +- **[Quick Start](../quick-start.md)** - Build a complete extension diff --git a/docs/developer_docs/extensions/quick-start.md b/docs/developer_docs/extensions/quick-start.md index 818b68c3573..1ef35833397 100644 --- a/docs/developer_docs/extensions/quick-start.md +++ b/docs/developer_docs/extensions/quick-start.md @@ -225,7 +225,7 @@ The `@apache-superset/core` package must be listed in both `peerDependencies` (t The webpack configuration requires specific settings for Module Federation. Key settings include `externalsType: "window"` and `externals` to map `@apache-superset/core` to `window.superset` at runtime, `import: false` for shared modules to use the host's React instead of bundling a separate copy, and `remoteEntry.[contenthash].js` for cache busting. -**Convention**: Superset always loads extensions by requesting the `./index` module from the Module Federation container. The `exposes` entry must be exactly `'./index': './src/index.tsx'` — do not rename or add additional entries. All API registrations must be reachable from that file. See [Architecture](./architecture#module-federation) for a full explanation. +**Convention**: Superset always loads extensions by requesting the `./index` module from the Module Federation container. The `exposes` entry must be exactly `'./index': './src/index.tsx'` — do not rename or add additional entries. All API registrations must be reachable from that file. See [Architecture](./architecture.md#module-federation) for a full explanation. ```javascript const path = require('path'); diff --git a/docs/developer_docs/guidelines/backend-style-guidelines.md b/docs/developer_docs/guidelines/backend-style-guidelines.md index d938298d8a6..ae33641dd3a 100644 --- a/docs/developer_docs/guidelines/backend-style-guidelines.md +++ b/docs/developer_docs/guidelines/backend-style-guidelines.md @@ -114,7 +114,7 @@ class CreateDashboardCommand(BaseCommand): ### Data Access Objects (DAOs) -See: [DAO Style Guidelines and Best Practices](./backend/dao-style-guidelines) +See: [DAO Style Guidelines and Best Practices](./backend/dao-style-guidelines.md) ## Testing diff --git a/docs/developer_docs/guidelines/frontend-style-guidelines.md b/docs/developer_docs/guidelines/frontend-style-guidelines.md index 7a4292d492f..fa4b43146e9 100644 --- a/docs/developer_docs/guidelines/frontend-style-guidelines.md +++ b/docs/developer_docs/guidelines/frontend-style-guidelines.md @@ -29,16 +29,16 @@ This is a list of statements that describe how we do frontend development in Sup - We develop using TypeScript. - See: [SIP-36](https://github.com/apache/superset/issues/9101) - We use React for building components, and Redux to manage app/global state. - - See: [Component Style Guidelines and Best Practices](./frontend/component-style-guidelines) + - See: [Component Style Guidelines and Best Practices](./frontend/component-style-guidelines.md) - We prefer functional components to class components and use hooks for local component state. - We use [Ant Design](https://ant.design/) components from our component library whenever possible, only building our own custom components when it's required. - See: [SIP-48](https://github.com/apache/superset/issues/11283) - We use [@emotion](https://emotion.sh/docs/introduction) to provide styling for our components, co-locating styling within component files. - See: [SIP-37](https://github.com/apache/superset/issues/9145) - - See: [Emotion Styling Guidelines and Best Practices](./frontend/emotion-styling-guidelines) + - See: [Emotion Styling Guidelines and Best Practices](./frontend/emotion-styling-guidelines.md) - We use Jest for unit tests, React Testing Library for component tests, and Cypress for end-to-end tests. - See: [SIP-56](https://github.com/apache/superset/issues/11830) - - See: [Testing Guidelines and Best Practices](../testing/testing-guidelines) + - See: [Testing Guidelines and Best Practices](../testing/testing-guidelines.md) - We add tests for every new component or file added to the frontend. - We organize our repo so similar files live near each other, and tests are co-located with the files they test. - See: [SIP-61](https://github.com/apache/superset/issues/12098) @@ -46,6 +46,6 @@ This is a list of statements that describe how we do frontend development in Sup - We use OXC (oxlint) and Prettier to automatically fix lint errors and format the code. - We do not debate code formatting style in PRs, instead relying on automated tooling to enforce it. - If there's not a linting rule, we don't have a rule! - - See: [Linting How-Tos](../contributing/howtos#typescript--javascript) + - See: [Linting How-Tos](../contributing/howtos.md#typescript--javascript) - We use [React Storybook](https://storybook.js.org/) to help preview/test and stabilize our components - A public Storybook with components from the `master` branch is available [here](https://apache-superset.github.io/superset-ui/?path=/story/*) diff --git a/docs/developer_docs/guidelines/frontend/component-style-guidelines.md b/docs/developer_docs/guidelines/frontend/component-style-guidelines.md index 76e5e7915de..59b422a0496 100644 --- a/docs/developer_docs/guidelines/frontend/component-style-guidelines.md +++ b/docs/developer_docs/guidelines/frontend/component-style-guidelines.md @@ -53,7 +53,7 @@ superset-frontend/src/components **Storybook:** Components should come with a storybook file whenever applicable, with the following naming convention `\{ComponentName\}.stories.tsx`. More details about Storybook below -**Unit and end-to-end tests:** All components should come with unit tests using Jest and React Testing Library. The file name should follow this naming convention `\{ComponentName\}.test.tsx`. Read the [Testing Guidelines and Best Practices](../../testing/testing-guidelines) for more details +**Unit and end-to-end tests:** All components should come with unit tests using Jest and React Testing Library. The file name should follow this naming convention `\{ComponentName\}.test.tsx`. Read the [Testing Guidelines and Best Practices](../../testing/testing-guidelines.md) for more details **Reference naming:** Use `PascalCase` for React components and `camelCase` for component instances diff --git a/docs/package.json b/docs/package.json index af522416ebe..250ba939580 100644 --- a/docs/package.json +++ b/docs/package.json @@ -30,6 +30,7 @@ "lint:db-metadata:report": "python3 ../superset/db_engine_specs/lint_metadata.py --markdown -o ../superset/db_engine_specs/METADATA_STATUS.md", "update:readme-db-logos": "node scripts/generate-database-docs.mjs --update-readme", "eslint": "eslint .", + "lint:docs-links": "node scripts/lint-docs-links.mjs", "version:add": "node scripts/manage-versions.mjs add", "version:remove": "node scripts/manage-versions.mjs remove", "version:add:docs": "node scripts/manage-versions.mjs add docs", diff --git a/docs/scripts/generate-superset-components.mjs b/docs/scripts/generate-superset-components.mjs index 2244631d7ca..c3ca472a26b 100644 --- a/docs/scripts/generate-superset-components.mjs +++ b/docs/scripts/generate-superset-components.mjs @@ -1260,7 +1260,13 @@ function generateCategoryIndex(category, components) { }; const componentList = components .sort((a, b) => a.componentName.localeCompare(b.componentName)) - .map(c => `- [${c.componentName}](./${c.componentName.toLowerCase()})`) + // `.md` suffix on the relative link is required: Docusaurus only + // validates and rewrites *file-based* references (.md/.mdx). Bare + // relative paths bypass the file resolver and get emitted as raw + // HTML hrefs that the browser resolves against the current URL — + // which gives the wrong directory for trailing-slash routes and + // breaks SPA navigation. See docs/scripts/lint-docs-links.mjs. + .map(c => `- [${c.componentName}](./${c.componentName.toLowerCase()}.md)`) .join('\n'); return `--- @@ -1366,7 +1372,7 @@ This documentation is auto-generated from Storybook stories. To add or update co 4. Run \`yarn generate:superset-components\` in the \`docs/\` directory :::info Work in Progress -This component library is actively being documented. See the [Components TODO](./TODO) page for a list of components awaiting documentation. +This component library is actively being documented. See the [Components TODO](./TODO.md) page for a list of components awaiting documentation. ::: --- diff --git a/docs/scripts/lint-docs-links.mjs b/docs/scripts/lint-docs-links.mjs new file mode 100644 index 00000000000..f39fdf57fc0 --- /dev/null +++ b/docs/scripts/lint-docs-links.mjs @@ -0,0 +1,178 @@ +#!/usr/bin/env node +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * lint-docs-links — fail on bare relative internal links in markdown. + * + * Why this exists + * ─────────────── + * Docusaurus's `onBrokenLinks: 'throw'` build setting only validates + * *file-based* markdown references — links whose URL ends in `.md` or + * `.mdx`. Those go through the file resolver, which knows how to map + * a source file to its final URL and can flag a missing target. + * + * Bare relative URL paths like `[Foo](../foo)` or `[Bar](./bar)` skip + * the file resolver entirely. Docusaurus emits them as raw `href` + * attributes that the browser then resolves against the *current* + * page URL. For trailing-slash routes (which is what Docusaurus uses + * by default), `../foo` from `/section/group/page/` lands on + * `/section/group/foo` — usually the wrong directory. The page + * navigates client-side and 404s. + * + * The `build` job + `onBrokenLinks: 'throw'` cannot catch this class + * of bug. The `linkinator` job *can* (it crawls rendered HTML) but is + * configured `continue-on-error: true` so failures are advisory. + * + * This script runs at PR time as a fast, blocking source-level check. + * It scans every `.md` and `.mdx` file under the active content + * trees (skipping `versioned_docs/` snapshots, which are frozen + * historical content) and fails if it finds any markdown link whose + * URL starts with `./` or `../` and does NOT end in `.md`/`.mdx`. + * + * Excluded: + * - URLs inside fenced code blocks + * - URLs that point at images / static assets (`.png`, `.svg`, …) + * - URLs that point at non-content files (`.json`, `.yaml`, …) + * + * Run from `docs/`: + * node scripts/lint-docs-links.mjs + * + * Exits 0 on clean, 1 on any finding. + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const docsRoot = path.join(__dirname, '..'); + +// Active content trees. We deliberately skip `*_versioned_docs/` — +// those are snapshots of historical content; even if they have +// pre-existing issues we don't want to rewrite history. +const ROOTS = ['docs', 'admin_docs', 'developer_docs', 'components']; + +// Link target file extensions that are NOT documentation pages +// (images, data, etc.) — these are legitimately allowed to be bare +// relative paths. +const NON_DOC_EXTENSIONS = new Set([ + '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico', + '.json', '.yaml', '.yml', '.txt', '.csv', + '.zip', '.tar', '.gz', + '.pdf', + '.mp4', '.webm', '.mov', +]); + +// Matches `[label](url)` where url starts with `./` or `../`. +// Multiline-safe inside a single line. +const LINK_RE = /\[[^\]\n]+?\]\((?\.{1,2}\/[^)\s]+?)\)/g; + +function classifyUrl(url) { + // Strip anchor / query before extension test. + const main = url.split('#', 1)[0].split('?', 1)[0]; + const ext = path.extname(main).toLowerCase(); + if (ext === '.md' || ext === '.mdx') return 'doc-with-ext'; + if (ext && NON_DOC_EXTENSIONS.has(ext)) return 'asset'; + return 'bare'; +} + +function* walk(dir) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + // Skip versioned snapshots and node_modules etc. + if ( + entry.name.startsWith('.') || + entry.name === 'node_modules' || + entry.name.endsWith('_versioned_docs') || + entry.name === 'versioned_docs' + ) { + continue; + } + yield* walk(full); + } else if (entry.isFile()) { + if (entry.name.endsWith('.md') || entry.name.endsWith('.mdx')) { + yield full; + } + } + } +} + +function lintFile(file) { + const src = fs.readFileSync(file, 'utf8'); + const findings = []; + let inFence = false; + const lines = src.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.trimStart().startsWith('```')) { + inFence = !inFence; + continue; + } + if (inFence) continue; + for (const m of line.matchAll(LINK_RE)) { + const url = m.groups.url; + if (classifyUrl(url) === 'bare') { + findings.push({ line: i + 1, url, raw: line.trim() }); + } + } + } + return findings; +} + +const findings = []; +for (const root of ROOTS) { + const abs = path.join(docsRoot, root); + if (!fs.existsSync(abs)) continue; + for (const file of walk(abs)) { + const fileFindings = lintFile(file); + for (const f of fileFindings) { + findings.push({ file: path.relative(docsRoot, file), ...f }); + } + } +} + +if (findings.length === 0) { + console.log('✓ lint-docs-links: no bare relative internal links found'); + process.exit(0); +} + +console.error( + `✗ lint-docs-links: found ${findings.length} bare relative internal link(s)` +); +console.error(''); +console.error( + 'Bare relative links like `[X](../foo)` skip Docusaurus\'s file resolver' +); +console.error( + 'and get emitted as raw hrefs that the browser resolves against the' +); +console.error( + 'current page URL — wrong directory for trailing-slash routes. Add a' +); +console.error('`.md` (or `.mdx`) extension so the file resolver picks them up.'); +console.error(''); +for (const f of findings) { + console.error(` ${f.file}:${f.line} ${f.url}`); +} +console.error(''); +process.exit(1);