Compare commits
2 Commits
main
...
jc/orphan-
| Author | SHA1 | Date |
|---|---|---|
|
|
ef937e402c | |
|
|
2da34337a6 |
|
|
@ -146,6 +146,10 @@ export type DocusaurusConfig = {
|
|||
* @default false
|
||||
*/
|
||||
noIndex: boolean;
|
||||
orphanPages?: {
|
||||
onOrphanPage: ReportingSeverity;
|
||||
entryPoints: string[];
|
||||
};
|
||||
/**
|
||||
* The behavior of Docusaurus when it detects any broken link.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -132,12 +132,7 @@ async function buildLocale({
|
|||
outDir,
|
||||
generatedFilesDir,
|
||||
plugins,
|
||||
siteConfig: {
|
||||
baseUrl,
|
||||
onBrokenLinks,
|
||||
staticDirectories: staticDirectoriesOption,
|
||||
},
|
||||
routes,
|
||||
siteConfig: {staticDirectories: staticDirectoriesOption},
|
||||
} = props;
|
||||
|
||||
const clientManifestPath = path.join(
|
||||
|
|
@ -264,13 +259,7 @@ async function buildLocale({
|
|||
}),
|
||||
);
|
||||
|
||||
await handleBrokenLinks({
|
||||
allCollectedLinks,
|
||||
routes,
|
||||
onBrokenLinks,
|
||||
outDir,
|
||||
baseUrl,
|
||||
});
|
||||
await handleBrokenLinks({allCollectedLinks, props});
|
||||
|
||||
logger.success`Generated static files in path=${path.relative(
|
||||
process.cwd(),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {jest} from '@jest/globals';
|
|||
import path from 'path';
|
||||
import _ from 'lodash';
|
||||
import {handleBrokenLinks} from '../brokenLinks';
|
||||
import type {RouteConfig} from '@docusaurus/types';
|
||||
import type {DocusaurusConfig, Props, RouteConfig} from '@docusaurus/types';
|
||||
|
||||
describe('handleBrokenLinks', () => {
|
||||
const routes: RouteConfig[] = [
|
||||
|
|
@ -136,10 +136,14 @@ describe('handleBrokenLinks', () => {
|
|||
};
|
||||
await handleBrokenLinks({
|
||||
allCollectedLinks: allCollectedCorrectLinks,
|
||||
onBrokenLinks: 'error',
|
||||
routes,
|
||||
baseUrl: '/',
|
||||
outDir,
|
||||
props: {
|
||||
routes,
|
||||
baseUrl: '/',
|
||||
outDir,
|
||||
siteConfig: {
|
||||
onBrokenLinks: 'error',
|
||||
} as DocusaurusConfig,
|
||||
} as Props,
|
||||
});
|
||||
expect(consoleMock).toBeCalledTimes(0);
|
||||
});
|
||||
|
|
@ -148,10 +152,14 @@ describe('handleBrokenLinks', () => {
|
|||
await expect(() =>
|
||||
handleBrokenLinks({
|
||||
allCollectedLinks,
|
||||
onBrokenLinks: 'throw',
|
||||
routes,
|
||||
baseUrl: '/',
|
||||
outDir,
|
||||
props: {
|
||||
routes,
|
||||
baseUrl: '/',
|
||||
outDir,
|
||||
siteConfig: {
|
||||
onBrokenLinks: 'throw',
|
||||
} as DocusaurusConfig,
|
||||
} as Props,
|
||||
}),
|
||||
).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
|
@ -162,10 +170,14 @@ describe('handleBrokenLinks', () => {
|
|||
const lodashMock = jest.spyOn(_, 'mapValues');
|
||||
await handleBrokenLinks({
|
||||
allCollectedLinks,
|
||||
onBrokenLinks: 'ignore',
|
||||
routes,
|
||||
baseUrl: '/',
|
||||
outDir,
|
||||
props: {
|
||||
routes,
|
||||
baseUrl: '/',
|
||||
outDir,
|
||||
siteConfig: {
|
||||
onBrokenLinks: 'ignore',
|
||||
} as DocusaurusConfig,
|
||||
} as Props,
|
||||
});
|
||||
expect(lodashMock).toBeCalledTimes(0);
|
||||
lodashMock.mockRestore();
|
||||
|
|
@ -185,10 +197,14 @@ describe('handleBrokenLinks', () => {
|
|||
await expect(() =>
|
||||
handleBrokenLinks({
|
||||
allCollectedLinks,
|
||||
onBrokenLinks: 'throw',
|
||||
routes,
|
||||
baseUrl: '/',
|
||||
outDir,
|
||||
props: {
|
||||
routes,
|
||||
baseUrl: '/',
|
||||
outDir,
|
||||
siteConfig: {
|
||||
onBrokenLinks: 'throw',
|
||||
} as DocusaurusConfig,
|
||||
} as Props,
|
||||
}),
|
||||
).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import {
|
|||
resolvePathname,
|
||||
} from '@docusaurus/utils';
|
||||
import {getAllFinalRoutes} from './utils';
|
||||
import type {RouteConfig, ReportingSeverity} from '@docusaurus/types';
|
||||
import type {RouteConfig, Props, DocusaurusConfig} from '@docusaurus/types';
|
||||
|
||||
type BrokenLink = {
|
||||
link: string;
|
||||
|
|
@ -52,8 +52,7 @@ function getPageBrokenLinks({
|
|||
// @ts-expect-error: React router types RouteConfig with an actual React
|
||||
// component, but we load route components with string paths.
|
||||
// We don't actually access component here, so it's fine.
|
||||
.map((l) => matchRoutes(routes, l))
|
||||
.flat();
|
||||
.flatMap((l) => matchRoutes(routes, l));
|
||||
return matchedRoutes.length === 0;
|
||||
}
|
||||
|
||||
|
|
@ -78,10 +77,8 @@ function getAllBrokenLinks({
|
|||
allCollectedLinks: {[location: string]: string[]};
|
||||
routes: RouteConfig[];
|
||||
}): {[location: string]: BrokenLink[]} {
|
||||
const filteredRoutes = filterIntermediateRoutes(routes);
|
||||
|
||||
const allBrokenLinks = _.mapValues(allCollectedLinks, (pageLinks, pagePath) =>
|
||||
getPageBrokenLinks({pageLinks, pagePath, routes: filteredRoutes}),
|
||||
getPageBrokenLinks({pageLinks, pagePath, routes}),
|
||||
);
|
||||
|
||||
return _.pickBy(allBrokenLinks, (brokenLinks) => brokenLinks.length > 0);
|
||||
|
|
@ -214,19 +211,53 @@ async function filterExistingFileLinks({
|
|||
);
|
||||
}
|
||||
|
||||
export async function handleBrokenLinks({
|
||||
function findOrphanLinks({
|
||||
allCollectedLinks,
|
||||
onBrokenLinks,
|
||||
orphanPages,
|
||||
routes,
|
||||
baseUrl,
|
||||
outDir,
|
||||
}: {
|
||||
allCollectedLinks: {[location: string]: string[]};
|
||||
onBrokenLinks: ReportingSeverity;
|
||||
orphanPages: DocusaurusConfig['orphanPages'];
|
||||
routes: RouteConfig[];
|
||||
baseUrl: string;
|
||||
outDir: string;
|
||||
}) {
|
||||
if (!orphanPages || orphanPages.onOrphanPage === 'ignore') {
|
||||
return;
|
||||
}
|
||||
const visited = new Set<string>();
|
||||
function dfs(link: string) {
|
||||
// @ts-expect-error: see comment above
|
||||
const normalLink = matchRoutes(routes, link)[0]?.match.path;
|
||||
if (!normalLink || visited.has(normalLink)) {
|
||||
return;
|
||||
}
|
||||
visited.add(normalLink);
|
||||
allCollectedLinks[normalLink]?.forEach((l) =>
|
||||
dfs(resolvePathname(l, link)),
|
||||
);
|
||||
}
|
||||
orphanPages.entryPoints.forEach(dfs);
|
||||
const orphaned = routes.map((r) => r.path).filter((l) => !visited.has(l));
|
||||
reportMessage(
|
||||
logger.interpolate`Orphan pages found: url=${Array.from(orphaned)}`,
|
||||
orphanPages.onOrphanPage,
|
||||
);
|
||||
}
|
||||
|
||||
export async function handleBrokenLinks({
|
||||
allCollectedLinks,
|
||||
props: {
|
||||
routes: allRoutes,
|
||||
baseUrl,
|
||||
outDir,
|
||||
siteConfig: {onBrokenLinks, orphanPages},
|
||||
},
|
||||
}: {
|
||||
allCollectedLinks: {[location: string]: string[]};
|
||||
props: Props;
|
||||
}): Promise<void> {
|
||||
const routes = filterIntermediateRoutes(allRoutes);
|
||||
findOrphanLinks({allCollectedLinks, orphanPages, routes});
|
||||
|
||||
if (onBrokenLinks === 'ignore') {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,6 +227,12 @@ export const ConfigSchema = Joi.object<DocusaurusConfig>({
|
|||
clientModules: Joi.array()
|
||||
.items(Joi.string())
|
||||
.default(DEFAULT_CONFIG.clientModules),
|
||||
orphanPages: Joi.object({
|
||||
onOrphanPage: Joi.string()
|
||||
.equal('ignore', 'log', 'warn', 'error', 'throw')
|
||||
.default('warn'),
|
||||
entryPoints: Joi.array().items(Joi.string()).default([]),
|
||||
}),
|
||||
tagline: Joi.string().allow('').default(DEFAULT_CONFIG.tagline),
|
||||
titleDelimiter: Joi.string().default(DEFAULT_CONFIG.titleDelimiter),
|
||||
noIndex: Joi.bool().default(DEFAULT_CONFIG.noIndex),
|
||||
|
|
|
|||
|
|
@ -117,6 +117,10 @@ const config = {
|
|||
description:
|
||||
'An optimized site generator in React. Docusaurus helps you to move fast and write content. Build documentation websites, blogs, marketing pages, and more.',
|
||||
},
|
||||
orphanPages: {
|
||||
onOrphanPage: 'warn',
|
||||
entryPoints: ['/', '/tests'],
|
||||
},
|
||||
staticDirectories: [
|
||||
'static',
|
||||
path.join(__dirname, '_dogfooding/_asset-tests'),
|
||||
|
|
|
|||
Loading…
Reference in New Issue