mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-05 04:12:53 +02:00
refactor(core): code cleanup (#7084)
This commit is contained in:
parent
ff96606865
commit
898611d4ad
16 changed files with 114 additions and 179 deletions
1
packages/docusaurus-types/src/index.d.ts
vendored
1
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -227,7 +227,6 @@ export type LoadContext = {
|
||||||
*/
|
*/
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
i18n: I18n;
|
i18n: I18n;
|
||||||
ssrTemplate: string;
|
|
||||||
codeTranslations: CodeTranslations;
|
codeTranslations: CodeTranslations;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import renderRoutes from './exports/renderRoutes';
|
||||||
import {BrowserContextProvider} from './browserContext';
|
import {BrowserContextProvider} from './browserContext';
|
||||||
import {DocusaurusContextProvider} from './docusaurusContext';
|
import {DocusaurusContextProvider} from './docusaurusContext';
|
||||||
import PendingNavigation from './PendingNavigation';
|
import PendingNavigation from './PendingNavigation';
|
||||||
import BaseUrlIssueBanner from './baseUrlIssueBanner/BaseUrlIssueBanner';
|
import BaseUrlIssueBanner from './BaseUrlIssueBanner';
|
||||||
import SiteMetadataDefaults from './SiteMetadataDefaults';
|
import SiteMetadataDefaults from './SiteMetadataDefaults';
|
||||||
import Root from '@theme/Root';
|
import Root from '@theme/Root';
|
||||||
import SiteMetadata from '@theme/SiteMetadata';
|
import SiteMetadata from '@theme/SiteMetadata';
|
||||||
|
|
|
@ -74,11 +74,11 @@ function insertBanner() {
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
__DOCUSAURUS_INSERT_BASEURL_BANNER: boolean;
|
[InsertBannerWindowAttribute]: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function BaseUrlIssueBannerEnabled() {
|
function BaseUrlIssueBanner() {
|
||||||
const {
|
const {
|
||||||
siteConfig: {baseUrl},
|
siteConfig: {baseUrl},
|
||||||
} = useDocusaurusContext();
|
} = useDocusaurusContext();
|
||||||
|
@ -92,6 +92,8 @@ function BaseUrlIssueBannerEnabled() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!ExecutionEnvironment.canUseDOM && (
|
{!ExecutionEnvironment.canUseDOM && (
|
||||||
|
// Safe to use `ExecutionEnvironment`, because `Head` is purely
|
||||||
|
// side-effect and doesn't affect hydration
|
||||||
<Head>
|
<Head>
|
||||||
<script>{createInlineScript(baseUrl)}</script>
|
<script>{createInlineScript(baseUrl)}</script>
|
||||||
</Head>
|
</Head>
|
||||||
|
@ -103,22 +105,22 @@ function BaseUrlIssueBannerEnabled() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We want to help the users with a bad baseUrl configuration (very common
|
* We want to help the users with a bad baseUrl configuration (very common
|
||||||
* error) Help message is inlined, and hidden if JS or CSS is able to load
|
* error). Help message is inlined, and hidden if JS or CSS is able to load.
|
||||||
|
*
|
||||||
|
* This component only inserts the base URL banner for the homepage, to avoid
|
||||||
|
* polluting every statically rendered page.
|
||||||
|
*
|
||||||
* Note: it might create false positives (ie network failures): not a big deal
|
* Note: it might create false positives (ie network failures): not a big deal
|
||||||
* Note: we only inline this for the homepage to avoid polluting all the site's
|
*
|
||||||
* pages
|
|
||||||
* @see https://github.com/facebook/docusaurus/pull/3621
|
* @see https://github.com/facebook/docusaurus/pull/3621
|
||||||
*/
|
*/
|
||||||
export default function BaseUrlIssueBanner(): JSX.Element | null {
|
export default function MaybeBaseUrlIssueBanner(): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
siteConfig: {baseUrl, baseUrlIssueBanner},
|
siteConfig: {baseUrl, baseUrlIssueBanner},
|
||||||
} = useDocusaurusContext();
|
} = useDocusaurusContext();
|
||||||
const {pathname} = useLocation();
|
const {pathname} = useLocation();
|
||||||
|
// returns true for the homepage during SSR
|
||||||
// returns true for the homepage during SRR
|
|
||||||
const isHomePage = pathname === baseUrl;
|
const isHomePage = pathname === baseUrl;
|
||||||
|
|
||||||
const enabled = baseUrlIssueBanner && isHomePage;
|
const enabled = baseUrlIssueBanner && isHomePage;
|
||||||
|
return enabled ? <BaseUrlIssueBanner /> : null;
|
||||||
return enabled ? <BaseUrlIssueBannerEnabled /> : null;
|
|
||||||
}
|
}
|
|
@ -7,16 +7,14 @@
|
||||||
|
|
||||||
import React, {isValidElement} from 'react';
|
import React, {isValidElement} from 'react';
|
||||||
import useIsBrowser from '@docusaurus/useIsBrowser';
|
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||||
|
import type {Props} from '@docusaurus/BrowserOnly';
|
||||||
|
|
||||||
// Similar comp to the one described here:
|
// Similar comp to the one described here:
|
||||||
// https://www.joshwcomeau.com/react/the-perils-of-rehydration/#abstractions
|
// https://www.joshwcomeau.com/react/the-perils-of-rehydration/#abstractions
|
||||||
export default function BrowserOnly({
|
export default function BrowserOnly({
|
||||||
children,
|
children,
|
||||||
fallback,
|
fallback,
|
||||||
}: {
|
}: Props): JSX.Element | null {
|
||||||
children: () => JSX.Element;
|
|
||||||
fallback?: JSX.Element;
|
|
||||||
}): JSX.Element | null {
|
|
||||||
const isBrowser = useIsBrowser();
|
const isBrowser = useIsBrowser();
|
||||||
|
|
||||||
if (isBrowser) {
|
if (isBrowser) {
|
||||||
|
@ -27,7 +25,7 @@ export default function BrowserOnly({
|
||||||
throw new Error(`Docusaurus error: The children of <BrowserOnly> must be a "render function", e.g. <BrowserOnly>{() => <span>{window.location.href}</span>}</BrowserOnly>.
|
throw new Error(`Docusaurus error: The children of <BrowserOnly> must be a "render function", e.g. <BrowserOnly>{() => <span>{window.location.href}</span>}</BrowserOnly>.
|
||||||
Current type: ${isValidElement(children) ? 'React element' : typeof children}`);
|
Current type: ${isValidElement(children) ? 'React element' : typeof children}`);
|
||||||
}
|
}
|
||||||
return <>{children()}</>;
|
return <>{children?.()}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fallback ?? null;
|
return fallback ?? null;
|
||||||
|
|
|
@ -18,7 +18,6 @@ We don't ship a markdown parser nor a feature-complete i18n library on purpose.
|
||||||
More details here: https://github.com/facebook/docusaurus/pull/4295
|
More details here: https://github.com/facebook/docusaurus/pull/4295
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const ValueRegexp = /\{\w+\}/g;
|
|
||||||
const ValueFoundMarker = '{}'; // does not care much
|
const ValueFoundMarker = '{}'; // does not care much
|
||||||
|
|
||||||
// If all the values are plain strings, then interpolate returns a simple string
|
// If all the values are plain strings, then interpolate returns a simple string
|
||||||
|
@ -39,51 +38,44 @@ export function interpolate<Str extends string, Value extends ReactNode>(
|
||||||
): ReactNode {
|
): ReactNode {
|
||||||
const elements: (Value | string)[] = [];
|
const elements: (Value | string)[] = [];
|
||||||
|
|
||||||
const processedText = text.replace(ValueRegexp, (match: string) => {
|
const processedText = text.replace(
|
||||||
// remove {{ and }} around the placeholder
|
// eslint-disable-next-line prefer-named-capture-group
|
||||||
const key = match.substring(
|
/\{(\w+)\}/g,
|
||||||
1,
|
(match, key: ExtractInterpolatePlaceholders<Str>) => {
|
||||||
match.length - 1,
|
const value = values?.[key];
|
||||||
) as ExtractInterpolatePlaceholders<Str>;
|
|
||||||
|
|
||||||
const value = values?.[key];
|
if (typeof value !== 'undefined') {
|
||||||
|
const element = isValidElement(value)
|
||||||
if (typeof value !== 'undefined') {
|
? value
|
||||||
const element = isValidElement(value)
|
: // For non-React elements: basic primitive->string conversion
|
||||||
? value
|
String(value);
|
||||||
: // For non-React elements: basic primitive->string conversion
|
elements.push(element);
|
||||||
String(value);
|
return ValueFoundMarker;
|
||||||
elements.push(element);
|
}
|
||||||
return ValueFoundMarker;
|
return match; // no match? add warning?
|
||||||
}
|
},
|
||||||
return match; // no match? add warning?
|
);
|
||||||
});
|
|
||||||
|
|
||||||
// No interpolation to be done: just return the text
|
// No interpolation to be done: just return the text
|
||||||
if (elements.length === 0) {
|
if (elements.length === 0) {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
// Basic string interpolation: returns interpolated string
|
// Basic string interpolation: returns interpolated string
|
||||||
if (elements.every((el) => typeof el === 'string')) {
|
if (elements.every((el): el is string => typeof el === 'string')) {
|
||||||
return processedText
|
return processedText
|
||||||
.split(ValueFoundMarker)
|
.split(ValueFoundMarker)
|
||||||
.reduce<string>(
|
.reduce<string>(
|
||||||
(str, value, index) =>
|
(str, value, index) => str.concat(value).concat(elements[index] ?? ''),
|
||||||
str.concat(value).concat((elements[index] as string) ?? ''),
|
|
||||||
'',
|
'',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// JSX interpolation: returns ReactNode
|
// JSX interpolation: returns ReactNode
|
||||||
return processedText.split(ValueFoundMarker).reduce<ReactNode[]>(
|
return processedText.split(ValueFoundMarker).map((value, index) => (
|
||||||
(array, value, index) => [
|
<React.Fragment key={index}>
|
||||||
...array,
|
{value}
|
||||||
<React.Fragment key={index}>
|
{elements[index]}
|
||||||
{value}
|
</React.Fragment>
|
||||||
{elements[index]}
|
));
|
||||||
</React.Fragment>,
|
|
||||||
],
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Interpolate<Str extends string>({
|
export default function Interpolate<Str extends string>({
|
||||||
|
|
|
@ -103,35 +103,29 @@ function Link(
|
||||||
const IOSupported = ExecutionEnvironment.canUseIntersectionObserver;
|
const IOSupported = ExecutionEnvironment.canUseIntersectionObserver;
|
||||||
|
|
||||||
const ioRef = useRef<IntersectionObserver>();
|
const ioRef = useRef<IntersectionObserver>();
|
||||||
const handleIntersection = (el: HTMLAnchorElement, cb: () => void) => {
|
|
||||||
ioRef.current = new window.IntersectionObserver((entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (el === entry.target) {
|
|
||||||
// If element is in viewport, stop observing and run callback.
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
|
|
||||||
if (entry.isIntersecting || entry.intersectionRatio > 0) {
|
|
||||||
ioRef.current!.unobserve(el);
|
|
||||||
ioRef.current!.disconnect();
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add element to the observer.
|
const handleRef = (el: HTMLAnchorElement | null) => {
|
||||||
ioRef.current!.observe(el);
|
innerRef.current = el;
|
||||||
};
|
|
||||||
|
|
||||||
const handleRef = (ref: HTMLAnchorElement | null) => {
|
if (IOSupported && el && isInternal) {
|
||||||
innerRef.current = ref;
|
|
||||||
|
|
||||||
if (IOSupported && ref && isInternal) {
|
|
||||||
// If IO supported and element reference found, set up Observer.
|
// If IO supported and element reference found, set up Observer.
|
||||||
handleIntersection(ref, () => {
|
ioRef.current = new window.IntersectionObserver((entries) => {
|
||||||
if (targetLink != null) {
|
entries.forEach((entry) => {
|
||||||
window.docusaurus.prefetch(targetLink);
|
if (el === entry.target) {
|
||||||
}
|
// If element is in viewport, stop observing and run callback.
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
|
||||||
|
if (entry.isIntersecting || entry.intersectionRatio > 0) {
|
||||||
|
ioRef.current!.unobserve(el);
|
||||||
|
ioRef.current!.disconnect();
|
||||||
|
if (targetLink != null) {
|
||||||
|
window.docusaurus.prefetch(targetLink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
// Add element to the observer.
|
||||||
|
ioRef.current.observe(el);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -161,8 +155,8 @@ function Link(
|
||||||
const isAnchorLink = targetLink?.startsWith('#') ?? false;
|
const isAnchorLink = targetLink?.startsWith('#') ?? false;
|
||||||
const isRegularHtmlLink = !targetLink || !isInternal || isAnchorLink;
|
const isRegularHtmlLink = !targetLink || !isInternal || isAnchorLink;
|
||||||
|
|
||||||
if (targetLink && isInternal && !isAnchorLink && !noBrokenLinkCheck) {
|
if (!isRegularHtmlLink && !noBrokenLinkCheck) {
|
||||||
linksCollector.collectLink(targetLink);
|
linksCollector.collectLink(targetLink!);
|
||||||
}
|
}
|
||||||
|
|
||||||
return isRegularHtmlLink ? (
|
return isRegularHtmlLink ? (
|
||||||
|
|
|
@ -91,7 +91,7 @@ async function createTestSite() {
|
||||||
const siteThemePathPosix = posixPath(siteThemePath);
|
const siteThemePathPosix = posixPath(siteThemePath);
|
||||||
expect(tree(siteThemePathPosix)).toMatchSnapshot('theme dir tree');
|
expect(tree(siteThemePathPosix)).toMatchSnapshot('theme dir tree');
|
||||||
|
|
||||||
const files = Globby.sync(siteThemePathPosix)
|
const files = (await Globby(siteThemePathPosix))
|
||||||
.map((file) => path.posix.relative(siteThemePathPosix, file))
|
.map((file) => path.posix.relative(siteThemePathPosix, file))
|
||||||
.sort();
|
.sort();
|
||||||
|
|
||||||
|
|
|
@ -12,22 +12,10 @@ This could lead to non-deterministic routing behavior."
|
||||||
exports[`loadRoutes loads flat route config 1`] = `
|
exports[`loadRoutes loads flat route config 1`] = `
|
||||||
{
|
{
|
||||||
"registry": {
|
"registry": {
|
||||||
"__comp---theme-blog-list-pagea-6-a-7ba": {
|
"__comp---theme-blog-list-pagea-6-a-7ba": "@theme/BlogListPage",
|
||||||
"loader": "() => import(/* webpackChunkName: '__comp---theme-blog-list-pagea-6-a-7ba' */ '@theme/BlogListPage')",
|
"content---blog-0-b-4-09e": "blog/2018-12-14-Happy-First-Birthday-Slash.md?truncated=true",
|
||||||
"modulePath": "@theme/BlogListPage",
|
"content---blog-7-b-8-fd9": "blog/2018-12-14-Happy-First-Birthday-Slash.md",
|
||||||
},
|
"metadata---blog-0-b-6-74c": "blog-2018-12-14-happy-first-birthday-slash-d2c.json",
|
||||||
"content---blog-0-b-4-09e": {
|
|
||||||
"loader": "() => import(/* webpackChunkName: 'content---blog-0-b-4-09e' */ 'blog/2018-12-14-Happy-First-Birthday-Slash.md?truncated=true')",
|
|
||||||
"modulePath": "blog/2018-12-14-Happy-First-Birthday-Slash.md?truncated=true",
|
|
||||||
},
|
|
||||||
"content---blog-7-b-8-fd9": {
|
|
||||||
"loader": "() => import(/* webpackChunkName: 'content---blog-7-b-8-fd9' */ 'blog/2018-12-14-Happy-First-Birthday-Slash.md')",
|
|
||||||
"modulePath": "blog/2018-12-14-Happy-First-Birthday-Slash.md",
|
|
||||||
},
|
|
||||||
"metadata---blog-0-b-6-74c": {
|
|
||||||
"loader": "() => import(/* webpackChunkName: 'metadata---blog-0-b-6-74c' */ 'blog-2018-12-14-happy-first-birthday-slash-d2c.json')",
|
|
||||||
"modulePath": "blog-2018-12-14-happy-first-birthday-slash-d2c.json",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"routesChunkNames": {
|
"routesChunkNames": {
|
||||||
"/blog-599": {
|
"/blog-599": {
|
||||||
|
@ -71,34 +59,13 @@ export default [
|
||||||
exports[`loadRoutes loads nested route config 1`] = `
|
exports[`loadRoutes loads nested route config 1`] = `
|
||||||
{
|
{
|
||||||
"registry": {
|
"registry": {
|
||||||
"__comp---theme-doc-item-178-a40": {
|
"__comp---theme-doc-item-178-a40": "@theme/DocItem",
|
||||||
"loader": "() => import(/* webpackChunkName: '__comp---theme-doc-item-178-a40' */ '@theme/DocItem')",
|
"__comp---theme-doc-page-1-be-9be": "@theme/DocPage",
|
||||||
"modulePath": "@theme/DocItem",
|
"content---docs-foo-baz-8-ce-61e": "docs/foo/baz.md",
|
||||||
},
|
"content---docs-helloaff-811": "docs/hello.md",
|
||||||
"__comp---theme-doc-page-1-be-9be": {
|
"docsMetadata---docs-routef-34-881": "docs-b5f.json",
|
||||||
"loader": "() => import(/* webpackChunkName: '__comp---theme-doc-page-1-be-9be' */ '@theme/DocPage')",
|
"metadata---docs-foo-baz-2-cf-fa7": "docs-foo-baz-dd9.json",
|
||||||
"modulePath": "@theme/DocPage",
|
"metadata---docs-hello-956-741": "docs-hello-da2.json",
|
||||||
},
|
|
||||||
"content---docs-foo-baz-8-ce-61e": {
|
|
||||||
"loader": "() => import(/* webpackChunkName: 'content---docs-foo-baz-8-ce-61e' */ 'docs/foo/baz.md')",
|
|
||||||
"modulePath": "docs/foo/baz.md",
|
|
||||||
},
|
|
||||||
"content---docs-helloaff-811": {
|
|
||||||
"loader": "() => import(/* webpackChunkName: 'content---docs-helloaff-811' */ 'docs/hello.md')",
|
|
||||||
"modulePath": "docs/hello.md",
|
|
||||||
},
|
|
||||||
"docsMetadata---docs-routef-34-881": {
|
|
||||||
"loader": "() => import(/* webpackChunkName: 'docsMetadata---docs-routef-34-881' */ 'docs-b5f.json')",
|
|
||||||
"modulePath": "docs-b5f.json",
|
|
||||||
},
|
|
||||||
"metadata---docs-foo-baz-2-cf-fa7": {
|
|
||||||
"loader": "() => import(/* webpackChunkName: 'metadata---docs-foo-baz-2-cf-fa7' */ 'docs-foo-baz-dd9.json')",
|
|
||||||
"modulePath": "docs-foo-baz-dd9.json",
|
|
||||||
},
|
|
||||||
"metadata---docs-hello-956-741": {
|
|
||||||
"loader": "() => import(/* webpackChunkName: 'metadata---docs-hello-956-741' */ 'docs-hello-da2.json')",
|
|
||||||
"modulePath": "docs-hello-da2.json",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"routesChunkNames": {
|
"routesChunkNames": {
|
||||||
"/docs/hello-44b": {
|
"/docs/hello-44b": {
|
||||||
|
@ -159,10 +126,7 @@ export default [
|
||||||
exports[`loadRoutes loads route config with empty (but valid) path string 1`] = `
|
exports[`loadRoutes loads route config with empty (but valid) path string 1`] = `
|
||||||
{
|
{
|
||||||
"registry": {
|
"registry": {
|
||||||
"__comp---hello-world-jse-0-f-b6c": {
|
"__comp---hello-world-jse-0-f-b6c": "hello/world.js",
|
||||||
"loader": "() => import(/* webpackChunkName: '__comp---hello-world-jse-0-f-b6c' */ 'hello/world.js')",
|
|
||||||
"modulePath": "hello/world.js",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"routesChunkNames": {
|
"routesChunkNames": {
|
||||||
"-b2a": {
|
"-b2a": {
|
||||||
|
|
|
@ -101,7 +101,7 @@ describe('handleDuplicateRoutes', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('loadRoutes', () => {
|
describe('loadRoutes', () => {
|
||||||
it('loads nested route config', async () => {
|
it('loads nested route config', () => {
|
||||||
const nestedRouteConfig: RouteConfig = {
|
const nestedRouteConfig: RouteConfig = {
|
||||||
component: '@theme/DocPage',
|
component: '@theme/DocPage',
|
||||||
path: '/docs:route',
|
path: '/docs:route',
|
||||||
|
@ -135,12 +135,10 @@ describe('loadRoutes', () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
await expect(
|
expect(loadRoutes([nestedRouteConfig], '/', 'ignore')).toMatchSnapshot();
|
||||||
loadRoutes([nestedRouteConfig], '/', 'ignore'),
|
|
||||||
).resolves.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads flat route config', async () => {
|
it('loads flat route config', () => {
|
||||||
const flatRouteConfig: RouteConfig = {
|
const flatRouteConfig: RouteConfig = {
|
||||||
path: '/blog',
|
path: '/blog',
|
||||||
component: '@theme/BlogListPage',
|
component: '@theme/BlogListPage',
|
||||||
|
@ -169,17 +167,15 @@ describe('loadRoutes', () => {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
await expect(
|
expect(loadRoutes([flatRouteConfig], '/', 'ignore')).toMatchSnapshot();
|
||||||
loadRoutes([flatRouteConfig], '/', 'ignore'),
|
|
||||||
).resolves.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects invalid route config', async () => {
|
it('rejects invalid route config', () => {
|
||||||
const routeConfigWithoutPath = {
|
const routeConfigWithoutPath = {
|
||||||
component: 'hello/world.js',
|
component: 'hello/world.js',
|
||||||
} as RouteConfig;
|
} as RouteConfig;
|
||||||
|
|
||||||
await expect(loadRoutes([routeConfigWithoutPath], '/', 'ignore')).rejects
|
expect(() => loadRoutes([routeConfigWithoutPath], '/', 'ignore'))
|
||||||
.toThrowErrorMatchingInlineSnapshot(`
|
.toThrowErrorMatchingInlineSnapshot(`
|
||||||
"Invalid route config: path must be a string and component is required.
|
"Invalid route config: path must be a string and component is required.
|
||||||
{\\"component\\":\\"hello/world.js\\"}"
|
{\\"component\\":\\"hello/world.js\\"}"
|
||||||
|
@ -189,21 +185,19 @@ describe('loadRoutes', () => {
|
||||||
path: '/hello/world',
|
path: '/hello/world',
|
||||||
} as RouteConfig;
|
} as RouteConfig;
|
||||||
|
|
||||||
await expect(loadRoutes([routeConfigWithoutComponent], '/', 'ignore'))
|
expect(() => loadRoutes([routeConfigWithoutComponent], '/', 'ignore'))
|
||||||
.rejects.toThrowErrorMatchingInlineSnapshot(`
|
.toThrowErrorMatchingInlineSnapshot(`
|
||||||
"Invalid route config: path must be a string and component is required.
|
"Invalid route config: path must be a string and component is required.
|
||||||
{\\"path\\":\\"/hello/world\\"}"
|
{\\"path\\":\\"/hello/world\\"}"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads route config with empty (but valid) path string', async () => {
|
it('loads route config with empty (but valid) path string', () => {
|
||||||
const routeConfig = {
|
const routeConfig = {
|
||||||
path: '',
|
path: '',
|
||||||
component: 'hello/world.js',
|
component: 'hello/world.js',
|
||||||
} as RouteConfig;
|
} as RouteConfig;
|
||||||
|
|
||||||
await expect(
|
expect(loadRoutes([routeConfig], '/', 'ignore')).toMatchSnapshot();
|
||||||
loadRoutes([routeConfig], '/', 'ignore'),
|
|
||||||
).resolves.toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -53,7 +53,7 @@ function getPageBrokenLinks({
|
||||||
// component, but we load route components with string paths.
|
// component, but we load route components with string paths.
|
||||||
// We don't actually access component here, so it's fine.
|
// We don't actually access component here, so it's fine.
|
||||||
.map((l) => matchRoutes(routes, l))
|
.map((l) => matchRoutes(routes, l))
|
||||||
.reduce((prev, cur) => prev.concat(cur));
|
.flat();
|
||||||
return matchedRoutes.length === 0;
|
return matchedRoutes.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,9 +51,8 @@ Note: Docusaurus only support running one locale at a time.`;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const localeConfigs = locales.reduce(
|
const localeConfigs = Object.fromEntries(
|
||||||
(acc, locale) => ({...acc, [locale]: getLocaleConfig(locale)}),
|
locales.map((locale) => [locale, getLocaleConfig(locale)]),
|
||||||
{},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -16,7 +16,6 @@ import {
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {loadSiteConfig} from './config';
|
import {loadSiteConfig} from './config';
|
||||||
import ssrDefaultTemplate from '../webpack/templates/ssr.html.template';
|
|
||||||
import {loadClientModules} from './clientModules';
|
import {loadClientModules} from './clientModules';
|
||||||
import {loadPlugins} from './plugins';
|
import {loadPlugins} from './plugins';
|
||||||
import {loadRoutes} from './routes';
|
import {loadRoutes} from './routes';
|
||||||
|
@ -101,7 +100,6 @@ export async function loadContext(
|
||||||
outDir,
|
outDir,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
i18n,
|
i18n,
|
||||||
ssrTemplate: siteConfig.ssrTemplate ?? ssrDefaultTemplate,
|
|
||||||
codeTranslations,
|
codeTranslations,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -122,18 +120,16 @@ export async function load(options: LoadContextOptions): Promise<Props> {
|
||||||
outDir,
|
outDir,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
i18n,
|
i18n,
|
||||||
ssrTemplate,
|
|
||||||
codeTranslations: siteCodeTranslations,
|
codeTranslations: siteCodeTranslations,
|
||||||
} = context;
|
} = context;
|
||||||
const {plugins, pluginsRouteConfigs, globalData} = await loadPlugins(context);
|
const {plugins, pluginsRouteConfigs, globalData} = await loadPlugins(context);
|
||||||
const clientModules = loadClientModules(plugins);
|
const clientModules = loadClientModules(plugins);
|
||||||
const {headTags, preBodyTags, postBodyTags} = loadHtmlTags(plugins);
|
const {headTags, preBodyTags, postBodyTags} = loadHtmlTags(plugins);
|
||||||
const {registry, routesChunkNames, routesConfig, routesPaths} =
|
const {registry, routesChunkNames, routesConfig, routesPaths} = loadRoutes(
|
||||||
await loadRoutes(
|
pluginsRouteConfigs,
|
||||||
pluginsRouteConfigs,
|
baseUrl,
|
||||||
baseUrl,
|
siteConfig.onDuplicateRoutes,
|
||||||
siteConfig.onDuplicateRoutes,
|
);
|
||||||
);
|
|
||||||
const codeTranslations = {
|
const codeTranslations = {
|
||||||
...(await getPluginsDefaultCodeTranslationMessages(plugins)),
|
...(await getPluginsDefaultCodeTranslationMessages(plugins)),
|
||||||
...siteCodeTranslations,
|
...siteCodeTranslations,
|
||||||
|
@ -153,8 +149,6 @@ next build. You can clear all build artifacts (including this folder) with the
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Site config must be generated after plugins
|
|
||||||
// We want the generated config to have been normalized by the plugins!
|
|
||||||
const genSiteConfig = generate(
|
const genSiteConfig = generate(
|
||||||
generatedFilesDir,
|
generatedFilesDir,
|
||||||
DEFAULT_CONFIG_FILE_NAME,
|
DEFAULT_CONFIG_FILE_NAME,
|
||||||
|
@ -174,7 +168,7 @@ export default ${JSON.stringify(siteConfig, null, 2)};
|
||||||
${clientModules
|
${clientModules
|
||||||
// import() is async so we use require() because client modules can have
|
// import() is async so we use require() because client modules can have
|
||||||
// CSS and the order matters for loading CSS.
|
// CSS and the order matters for loading CSS.
|
||||||
.map((module) => ` require('${escapePath(module)}'),`)
|
.map((clientModule) => ` require('${escapePath(clientModule)}'),`)
|
||||||
.join('\n')}
|
.join('\n')}
|
||||||
];
|
];
|
||||||
`,
|
`,
|
||||||
|
@ -187,10 +181,8 @@ ${clientModules
|
||||||
${Object.entries(registry)
|
${Object.entries(registry)
|
||||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
.map(
|
.map(
|
||||||
([key, chunk]) =>
|
([chunkName, modulePath]) =>
|
||||||
` '${key}': [${chunk.loader}, '${escapePath(
|
` '${chunkName}': [() => import(/* webpackChunkName: '${chunkName}' */ '${modulePath}'), '${modulePath}', require.resolveWeak('${modulePath}')],`,
|
||||||
chunk.modulePath,
|
|
||||||
)}', require.resolveWeak('${escapePath(chunk.modulePath)}')],`,
|
|
||||||
)
|
)
|
||||||
.join('\n')}};
|
.join('\n')}};
|
||||||
`,
|
`,
|
||||||
|
@ -256,7 +248,6 @@ ${Object.entries(registry)
|
||||||
headTags,
|
headTags,
|
||||||
preBodyTags,
|
preBodyTags,
|
||||||
postBodyTags,
|
postBodyTags,
|
||||||
ssrTemplate,
|
|
||||||
codeTranslations,
|
codeTranslations,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,9 +29,12 @@ type LoadedRoutes = {
|
||||||
routesConfig: string;
|
routesConfig: string;
|
||||||
/** @see {ChunkNames} */
|
/** @see {ChunkNames} */
|
||||||
routesChunkNames: RouteChunkNames;
|
routesChunkNames: RouteChunkNames;
|
||||||
/** A map from chunk name to module loaders. */
|
/**
|
||||||
|
* A map from chunk name to module paths. Module paths would have backslash
|
||||||
|
* escaped already, so they can be directly printed.
|
||||||
|
*/
|
||||||
registry: {
|
registry: {
|
||||||
[chunkName: string]: {loader: string; modulePath: string};
|
[chunkName: string]: string;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Collect all page paths for injecting it later in the plugin lifecycle.
|
* Collect all page paths for injecting it later in the plugin lifecycle.
|
||||||
|
@ -195,12 +198,7 @@ function genChunkNames(
|
||||||
// This is a leaf node, no need to recurse
|
// This is a leaf node, no need to recurse
|
||||||
const modulePath = getModulePath(routeModule);
|
const modulePath = getModulePath(routeModule);
|
||||||
const chunkName = genChunkName(modulePath, prefix, name);
|
const chunkName = genChunkName(modulePath, prefix, name);
|
||||||
res.registry[chunkName] = {
|
res.registry[chunkName] = escapePath(modulePath);
|
||||||
loader: `() => import(/* webpackChunkName: '${chunkName}' */ '${escapePath(
|
|
||||||
modulePath,
|
|
||||||
)}')`,
|
|
||||||
modulePath,
|
|
||||||
};
|
|
||||||
return chunkName;
|
return chunkName;
|
||||||
}
|
}
|
||||||
if (Array.isArray(routeModule)) {
|
if (Array.isArray(routeModule)) {
|
||||||
|
@ -294,11 +292,11 @@ ${JSON.stringify(routeConfig)}`,
|
||||||
* chunk names.
|
* chunk names.
|
||||||
* - `registry`, a mapping from chunk names to options for react-loadable.
|
* - `registry`, a mapping from chunk names to options for react-loadable.
|
||||||
*/
|
*/
|
||||||
export async function loadRoutes(
|
export function loadRoutes(
|
||||||
routeConfigs: RouteConfig[],
|
routeConfigs: RouteConfig[],
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
onDuplicateRoutes: ReportingSeverity,
|
onDuplicateRoutes: ReportingSeverity,
|
||||||
): Promise<LoadedRoutes> {
|
): LoadedRoutes {
|
||||||
handleDuplicateRoutes(routeConfigs, onDuplicateRoutes);
|
handleDuplicateRoutes(routeConfigs, onDuplicateRoutes);
|
||||||
const res: LoadedRoutes = {
|
const res: LoadedRoutes = {
|
||||||
// To be written by `genRouteCode`
|
// To be written by `genRouteCode`
|
||||||
|
@ -308,11 +306,16 @@ export async function loadRoutes(
|
||||||
routesPaths: [normalizeUrl([baseUrl, '404.html'])],
|
routesPaths: [normalizeUrl([baseUrl, '404.html'])],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// `genRouteCode` would mutate `res`
|
||||||
|
const routeConfigSerialized = routeConfigs
|
||||||
|
.map((r) => genRouteCode(r, res))
|
||||||
|
.join(',\n');
|
||||||
|
|
||||||
res.routesConfig = `import React from 'react';
|
res.routesConfig = `import React from 'react';
|
||||||
import ComponentCreator from '@docusaurus/ComponentCreator';
|
import ComponentCreator from '@docusaurus/ComponentCreator';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
${indent(`${routeConfigs.map((r) => genRouteCode(r, res)).join(',\n')},`)}
|
${indent(routeConfigSerialized)},
|
||||||
{
|
{
|
||||||
path: '*',
|
path: '*',
|
||||||
component: ComponentCreator('*'),
|
component: ComponentCreator('*'),
|
||||||
|
|
|
@ -81,8 +81,7 @@ function checkDocusaurusPackagesVersion(siteMetadata: SiteMetadata) {
|
||||||
versionInfo.version &&
|
versionInfo.version &&
|
||||||
versionInfo.version !== docusaurusVersion
|
versionInfo.version !== docusaurusVersion
|
||||||
) {
|
) {
|
||||||
// should we throw instead?
|
// Should we throw instead? It still could work with different versions
|
||||||
// It still could work with different versions
|
|
||||||
logger.error`Invalid name=${plugin} version number=${versionInfo.version}.
|
logger.error`Invalid name=${plugin} version number=${versionInfo.version}.
|
||||||
All official @docusaurus/* packages should have the exact same version as @docusaurus/core (number=${docusaurusVersion}).
|
All official @docusaurus/* packages should have the exact same version as @docusaurus/core (number=${docusaurusVersion}).
|
||||||
Maybe you want to check, or regenerate your yarn.lock or package-lock.json file?`;
|
Maybe you want to check, or regenerate your yarn.lock or package-lock.json file?`;
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {createBaseConfig} from './base';
|
||||||
import WaitPlugin from './plugins/WaitPlugin';
|
import WaitPlugin from './plugins/WaitPlugin';
|
||||||
import LogPlugin from './plugins/LogPlugin';
|
import LogPlugin from './plugins/LogPlugin';
|
||||||
import {NODE_MAJOR_VERSION, NODE_MINOR_VERSION} from '@docusaurus/utils';
|
import {NODE_MAJOR_VERSION, NODE_MINOR_VERSION} from '@docusaurus/utils';
|
||||||
|
import ssrDefaultTemplate from './templates/ssr.html.template';
|
||||||
|
|
||||||
// Forked for Docusaurus: https://github.com/slorber/static-site-generator-webpack-plugin
|
// Forked for Docusaurus: https://github.com/slorber/static-site-generator-webpack-plugin
|
||||||
import StaticSiteGeneratorPlugin from '@slorber/static-site-generator-webpack-plugin';
|
import StaticSiteGeneratorPlugin from '@slorber/static-site-generator-webpack-plugin';
|
||||||
|
@ -32,8 +33,7 @@ export default async function createServerConfig({
|
||||||
headTags,
|
headTags,
|
||||||
preBodyTags,
|
preBodyTags,
|
||||||
postBodyTags,
|
postBodyTags,
|
||||||
ssrTemplate,
|
siteConfig: {noIndex, trailingSlash, ssrTemplate},
|
||||||
siteConfig: {noIndex, trailingSlash},
|
|
||||||
} = props;
|
} = props;
|
||||||
const config = await createBaseConfig(props, true);
|
const config = await createBaseConfig(props, true);
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ export default async function createServerConfig({
|
||||||
preBodyTags,
|
preBodyTags,
|
||||||
postBodyTags,
|
postBodyTags,
|
||||||
onLinksCollected,
|
onLinksCollected,
|
||||||
ssrTemplate,
|
ssrTemplate: ssrTemplate ?? ssrDefaultTemplate,
|
||||||
noIndex,
|
noIndex,
|
||||||
},
|
},
|
||||||
paths: ssgPaths,
|
paths: ssgPaths,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue