Compare commits

...

2 Commits

Author SHA1 Message Date
Joshua Chen ef937e402c
fix test 2022-06-11 18:08:57 +08:00
Joshua Chen 2da34337a6
feat: report orphan pages 2022-06-11 17:35:57 +08:00
6 changed files with 93 additions and 43 deletions

View File

@ -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.
*

View File

@ -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(),

View File

@ -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',
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',
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',
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',
props: {
routes,
baseUrl: '/',
outDir,
siteConfig: {
onBrokenLinks: 'throw',
} as DocusaurusConfig,
} as Props,
}),
).rejects.toThrowErrorMatchingSnapshot();
});

View File

@ -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;
}

View File

@ -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),

View File

@ -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'),