mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 23:57:22 +02:00
feat(core): simplify plugin API, support route.props (#10042)
This commit is contained in:
parent
d1590e37ac
commit
5c1d6464d8
26 changed files with 2858 additions and 2240 deletions
2
.github/workflows/build-perf.yml
vendored
2
.github/workflows/build-perf.yml
vendored
|
@ -48,7 +48,7 @@ jobs:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
build-script: build:website:en
|
build-script: build:website:en
|
||||||
clean-script: clear:website # see https://github.com/facebook/docusaurus/pull/6838
|
clean-script: clear:website # see https://github.com/facebook/docusaurus/pull/6838
|
||||||
pattern: '{website/build/assets/js/main*js,website/build/assets/css/styles*css,website/.docusaurus/globalData.json,website/build/index.html,website/build/blog/index.html,website/build/blog/**/introducing-docusaurus/*,website/build/docs/index.html,website/build/docs/installation/index.html,website/build/tests/docs/index.html,website/build/tests/docs/standalone/index.html}'
|
pattern: '{website/build/assets/js/main*js,website/build/assets/css/styles*css,website/.docusaurus/globalData.json,website/.docusaurus/registry.js,website/.docusaurus/routes.js,website/.docusaurus/routesChunkNames.json,website/.docusaurus/site-metadata.json,website/.docusaurus/codeTranslations.json,website/.docusaurus/i18n.json,website/.docusaurus/docusaurus.config.mjs,website/build/index.html,website/build/blog/index.html,website/build/blog/**/introducing-docusaurus/*,website/build/docs/index.html,website/build/docs/installation/index.html,website/build/tests/docs/index.html,website/build/tests/docs/standalone/index.html}'
|
||||||
strip-hash: '\.([^;]\w{7})\.'
|
strip-hash: '\.([^;]\w{7})\.'
|
||||||
minimum-change-threshold: 30
|
minimum-change-threshold: 30
|
||||||
compression: none
|
compression: none
|
||||||
|
|
|
@ -56,6 +56,7 @@ exports[`translateContent falls back when translation is incomplete 1`] = `
|
||||||
"source": "/blog/2021/06/19/hello",
|
"source": "/blog/2021/06/19/hello",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"title": "Hello",
|
"title": "Hello",
|
||||||
|
"unlisted": false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -99,6 +100,7 @@ exports[`translateContent returns translated loaded 1`] = `
|
||||||
"source": "/blog/2021/06/19/hello",
|
"source": "/blog/2021/06/19/hello",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"title": "Hello",
|
"title": "Hello",
|
||||||
|
"unlisted": false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -34,6 +34,7 @@ const sampleBlogPosts: BlogPost[] = [
|
||||||
hasTruncateMarker: true,
|
hasTruncateMarker: true,
|
||||||
authors: [],
|
authors: [],
|
||||||
frontMatter: {},
|
frontMatter: {},
|
||||||
|
unlisted: false,
|
||||||
},
|
},
|
||||||
content: '',
|
content: '',
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,7 +11,6 @@ import {
|
||||||
normalizeUrl,
|
normalizeUrl,
|
||||||
docuHash,
|
docuHash,
|
||||||
aliasedSitePath,
|
aliasedSitePath,
|
||||||
aliasedSitePathToRelativePath,
|
|
||||||
getPluginI18nPath,
|
getPluginI18nPath,
|
||||||
posixPath,
|
posixPath,
|
||||||
addTrailingPathSeparator,
|
addTrailingPathSeparator,
|
||||||
|
@ -32,24 +31,17 @@ import footnoteIDFixer from './remark/footnoteIDFixer';
|
||||||
import {translateContent, getTranslationFiles} from './translations';
|
import {translateContent, getTranslationFiles} from './translations';
|
||||||
import {createBlogFeedFiles} from './feed';
|
import {createBlogFeedFiles} from './feed';
|
||||||
|
|
||||||
import {toTagProp, toTagsProp} from './props';
|
import {createAllRoutes} from './routes';
|
||||||
import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';
|
import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';
|
||||||
import type {
|
import type {LoadContext, Plugin, HtmlTags} from '@docusaurus/types';
|
||||||
LoadContext,
|
|
||||||
Plugin,
|
|
||||||
HtmlTags,
|
|
||||||
RouteMetadata,
|
|
||||||
} from '@docusaurus/types';
|
|
||||||
import type {
|
import type {
|
||||||
PluginOptions,
|
PluginOptions,
|
||||||
BlogPostFrontMatter,
|
BlogPostFrontMatter,
|
||||||
BlogPostMetadata,
|
BlogPostMetadata,
|
||||||
Assets,
|
Assets,
|
||||||
BlogTag,
|
|
||||||
BlogTags,
|
BlogTags,
|
||||||
BlogContent,
|
BlogContent,
|
||||||
BlogPaginated,
|
BlogPaginated,
|
||||||
BlogMetadata,
|
|
||||||
} from '@docusaurus/plugin-content-blog';
|
} from '@docusaurus/plugin-content-blog';
|
||||||
|
|
||||||
export default async function pluginContentBlog(
|
export default async function pluginContentBlog(
|
||||||
|
@ -80,6 +72,9 @@ export default async function pluginContentBlog(
|
||||||
'docusaurus-plugin-content-blog',
|
'docusaurus-plugin-content-blog',
|
||||||
);
|
);
|
||||||
const dataDir = path.join(pluginDataDirRoot, pluginId);
|
const dataDir = path.join(pluginDataDirRoot, pluginId);
|
||||||
|
// TODO Docusaurus v4 breaking change
|
||||||
|
// module aliasing should be automatic
|
||||||
|
// we should never find local absolute FS paths in the codegen registry
|
||||||
const aliasedSource = (source: string) =>
|
const aliasedSource = (source: string) =>
|
||||||
`~blog/${posixPath(path.relative(pluginDataDirRoot, source))}`;
|
`~blog/${posixPath(path.relative(pluginDataDirRoot, source))}`;
|
||||||
|
|
||||||
|
@ -185,213 +180,14 @@ export default async function pluginContentBlog(
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
async contentLoaded({content: blogContents, actions}) {
|
async contentLoaded({content, actions}) {
|
||||||
const {
|
await createAllRoutes({
|
||||||
blogListComponent,
|
baseUrl,
|
||||||
blogPostComponent,
|
content,
|
||||||
blogTagsListComponent,
|
actions,
|
||||||
blogTagsPostsComponent,
|
options,
|
||||||
blogArchiveComponent,
|
aliasedSource,
|
||||||
routeBasePath,
|
});
|
||||||
archiveBasePath,
|
|
||||||
blogTitle,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const {addRoute, createData} = actions;
|
|
||||||
const {
|
|
||||||
blogSidebarTitle,
|
|
||||||
blogPosts,
|
|
||||||
blogListPaginated,
|
|
||||||
blogTags,
|
|
||||||
blogTagsListPath,
|
|
||||||
} = blogContents;
|
|
||||||
|
|
||||||
const listedBlogPosts = blogPosts.filter(shouldBeListed);
|
|
||||||
|
|
||||||
const blogItemsToMetadata: {[postId: string]: BlogPostMetadata} = {};
|
|
||||||
|
|
||||||
const sidebarBlogPosts =
|
|
||||||
options.blogSidebarCount === 'ALL'
|
|
||||||
? blogPosts
|
|
||||||
: blogPosts.slice(0, options.blogSidebarCount);
|
|
||||||
|
|
||||||
function blogPostItemsModule(items: string[]) {
|
|
||||||
return items.map((postId) => {
|
|
||||||
const blogPostMetadata = blogItemsToMetadata[postId]!;
|
|
||||||
return {
|
|
||||||
content: {
|
|
||||||
__import: true,
|
|
||||||
path: blogPostMetadata.source,
|
|
||||||
query: {
|
|
||||||
truncated: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (archiveBasePath && listedBlogPosts.length) {
|
|
||||||
const archiveUrl = normalizeUrl([
|
|
||||||
baseUrl,
|
|
||||||
routeBasePath,
|
|
||||||
archiveBasePath,
|
|
||||||
]);
|
|
||||||
// Create a blog archive route
|
|
||||||
const archiveProp = await createData(
|
|
||||||
`${docuHash(archiveUrl)}.json`,
|
|
||||||
JSON.stringify({blogPosts: listedBlogPosts}, null, 2),
|
|
||||||
);
|
|
||||||
addRoute({
|
|
||||||
path: archiveUrl,
|
|
||||||
component: blogArchiveComponent,
|
|
||||||
exact: true,
|
|
||||||
modules: {
|
|
||||||
archive: aliasedSource(archiveProp),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// This prop is useful to provide the blog list sidebar
|
|
||||||
const sidebarProp = await createData(
|
|
||||||
// Note that this created data path must be in sync with
|
|
||||||
// metadataPath provided to mdx-loader.
|
|
||||||
`blog-post-list-prop-${pluginId}.json`,
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
title: blogSidebarTitle,
|
|
||||||
items: sidebarBlogPosts.map((blogPost) => ({
|
|
||||||
title: blogPost.metadata.title,
|
|
||||||
permalink: blogPost.metadata.permalink,
|
|
||||||
unlisted: blogPost.metadata.unlisted,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const blogMetadata: BlogMetadata = {
|
|
||||||
blogBasePath: normalizeUrl([baseUrl, routeBasePath]),
|
|
||||||
blogTitle,
|
|
||||||
};
|
|
||||||
const blogMetadataPath = await createData(
|
|
||||||
`blogMetadata-${pluginId}.json`,
|
|
||||||
JSON.stringify(blogMetadata, null, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
function createBlogPostRouteMetadata(
|
|
||||||
blogPostMeta: BlogPostMetadata,
|
|
||||||
): RouteMetadata {
|
|
||||||
return {
|
|
||||||
sourceFilePath: aliasedSitePathToRelativePath(blogPostMeta.source),
|
|
||||||
lastUpdatedAt: blogPostMeta.lastUpdatedAt,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create routes for blog entries.
|
|
||||||
await Promise.all(
|
|
||||||
blogPosts.map(async (blogPost) => {
|
|
||||||
const {id, metadata} = blogPost;
|
|
||||||
await createData(
|
|
||||||
// Note that this created data path must be in sync with
|
|
||||||
// metadataPath provided to mdx-loader.
|
|
||||||
`${docuHash(metadata.source)}.json`,
|
|
||||||
JSON.stringify(metadata, null, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
addRoute({
|
|
||||||
path: metadata.permalink,
|
|
||||||
component: blogPostComponent,
|
|
||||||
exact: true,
|
|
||||||
modules: {
|
|
||||||
sidebar: aliasedSource(sidebarProp),
|
|
||||||
content: metadata.source,
|
|
||||||
},
|
|
||||||
metadata: createBlogPostRouteMetadata(metadata),
|
|
||||||
context: {
|
|
||||||
blogMetadata: aliasedSource(blogMetadataPath),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
blogItemsToMetadata[id] = metadata;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create routes for blog's paginated list entries.
|
|
||||||
await Promise.all(
|
|
||||||
blogListPaginated.map(async (listPage) => {
|
|
||||||
const {metadata, items} = listPage;
|
|
||||||
const {permalink} = metadata;
|
|
||||||
const pageMetadataPath = await createData(
|
|
||||||
`${docuHash(permalink)}.json`,
|
|
||||||
JSON.stringify(metadata, null, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
addRoute({
|
|
||||||
path: permalink,
|
|
||||||
component: blogListComponent,
|
|
||||||
exact: true,
|
|
||||||
modules: {
|
|
||||||
sidebar: aliasedSource(sidebarProp),
|
|
||||||
items: blogPostItemsModule(items),
|
|
||||||
metadata: aliasedSource(pageMetadataPath),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Tags. This is the last part so we early-return if there are no tags.
|
|
||||||
if (Object.keys(blogTags).length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createTagsListPage() {
|
|
||||||
const tagsPropPath = await createData(
|
|
||||||
`${docuHash(`${blogTagsListPath}-tags`)}.json`,
|
|
||||||
JSON.stringify(toTagsProp({blogTags}), null, 2),
|
|
||||||
);
|
|
||||||
addRoute({
|
|
||||||
path: blogTagsListPath,
|
|
||||||
component: blogTagsListComponent,
|
|
||||||
exact: true,
|
|
||||||
modules: {
|
|
||||||
sidebar: aliasedSource(sidebarProp),
|
|
||||||
tags: aliasedSource(tagsPropPath),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createTagPostsListPage(tag: BlogTag): Promise<void> {
|
|
||||||
await Promise.all(
|
|
||||||
tag.pages.map(async (blogPaginated) => {
|
|
||||||
const {metadata, items} = blogPaginated;
|
|
||||||
const tagPropPath = await createData(
|
|
||||||
`${docuHash(metadata.permalink)}.json`,
|
|
||||||
JSON.stringify(toTagProp({tag, blogTagsListPath}), null, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
const listMetadataPath = await createData(
|
|
||||||
`${docuHash(metadata.permalink)}-list.json`,
|
|
||||||
JSON.stringify(metadata, null, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
addRoute({
|
|
||||||
path: metadata.permalink,
|
|
||||||
component: blogTagsPostsComponent,
|
|
||||||
exact: true,
|
|
||||||
modules: {
|
|
||||||
sidebar: aliasedSource(sidebarProp),
|
|
||||||
items: blogPostItemsModule(items),
|
|
||||||
tag: aliasedSource(tagPropPath),
|
|
||||||
listMetadata: aliasedSource(listMetadataPath),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await createTagsListPage();
|
|
||||||
await Promise.all(Object.values(blogTags).map(createTagPostsListPage));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
translateContent({content, translationFiles}) {
|
translateContent({content, translationFiles}) {
|
||||||
|
|
263
packages/docusaurus-plugin-content-blog/src/routes.ts
Normal file
263
packages/docusaurus-plugin-content-blog/src/routes.ts
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
import {
|
||||||
|
normalizeUrl,
|
||||||
|
docuHash,
|
||||||
|
aliasedSitePathToRelativePath,
|
||||||
|
} from '@docusaurus/utils';
|
||||||
|
import {shouldBeListed} from './blogUtils';
|
||||||
|
|
||||||
|
import {toTagProp, toTagsProp} from './props';
|
||||||
|
import type {
|
||||||
|
PluginContentLoadedActions,
|
||||||
|
RouteConfig,
|
||||||
|
RouteMetadata,
|
||||||
|
} from '@docusaurus/types';
|
||||||
|
import type {
|
||||||
|
BlogPostMetadata,
|
||||||
|
BlogTag,
|
||||||
|
BlogMetadata,
|
||||||
|
BlogContent,
|
||||||
|
PluginOptions,
|
||||||
|
BlogPost,
|
||||||
|
BlogSidebar,
|
||||||
|
} from '@docusaurus/plugin-content-blog';
|
||||||
|
|
||||||
|
type CreateAllRoutesParam = {
|
||||||
|
baseUrl: string;
|
||||||
|
content: BlogContent;
|
||||||
|
options: PluginOptions;
|
||||||
|
actions: PluginContentLoadedActions;
|
||||||
|
aliasedSource: (str: string) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createAllRoutes(
|
||||||
|
param: CreateAllRoutesParam,
|
||||||
|
): Promise<void> {
|
||||||
|
const routes = await buildAllRoutes(param);
|
||||||
|
routes.forEach(param.actions.addRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildAllRoutes({
|
||||||
|
baseUrl,
|
||||||
|
content,
|
||||||
|
actions,
|
||||||
|
options,
|
||||||
|
aliasedSource,
|
||||||
|
}: CreateAllRoutesParam): Promise<RouteConfig[]> {
|
||||||
|
const {
|
||||||
|
blogListComponent,
|
||||||
|
blogPostComponent,
|
||||||
|
blogTagsListComponent,
|
||||||
|
blogTagsPostsComponent,
|
||||||
|
blogArchiveComponent,
|
||||||
|
routeBasePath,
|
||||||
|
archiveBasePath,
|
||||||
|
blogTitle,
|
||||||
|
} = options;
|
||||||
|
const pluginId = options.id!;
|
||||||
|
const {createData} = actions;
|
||||||
|
const {
|
||||||
|
blogSidebarTitle,
|
||||||
|
blogPosts,
|
||||||
|
blogListPaginated,
|
||||||
|
blogTags,
|
||||||
|
blogTagsListPath,
|
||||||
|
} = content;
|
||||||
|
|
||||||
|
const listedBlogPosts = blogPosts.filter(shouldBeListed);
|
||||||
|
|
||||||
|
const blogPostsById = _.keyBy(blogPosts, (post) => post.id);
|
||||||
|
function getBlogPostById(id: string): BlogPost {
|
||||||
|
const blogPost = blogPostsById[id];
|
||||||
|
if (!blogPost) {
|
||||||
|
throw new Error(`unexpected, can't find blog post id=${id}`);
|
||||||
|
}
|
||||||
|
return blogPost;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sidebarBlogPosts =
|
||||||
|
options.blogSidebarCount === 'ALL'
|
||||||
|
? blogPosts
|
||||||
|
: blogPosts.slice(0, options.blogSidebarCount);
|
||||||
|
|
||||||
|
async function createSidebarModule() {
|
||||||
|
const sidebar: BlogSidebar = {
|
||||||
|
title: blogSidebarTitle,
|
||||||
|
items: sidebarBlogPosts.map((blogPost) => ({
|
||||||
|
title: blogPost.metadata.title,
|
||||||
|
permalink: blogPost.metadata.permalink,
|
||||||
|
unlisted: blogPost.metadata.unlisted,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
const modulePath = await createData(
|
||||||
|
`blog-post-list-prop-${pluginId}.json`,
|
||||||
|
sidebar,
|
||||||
|
);
|
||||||
|
return aliasedSource(modulePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createBlogMetadataModule() {
|
||||||
|
const blogMetadata: BlogMetadata = {
|
||||||
|
blogBasePath: normalizeUrl([baseUrl, routeBasePath]),
|
||||||
|
blogTitle,
|
||||||
|
};
|
||||||
|
const modulePath = await createData(
|
||||||
|
`blogMetadata-${pluginId}.json`,
|
||||||
|
blogMetadata,
|
||||||
|
);
|
||||||
|
return aliasedSource(modulePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO we should have a parent blog route,
|
||||||
|
// and inject blog metadata + sidebar as a parent context
|
||||||
|
// unfortunately we can't have a parent route for blog yet
|
||||||
|
// because if both blog/docs are using routeBasePath /,
|
||||||
|
// React router config rendering doesn't support that well
|
||||||
|
const sidebarModulePath = await createSidebarModule();
|
||||||
|
const blogMetadataModulePath = await createBlogMetadataModule();
|
||||||
|
|
||||||
|
function blogPostItemsModule(ids: string[]) {
|
||||||
|
return ids.map((id) => {
|
||||||
|
return {
|
||||||
|
content: {
|
||||||
|
__import: true,
|
||||||
|
path: getBlogPostById(id).metadata.source,
|
||||||
|
query: {
|
||||||
|
truncated: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createArchiveRoute(): RouteConfig[] {
|
||||||
|
if (archiveBasePath && listedBlogPosts.length) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
path: normalizeUrl([baseUrl, routeBasePath, archiveBasePath]),
|
||||||
|
component: blogArchiveComponent,
|
||||||
|
exact: true,
|
||||||
|
props: {
|
||||||
|
archive: {blogPosts: listedBlogPosts},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBlogPostRouteMetadata(
|
||||||
|
blogPostMeta: BlogPostMetadata,
|
||||||
|
): RouteMetadata {
|
||||||
|
return {
|
||||||
|
sourceFilePath: aliasedSitePathToRelativePath(blogPostMeta.source),
|
||||||
|
lastUpdatedAt: blogPostMeta.lastUpdatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
blogPosts.map(async (blogPost) => {
|
||||||
|
const {metadata} = blogPost;
|
||||||
|
await createData(
|
||||||
|
// Note that this created data path must be in sync with
|
||||||
|
// metadataPath provided to mdx-loader.
|
||||||
|
`${docuHash(metadata.source)}.json`,
|
||||||
|
metadata,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
function createBlogPostRoute(blogPost: BlogPost): RouteConfig {
|
||||||
|
return {
|
||||||
|
path: blogPost.metadata.permalink,
|
||||||
|
component: blogPostComponent,
|
||||||
|
exact: true,
|
||||||
|
modules: {
|
||||||
|
sidebar: sidebarModulePath,
|
||||||
|
content: blogPost.metadata.source,
|
||||||
|
},
|
||||||
|
metadata: createBlogPostRouteMetadata(blogPost.metadata),
|
||||||
|
context: {
|
||||||
|
blogMetadata: blogMetadataModulePath,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBlogPostRoutes(): RouteConfig[] {
|
||||||
|
return blogPosts.map(createBlogPostRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBlogPostsPaginatedRoutes(): RouteConfig[] {
|
||||||
|
return blogListPaginated.map((paginated) => {
|
||||||
|
return {
|
||||||
|
path: paginated.metadata.permalink,
|
||||||
|
component: blogListComponent,
|
||||||
|
exact: true,
|
||||||
|
modules: {
|
||||||
|
sidebar: sidebarModulePath,
|
||||||
|
items: blogPostItemsModule(paginated.items),
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
metadata: paginated.metadata,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTagsRoutes(): RouteConfig[] {
|
||||||
|
// Tags. This is the last part so we early-return if there are no tags.
|
||||||
|
if (Object.keys(blogTags).length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagsListRoute: RouteConfig = {
|
||||||
|
path: blogTagsListPath,
|
||||||
|
component: blogTagsListComponent,
|
||||||
|
exact: true,
|
||||||
|
modules: {
|
||||||
|
sidebar: sidebarModulePath,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
tags: toTagsProp({blogTags}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createTagPaginatedRoutes(tag: BlogTag): RouteConfig[] {
|
||||||
|
return tag.pages.map((paginated) => {
|
||||||
|
return {
|
||||||
|
path: paginated.metadata.permalink,
|
||||||
|
component: blogTagsPostsComponent,
|
||||||
|
exact: true,
|
||||||
|
modules: {
|
||||||
|
sidebar: sidebarModulePath,
|
||||||
|
items: blogPostItemsModule(paginated.items),
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
tag: toTagProp({tag, blogTagsListPath}),
|
||||||
|
listMetadata: paginated.metadata,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagsPaginatedRoutes: RouteConfig[] = Object.values(blogTags).flatMap(
|
||||||
|
createTagPaginatedRoutes,
|
||||||
|
);
|
||||||
|
|
||||||
|
return [tagsListRoute, ...tagsPaginatedRoutes];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
...createBlogPostRoutes(),
|
||||||
|
...createBlogPostsPaginatedRoutes(),
|
||||||
|
...createTagsRoutes(),
|
||||||
|
...createArchiveRoute(),
|
||||||
|
];
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -31,7 +31,6 @@ import type {
|
||||||
Options,
|
Options,
|
||||||
PluginOptions,
|
PluginOptions,
|
||||||
PropSidebarItemLink,
|
PropSidebarItemLink,
|
||||||
PropSidebars,
|
|
||||||
} from '@docusaurus/plugin-content-docs';
|
} from '@docusaurus/plugin-content-docs';
|
||||||
import type {
|
import type {
|
||||||
SidebarItemsGeneratorOption,
|
SidebarItemsGeneratorOption,
|
||||||
|
@ -76,36 +75,11 @@ const createFakeActions = (contentDir: string) => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Query by prefix, because files have a hash at the end so it's not
|
|
||||||
// convenient to query by full filename
|
|
||||||
function getCreatedDataByPrefix(prefix: string) {
|
|
||||||
const entry = Object.entries(dataContainer).find(([key]) =>
|
|
||||||
key.startsWith(prefix),
|
|
||||||
);
|
|
||||||
if (!entry) {
|
|
||||||
throw new Error(`No created entry found for prefix "${prefix}".
|
|
||||||
Entries created:
|
|
||||||
- ${Object.keys(dataContainer).join('\n- ')}
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
return JSON.parse(entry[1] as string) as PropSidebars;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extra fns useful for tests!
|
// Extra fns useful for tests!
|
||||||
const utils = {
|
const utils = {
|
||||||
getGlobalData: () => globalDataContainer,
|
getGlobalData: () => globalDataContainer,
|
||||||
getRouteConfigs: () => routeConfigs,
|
getRouteConfigs: () => routeConfigs,
|
||||||
|
|
||||||
checkVersionMetadataPropCreated: (version: LoadedVersion | undefined) => {
|
|
||||||
if (!version) {
|
|
||||||
throw new Error('Version not found');
|
|
||||||
}
|
|
||||||
const versionMetadataProp = getCreatedDataByPrefix(
|
|
||||||
`version-${_.kebabCase(version.versionName)}-metadata-prop`,
|
|
||||||
);
|
|
||||||
expect(versionMetadataProp.docsSidebars).toEqual(toSidebarsProp(version));
|
|
||||||
},
|
|
||||||
|
|
||||||
expectSnapshot: () => {
|
expectSnapshot: () => {
|
||||||
// Sort the route config like in src/server/plugins/index.ts for
|
// Sort the route config like in src/server/plugins/index.ts for
|
||||||
// consistent snapshot ordering
|
// consistent snapshot ordering
|
||||||
|
@ -335,11 +309,8 @@ describe('simple website', () => {
|
||||||
await plugin.contentLoaded!({
|
await plugin.contentLoaded!({
|
||||||
content,
|
content,
|
||||||
actions,
|
actions,
|
||||||
allContent: {},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
utils.checkVersionMetadataPropCreated(currentVersion);
|
|
||||||
|
|
||||||
utils.expectSnapshot();
|
utils.expectSnapshot();
|
||||||
|
|
||||||
expect(utils.getGlobalData()).toMatchSnapshot();
|
expect(utils.getGlobalData()).toMatchSnapshot();
|
||||||
|
@ -464,14 +435,8 @@ describe('versioned website', () => {
|
||||||
await plugin.contentLoaded!({
|
await plugin.contentLoaded!({
|
||||||
content,
|
content,
|
||||||
actions,
|
actions,
|
||||||
allContent: {},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
utils.checkVersionMetadataPropCreated(currentVersion);
|
|
||||||
utils.checkVersionMetadataPropCreated(version101);
|
|
||||||
utils.checkVersionMetadataPropCreated(version100);
|
|
||||||
utils.checkVersionMetadataPropCreated(versionWithSlugs);
|
|
||||||
|
|
||||||
utils.expectSnapshot();
|
utils.expectSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -569,12 +534,8 @@ describe('versioned website (community)', () => {
|
||||||
await plugin.contentLoaded!({
|
await plugin.contentLoaded!({
|
||||||
content,
|
content,
|
||||||
actions,
|
actions,
|
||||||
allContent: {},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
utils.checkVersionMetadataPropCreated(currentVersion);
|
|
||||||
utils.checkVersionMetadataPropCreated(version100);
|
|
||||||
|
|
||||||
utils.expectSnapshot();
|
utils.expectSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -73,6 +73,9 @@ export default async function pluginContentDocs(
|
||||||
'docusaurus-plugin-content-docs',
|
'docusaurus-plugin-content-docs',
|
||||||
);
|
);
|
||||||
const dataDir = path.join(pluginDataDirRoot, pluginId);
|
const dataDir = path.join(pluginDataDirRoot, pluginId);
|
||||||
|
// TODO Docusaurus v4 breaking change
|
||||||
|
// module aliasing should be automatic
|
||||||
|
// we should never find local absolute FS paths in the codegen registry
|
||||||
const aliasedSource = (source: string) =>
|
const aliasedSource = (source: string) =>
|
||||||
`~docs/${posixPath(path.relative(pluginDataDirRoot, source))}`;
|
`~docs/${posixPath(path.relative(pluginDataDirRoot, source))}`;
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ import _ from 'lodash';
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
import {
|
import {
|
||||||
docuHash,
|
docuHash,
|
||||||
createSlugger,
|
|
||||||
normalizeUrl,
|
normalizeUrl,
|
||||||
aliasedSitePathToRelativePath,
|
aliasedSitePathToRelativePath,
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
|
@ -29,7 +28,6 @@ import type {
|
||||||
CategoryGeneratedIndexMetadata,
|
CategoryGeneratedIndexMetadata,
|
||||||
DocMetadata,
|
DocMetadata,
|
||||||
PluginOptions,
|
PluginOptions,
|
||||||
PropTagsListPage,
|
|
||||||
} from '@docusaurus/plugin-content-docs';
|
} from '@docusaurus/plugin-content-docs';
|
||||||
|
|
||||||
function createDocRouteMetadata(docMeta: DocMetadata): RouteMetadata {
|
function createDocRouteMetadata(docMeta: DocMetadata): RouteMetadata {
|
||||||
|
@ -41,36 +39,23 @@ function createDocRouteMetadata(docMeta: DocMetadata): RouteMetadata {
|
||||||
|
|
||||||
async function buildVersionCategoryGeneratedIndexRoutes({
|
async function buildVersionCategoryGeneratedIndexRoutes({
|
||||||
version,
|
version,
|
||||||
actions,
|
|
||||||
options,
|
options,
|
||||||
aliasedSource,
|
|
||||||
}: BuildVersionRoutesParam): Promise<RouteConfig[]> {
|
}: BuildVersionRoutesParam): Promise<RouteConfig[]> {
|
||||||
const slugs = createSlugger();
|
|
||||||
|
|
||||||
async function buildCategoryGeneratedIndexRoute(
|
async function buildCategoryGeneratedIndexRoute(
|
||||||
categoryGeneratedIndex: CategoryGeneratedIndexMetadata,
|
categoryGeneratedIndex: CategoryGeneratedIndexMetadata,
|
||||||
): Promise<RouteConfig> {
|
): Promise<RouteConfig> {
|
||||||
const {sidebar, ...prop} = categoryGeneratedIndex;
|
|
||||||
|
|
||||||
const propFileName = slugs.slug(
|
|
||||||
`${version.path}-${categoryGeneratedIndex.sidebar}-category-${categoryGeneratedIndex.title}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const propData = await actions.createData(
|
|
||||||
`${docuHash(`category/${propFileName}`)}.json`,
|
|
||||||
JSON.stringify(prop, null, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: categoryGeneratedIndex.permalink,
|
path: categoryGeneratedIndex.permalink,
|
||||||
component: options.docCategoryGeneratedIndexComponent,
|
component: options.docCategoryGeneratedIndexComponent,
|
||||||
exact: true,
|
exact: true,
|
||||||
modules: {
|
props: {
|
||||||
categoryGeneratedIndex: aliasedSource(propData),
|
categoryGeneratedIndex,
|
||||||
},
|
},
|
||||||
// Same as doc, this sidebar route attribute permits to associate this
|
// Same as doc, this sidebar route attribute permits to associate this
|
||||||
// subpage to the given sidebar
|
// subpage to the given sidebar
|
||||||
...(sidebar && {sidebar}),
|
...(categoryGeneratedIndex.sidebar && {
|
||||||
|
sidebar: categoryGeneratedIndex.sidebar,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +75,7 @@ async function buildVersionDocRoutes({
|
||||||
// Note that this created data path must be in sync with
|
// Note that this created data path must be in sync with
|
||||||
// metadataPath provided to mdx-loader.
|
// metadataPath provided to mdx-loader.
|
||||||
`${docuHash(doc.source)}.json`,
|
`${docuHash(doc.source)}.json`,
|
||||||
JSON.stringify(doc, null, 2),
|
doc,
|
||||||
);
|
);
|
||||||
|
|
||||||
const docRoute: RouteConfig = {
|
const docRoute: RouteConfig = {
|
||||||
|
@ -131,7 +116,7 @@ async function buildVersionSidebarRoute(param: BuildVersionRoutesParam) {
|
||||||
async function buildVersionTagsRoutes(
|
async function buildVersionTagsRoutes(
|
||||||
param: BuildVersionRoutesParam,
|
param: BuildVersionRoutesParam,
|
||||||
): Promise<RouteConfig[]> {
|
): Promise<RouteConfig[]> {
|
||||||
const {version, options, actions, aliasedSource} = param;
|
const {version, options} = param;
|
||||||
const versionTags = getVersionTags(version.docs);
|
const versionTags = getVersionTags(version.docs);
|
||||||
|
|
||||||
async function buildTagsListRoute(): Promise<RouteConfig | null> {
|
async function buildTagsListRoute(): Promise<RouteConfig | null> {
|
||||||
|
@ -139,37 +124,27 @@ async function buildVersionTagsRoutes(
|
||||||
if (Object.keys(versionTags).length === 0) {
|
if (Object.keys(versionTags).length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const tagsProp: PropTagsListPage['tags'] = toTagsListTagsProp(versionTags);
|
|
||||||
const tagsPropPath = await actions.createData(
|
|
||||||
`${docuHash(`tags-list-${version.versionName}-prop`)}.json`,
|
|
||||||
JSON.stringify(tagsProp, null, 2),
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
path: version.tagsPath,
|
path: version.tagsPath,
|
||||||
exact: true,
|
exact: true,
|
||||||
component: options.docTagsListComponent,
|
component: options.docTagsListComponent,
|
||||||
modules: {
|
props: {
|
||||||
tags: aliasedSource(tagsPropPath),
|
tags: toTagsListTagsProp(versionTags),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildTagDocListRoute(tag: VersionTag): Promise<RouteConfig> {
|
async function buildTagDocListRoute(tag: VersionTag): Promise<RouteConfig> {
|
||||||
const tagProps = toTagDocListProp({
|
|
||||||
allTagsPath: version.tagsPath,
|
|
||||||
tag,
|
|
||||||
docs: version.docs,
|
|
||||||
});
|
|
||||||
const tagPropPath = await actions.createData(
|
|
||||||
`${docuHash(`tag-${tag.permalink}`)}.json`,
|
|
||||||
JSON.stringify(tagProps, null, 2),
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
path: tag.permalink,
|
path: tag.permalink,
|
||||||
component: options.docTagDocListComponent,
|
component: options.docTagDocListComponent,
|
||||||
exact: true,
|
exact: true,
|
||||||
modules: {
|
props: {
|
||||||
tag: aliasedSource(tagPropPath),
|
tag: toTagDocListProp({
|
||||||
|
allTagsPath: version.tagsPath,
|
||||||
|
tag,
|
||||||
|
docs: version.docs,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -189,7 +164,7 @@ type BuildVersionRoutesParam = Omit<BuildAllRoutesParam, 'versions'> & {
|
||||||
async function buildVersionRoutes(
|
async function buildVersionRoutes(
|
||||||
param: BuildVersionRoutesParam,
|
param: BuildVersionRoutesParam,
|
||||||
): Promise<RouteConfig> {
|
): Promise<RouteConfig> {
|
||||||
const {version, actions, options, aliasedSource} = param;
|
const {version, options} = param;
|
||||||
|
|
||||||
async function buildVersionSubRoutes() {
|
async function buildVersionSubRoutes() {
|
||||||
const [sidebarRoute, tagsRoutes] = await Promise.all([
|
const [sidebarRoute, tagsRoutes] = await Promise.all([
|
||||||
|
@ -201,19 +176,15 @@ async function buildVersionRoutes(
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doBuildVersionRoutes(): Promise<RouteConfig> {
|
async function doBuildVersionRoutes(): Promise<RouteConfig> {
|
||||||
const versionProp = toVersionMetadataProp(options.id, version);
|
|
||||||
const versionPropPath = await actions.createData(
|
|
||||||
`${docuHash(`version-${version.versionName}-metadata-prop`)}.json`,
|
|
||||||
JSON.stringify(versionProp, null, 2),
|
|
||||||
);
|
|
||||||
const subRoutes = await buildVersionSubRoutes();
|
|
||||||
return {
|
return {
|
||||||
path: version.path,
|
path: version.path,
|
||||||
exact: false,
|
exact: false,
|
||||||
component: options.docVersionRootComponent,
|
component: options.docVersionRootComponent,
|
||||||
routes: subRoutes,
|
routes: await buildVersionSubRoutes(),
|
||||||
modules: {
|
props: {
|
||||||
version: aliasedSource(versionPropPath),
|
// TODO Docusaurus v4 breaking change?
|
||||||
|
// expose version metadata as route context instead of props
|
||||||
|
version: toVersionMetadataProp(options.id, version),
|
||||||
},
|
},
|
||||||
priority: version.routePriority,
|
priority: version.routePriority,
|
||||||
};
|
};
|
||||||
|
|
194
packages/docusaurus-plugin-content-pages/src/content.ts
Normal file
194
packages/docusaurus-plugin-content-pages/src/content.ts
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import path from 'path';
|
||||||
|
import {
|
||||||
|
encodePath,
|
||||||
|
fileToPath,
|
||||||
|
aliasedSitePath,
|
||||||
|
getFolderContainingFile,
|
||||||
|
Globby,
|
||||||
|
normalizeUrl,
|
||||||
|
parseMarkdownFile,
|
||||||
|
isUnlisted,
|
||||||
|
isDraft,
|
||||||
|
readLastUpdateData,
|
||||||
|
getEditUrl,
|
||||||
|
posixPath,
|
||||||
|
getPluginI18nPath,
|
||||||
|
} from '@docusaurus/utils';
|
||||||
|
import {validatePageFrontMatter} from './frontMatter';
|
||||||
|
import type {LoadContext} from '@docusaurus/types';
|
||||||
|
import type {PagesContentPaths} from './types';
|
||||||
|
import type {
|
||||||
|
PluginOptions,
|
||||||
|
Metadata,
|
||||||
|
LoadedContent,
|
||||||
|
} from '@docusaurus/plugin-content-pages';
|
||||||
|
|
||||||
|
export function createPagesContentPaths({
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
context: LoadContext;
|
||||||
|
options: PluginOptions;
|
||||||
|
}): PagesContentPaths {
|
||||||
|
const {siteDir, localizationDir} = context;
|
||||||
|
return {
|
||||||
|
contentPath: path.resolve(siteDir, options.path),
|
||||||
|
contentPathLocalized: getPluginI18nPath({
|
||||||
|
localizationDir,
|
||||||
|
pluginName: 'docusaurus-plugin-content-pages',
|
||||||
|
pluginId: options.id,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getContentPathList(contentPaths: PagesContentPaths): string[] {
|
||||||
|
return [contentPaths.contentPathLocalized, contentPaths.contentPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMarkdownSource = (source: string) =>
|
||||||
|
source.endsWith('.md') || source.endsWith('.mdx');
|
||||||
|
|
||||||
|
type LoadContentParams = {
|
||||||
|
context: LoadContext;
|
||||||
|
options: PluginOptions;
|
||||||
|
contentPaths: PagesContentPaths;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function loadPagesContent(
|
||||||
|
params: LoadContentParams,
|
||||||
|
): Promise<LoadedContent> {
|
||||||
|
const {options} = params;
|
||||||
|
|
||||||
|
const pagesFiles = await Globby(params.options.include, {
|
||||||
|
cwd: params.contentPaths.contentPath,
|
||||||
|
ignore: options.exclude,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function doProcessPageSourceFile(relativeSource: string) {
|
||||||
|
try {
|
||||||
|
return await processPageSourceFile(relativeSource, params);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(
|
||||||
|
`Processing of page source file path=${relativeSource} failed.`,
|
||||||
|
{cause: err as Error},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await Promise.all(pagesFiles.map(doProcessPageSourceFile))).filter(
|
||||||
|
(res): res is Metadata => {
|
||||||
|
return res !== undefined;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processPageSourceFile(
|
||||||
|
relativeSource: string,
|
||||||
|
params: LoadContentParams,
|
||||||
|
): Promise<Metadata | undefined> {
|
||||||
|
const {context, options, contentPaths} = params;
|
||||||
|
const {siteConfig, baseUrl, siteDir, i18n} = context;
|
||||||
|
const {editUrl} = options;
|
||||||
|
|
||||||
|
// Lookup in localized folder in priority
|
||||||
|
const contentPath = await getFolderContainingFile(
|
||||||
|
getContentPathList(contentPaths),
|
||||||
|
relativeSource,
|
||||||
|
);
|
||||||
|
|
||||||
|
const source = path.join(contentPath, relativeSource);
|
||||||
|
const aliasedSourcePath = aliasedSitePath(source, siteDir);
|
||||||
|
const permalink = normalizeUrl([
|
||||||
|
baseUrl,
|
||||||
|
options.routeBasePath,
|
||||||
|
encodePath(fileToPath(relativeSource)),
|
||||||
|
]);
|
||||||
|
if (!isMarkdownSource(relativeSource)) {
|
||||||
|
return {
|
||||||
|
type: 'jsx',
|
||||||
|
permalink,
|
||||||
|
source: aliasedSourcePath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await fs.readFile(source, 'utf-8');
|
||||||
|
const {
|
||||||
|
frontMatter: unsafeFrontMatter,
|
||||||
|
contentTitle,
|
||||||
|
excerpt,
|
||||||
|
} = await parseMarkdownFile({
|
||||||
|
filePath: source,
|
||||||
|
fileContent: content,
|
||||||
|
parseFrontMatter: siteConfig.markdown.parseFrontMatter,
|
||||||
|
});
|
||||||
|
const frontMatter = validatePageFrontMatter(unsafeFrontMatter);
|
||||||
|
|
||||||
|
const pagesDirPath = await getFolderContainingFile(
|
||||||
|
getContentPathList(contentPaths),
|
||||||
|
relativeSource,
|
||||||
|
);
|
||||||
|
|
||||||
|
const pagesSourceAbsolute = path.join(pagesDirPath, relativeSource);
|
||||||
|
|
||||||
|
function getPagesEditUrl() {
|
||||||
|
const pagesPathRelative = path.relative(
|
||||||
|
pagesDirPath,
|
||||||
|
path.resolve(pagesSourceAbsolute),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof editUrl === 'function') {
|
||||||
|
return editUrl({
|
||||||
|
pagesDirPath: posixPath(path.relative(siteDir, pagesDirPath)),
|
||||||
|
pagesPath: posixPath(pagesPathRelative),
|
||||||
|
permalink,
|
||||||
|
locale: i18n.currentLocale,
|
||||||
|
});
|
||||||
|
} else if (typeof editUrl === 'string') {
|
||||||
|
const isLocalized = pagesDirPath === contentPaths.contentPathLocalized;
|
||||||
|
const fileContentPath =
|
||||||
|
isLocalized && options.editLocalizedFiles
|
||||||
|
? contentPaths.contentPathLocalized
|
||||||
|
: contentPaths.contentPath;
|
||||||
|
|
||||||
|
const contentPathEditUrl = normalizeUrl([
|
||||||
|
editUrl,
|
||||||
|
posixPath(path.relative(siteDir, fileContentPath)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return getEditUrl(pagesPathRelative, contentPathEditUrl);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastUpdatedData = await readLastUpdateData(
|
||||||
|
source,
|
||||||
|
options,
|
||||||
|
frontMatter.last_update,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDraft({frontMatter})) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const unlisted = isUnlisted({frontMatter});
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'mdx',
|
||||||
|
permalink,
|
||||||
|
source: aliasedSourcePath,
|
||||||
|
title: frontMatter.title ?? contentTitle,
|
||||||
|
description: frontMatter.description ?? excerpt,
|
||||||
|
frontMatter,
|
||||||
|
lastUpdatedBy: lastUpdatedData.lastUpdatedBy,
|
||||||
|
lastUpdatedAt: lastUpdatedData.lastUpdatedAt,
|
||||||
|
editUrl: getPagesEditUrl(),
|
||||||
|
unlisted,
|
||||||
|
};
|
||||||
|
}
|
|
@ -8,57 +8,32 @@
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {
|
import {
|
||||||
encodePath,
|
|
||||||
fileToPath,
|
|
||||||
aliasedSitePath,
|
aliasedSitePath,
|
||||||
aliasedSitePathToRelativePath,
|
|
||||||
docuHash,
|
docuHash,
|
||||||
getPluginI18nPath,
|
|
||||||
getFolderContainingFile,
|
|
||||||
addTrailingPathSeparator,
|
addTrailingPathSeparator,
|
||||||
Globby,
|
|
||||||
createAbsoluteFilePathMatcher,
|
createAbsoluteFilePathMatcher,
|
||||||
normalizeUrl,
|
|
||||||
DEFAULT_PLUGIN_ID,
|
DEFAULT_PLUGIN_ID,
|
||||||
parseMarkdownFile,
|
|
||||||
isUnlisted,
|
|
||||||
isDraft,
|
|
||||||
readLastUpdateData,
|
|
||||||
getEditUrl,
|
|
||||||
posixPath,
|
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
import {validatePageFrontMatter} from './frontMatter';
|
import {createAllRoutes} from './routes';
|
||||||
import type {LoadContext, Plugin, RouteMetadata} from '@docusaurus/types';
|
import {
|
||||||
import type {PagesContentPaths} from './types';
|
createPagesContentPaths,
|
||||||
|
getContentPathList,
|
||||||
|
loadPagesContent,
|
||||||
|
} from './content';
|
||||||
|
import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||||
import type {
|
import type {
|
||||||
PluginOptions,
|
PluginOptions,
|
||||||
Metadata,
|
|
||||||
LoadedContent,
|
LoadedContent,
|
||||||
PageFrontMatter,
|
PageFrontMatter,
|
||||||
} from '@docusaurus/plugin-content-pages';
|
} from '@docusaurus/plugin-content-pages';
|
||||||
|
|
||||||
export function getContentPathList(contentPaths: PagesContentPaths): string[] {
|
|
||||||
return [contentPaths.contentPathLocalized, contentPaths.contentPath];
|
|
||||||
}
|
|
||||||
|
|
||||||
const isMarkdownSource = (source: string) =>
|
|
||||||
source.endsWith('.md') || source.endsWith('.mdx');
|
|
||||||
|
|
||||||
export default function pluginContentPages(
|
export default function pluginContentPages(
|
||||||
context: LoadContext,
|
context: LoadContext,
|
||||||
options: PluginOptions,
|
options: PluginOptions,
|
||||||
): Plugin<LoadedContent | null> {
|
): Plugin<LoadedContent | null> {
|
||||||
const {siteConfig, siteDir, generatedFilesDir, localizationDir, i18n} =
|
const {siteConfig, siteDir, generatedFilesDir} = context;
|
||||||
context;
|
|
||||||
|
|
||||||
const contentPaths: PagesContentPaths = {
|
const contentPaths = createPagesContentPaths({context, options});
|
||||||
contentPath: path.resolve(siteDir, options.path),
|
|
||||||
contentPathLocalized: getPluginI18nPath({
|
|
||||||
localizationDir,
|
|
||||||
pluginName: 'docusaurus-plugin-content-pages',
|
|
||||||
pluginId: options.id,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const pluginDataDirRoot = path.join(
|
const pluginDataDirRoot = path.join(
|
||||||
generatedFilesDir,
|
generatedFilesDir,
|
||||||
|
@ -77,182 +52,17 @@ export default function pluginContentPages(
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadContent() {
|
async loadContent() {
|
||||||
const {include, editUrl} = options;
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(contentPaths.contentPath))) {
|
if (!(await fs.pathExists(contentPaths.contentPath))) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
return loadPagesContent({context, options, contentPaths});
|
||||||
const {baseUrl} = siteConfig;
|
|
||||||
const pagesFiles = await Globby(include, {
|
|
||||||
cwd: contentPaths.contentPath,
|
|
||||||
ignore: options.exclude,
|
|
||||||
});
|
|
||||||
|
|
||||||
async function processPageSourceFile(
|
|
||||||
relativeSource: string,
|
|
||||||
): Promise<Metadata | undefined> {
|
|
||||||
// Lookup in localized folder in priority
|
|
||||||
const contentPath = await getFolderContainingFile(
|
|
||||||
getContentPathList(contentPaths),
|
|
||||||
relativeSource,
|
|
||||||
);
|
|
||||||
|
|
||||||
const source = path.join(contentPath, relativeSource);
|
|
||||||
const aliasedSourcePath = aliasedSitePath(source, siteDir);
|
|
||||||
const permalink = normalizeUrl([
|
|
||||||
baseUrl,
|
|
||||||
options.routeBasePath,
|
|
||||||
encodePath(fileToPath(relativeSource)),
|
|
||||||
]);
|
|
||||||
if (!isMarkdownSource(relativeSource)) {
|
|
||||||
return {
|
|
||||||
type: 'jsx',
|
|
||||||
permalink,
|
|
||||||
source: aliasedSourcePath,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const content = await fs.readFile(source, 'utf-8');
|
|
||||||
const {
|
|
||||||
frontMatter: unsafeFrontMatter,
|
|
||||||
contentTitle,
|
|
||||||
excerpt,
|
|
||||||
} = await parseMarkdownFile({
|
|
||||||
filePath: source,
|
|
||||||
fileContent: content,
|
|
||||||
parseFrontMatter: siteConfig.markdown.parseFrontMatter,
|
|
||||||
});
|
|
||||||
const frontMatter = validatePageFrontMatter(unsafeFrontMatter);
|
|
||||||
|
|
||||||
const pagesDirPath = await getFolderContainingFile(
|
|
||||||
getContentPathList(contentPaths),
|
|
||||||
relativeSource,
|
|
||||||
);
|
|
||||||
|
|
||||||
const pagesSourceAbsolute = path.join(pagesDirPath, relativeSource);
|
|
||||||
|
|
||||||
function getPagesEditUrl() {
|
|
||||||
const pagesPathRelative = path.relative(
|
|
||||||
pagesDirPath,
|
|
||||||
path.resolve(pagesSourceAbsolute),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (typeof editUrl === 'function') {
|
|
||||||
return editUrl({
|
|
||||||
pagesDirPath: posixPath(path.relative(siteDir, pagesDirPath)),
|
|
||||||
pagesPath: posixPath(pagesPathRelative),
|
|
||||||
permalink,
|
|
||||||
locale: i18n.currentLocale,
|
|
||||||
});
|
|
||||||
} else if (typeof editUrl === 'string') {
|
|
||||||
const isLocalized =
|
|
||||||
pagesDirPath === contentPaths.contentPathLocalized;
|
|
||||||
const fileContentPath =
|
|
||||||
isLocalized && options.editLocalizedFiles
|
|
||||||
? contentPaths.contentPathLocalized
|
|
||||||
: contentPaths.contentPath;
|
|
||||||
|
|
||||||
const contentPathEditUrl = normalizeUrl([
|
|
||||||
editUrl,
|
|
||||||
posixPath(path.relative(siteDir, fileContentPath)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return getEditUrl(pagesPathRelative, contentPathEditUrl);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastUpdatedData = await readLastUpdateData(
|
|
||||||
source,
|
|
||||||
options,
|
|
||||||
frontMatter.last_update,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isDraft({frontMatter})) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const unlisted = isUnlisted({frontMatter});
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'mdx',
|
|
||||||
permalink,
|
|
||||||
source: aliasedSourcePath,
|
|
||||||
title: frontMatter.title ?? contentTitle,
|
|
||||||
description: frontMatter.description ?? excerpt,
|
|
||||||
frontMatter,
|
|
||||||
lastUpdatedBy: lastUpdatedData.lastUpdatedBy,
|
|
||||||
lastUpdatedAt: lastUpdatedData.lastUpdatedAt,
|
|
||||||
editUrl: getPagesEditUrl(),
|
|
||||||
unlisted,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doProcessPageSourceFile(relativeSource: string) {
|
|
||||||
try {
|
|
||||||
return await processPageSourceFile(relativeSource);
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(
|
|
||||||
`Processing of page source file path=${relativeSource} failed.`,
|
|
||||||
{cause: err as Error},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
await Promise.all(pagesFiles.map(doProcessPageSourceFile))
|
|
||||||
).filter(Boolean) as Metadata[];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async contentLoaded({content, actions}) {
|
async contentLoaded({content, actions}) {
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
await createAllRoutes({content, options, actions});
|
||||||
const {addRoute, createData} = actions;
|
|
||||||
|
|
||||||
function createPageRouteMetadata(metadata: Metadata): RouteMetadata {
|
|
||||||
const lastUpdatedAt =
|
|
||||||
metadata.type === 'mdx' ? metadata.lastUpdatedAt : undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
sourceFilePath: aliasedSitePathToRelativePath(metadata.source),
|
|
||||||
lastUpdatedAt,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
content.map(async (metadata) => {
|
|
||||||
const {permalink, source} = metadata;
|
|
||||||
const routeMetadata = createPageRouteMetadata(metadata);
|
|
||||||
if (metadata.type === 'mdx') {
|
|
||||||
await createData(
|
|
||||||
// Note that this created data path must be in sync with
|
|
||||||
// metadataPath provided to mdx-loader.
|
|
||||||
`${docuHash(metadata.source)}.json`,
|
|
||||||
JSON.stringify(metadata, null, 2),
|
|
||||||
);
|
|
||||||
addRoute({
|
|
||||||
path: permalink,
|
|
||||||
component: options.mdxPageComponent,
|
|
||||||
exact: true,
|
|
||||||
metadata: routeMetadata,
|
|
||||||
modules: {
|
|
||||||
content: source,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
addRoute({
|
|
||||||
path: permalink,
|
|
||||||
component: source,
|
|
||||||
exact: true,
|
|
||||||
metadata: routeMetadata,
|
|
||||||
modules: {
|
|
||||||
config: `@generated/docusaurus.config`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
configureWebpack() {
|
configureWebpack() {
|
||||||
|
@ -265,11 +75,6 @@ export default function pluginContentPages(
|
||||||
} = options;
|
} = options;
|
||||||
const contentDirs = getContentPathList(contentPaths);
|
const contentDirs = getContentPathList(contentPaths);
|
||||||
return {
|
return {
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'~pages': pluginDataDirRoot,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
|
|
89
packages/docusaurus-plugin-content-pages/src/routes.ts
Normal file
89
packages/docusaurus-plugin-content-pages/src/routes.ts
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {aliasedSitePathToRelativePath, docuHash} from '@docusaurus/utils';
|
||||||
|
import type {
|
||||||
|
PluginContentLoadedActions,
|
||||||
|
RouteConfig,
|
||||||
|
RouteMetadata,
|
||||||
|
} from '@docusaurus/types';
|
||||||
|
import type {
|
||||||
|
PluginOptions,
|
||||||
|
Metadata,
|
||||||
|
LoadedContent,
|
||||||
|
MDXPageMetadata,
|
||||||
|
} from '@docusaurus/plugin-content-pages';
|
||||||
|
|
||||||
|
type CreateAllRoutesParam = {
|
||||||
|
content: LoadedContent;
|
||||||
|
options: PluginOptions;
|
||||||
|
actions: PluginContentLoadedActions;
|
||||||
|
};
|
||||||
|
|
||||||
|
function createPageRouteMetadata(metadata: Metadata): RouteMetadata {
|
||||||
|
const lastUpdatedAt =
|
||||||
|
metadata.type === 'mdx' ? metadata.lastUpdatedAt : undefined;
|
||||||
|
return {
|
||||||
|
sourceFilePath: aliasedSitePathToRelativePath(metadata.source),
|
||||||
|
lastUpdatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAllRoutes(
|
||||||
|
param: CreateAllRoutesParam,
|
||||||
|
): Promise<void> {
|
||||||
|
const routes = await buildAllRoutes(param);
|
||||||
|
routes.forEach(param.actions.addRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildAllRoutes({
|
||||||
|
content,
|
||||||
|
actions,
|
||||||
|
options,
|
||||||
|
}: CreateAllRoutesParam): Promise<RouteConfig[]> {
|
||||||
|
const {createData} = actions;
|
||||||
|
|
||||||
|
async function buildMDXPageRoute(
|
||||||
|
metadata: MDXPageMetadata,
|
||||||
|
): Promise<RouteConfig> {
|
||||||
|
await createData(
|
||||||
|
// Note that this created data path must be in sync with
|
||||||
|
// metadataPath provided to mdx-loader.
|
||||||
|
`${docuHash(metadata.source)}.json`,
|
||||||
|
metadata,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
path: metadata.permalink,
|
||||||
|
component: options.mdxPageComponent,
|
||||||
|
exact: true,
|
||||||
|
metadata: createPageRouteMetadata(metadata),
|
||||||
|
modules: {
|
||||||
|
content: metadata.source,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildJSXRoute(metadata: Metadata): Promise<RouteConfig> {
|
||||||
|
return {
|
||||||
|
path: metadata.permalink,
|
||||||
|
component: metadata.source,
|
||||||
|
exact: true,
|
||||||
|
metadata: createPageRouteMetadata(metadata),
|
||||||
|
modules: {
|
||||||
|
config: `@generated/docusaurus.config`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildPageRoute(metadata: Metadata): Promise<RouteConfig> {
|
||||||
|
return metadata.type === 'mdx'
|
||||||
|
? buildMDXPageRoute(metadata)
|
||||||
|
: buildJSXRoute(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(content.map(buildPageRoute));
|
||||||
|
}
|
|
@ -5,21 +5,12 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import {normalizeUrl} from '@docusaurus/utils';
|
||||||
import {docuHash, normalizeUrl, posixPath} from '@docusaurus/utils';
|
|
||||||
import type {LoadContext, Plugin} from '@docusaurus/types';
|
import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||||
|
|
||||||
export default function pluginDebug({
|
export default function pluginDebug({
|
||||||
siteConfig: {baseUrl},
|
siteConfig: {baseUrl},
|
||||||
generatedFilesDir,
|
|
||||||
}: LoadContext): Plugin<undefined> {
|
}: LoadContext): Plugin<undefined> {
|
||||||
const pluginDataDirRoot = path.join(
|
|
||||||
generatedFilesDir,
|
|
||||||
'docusaurus-plugin-debug',
|
|
||||||
);
|
|
||||||
const aliasedSource = (source: string) =>
|
|
||||||
`~debug/${posixPath(path.relative(pluginDataDirRoot, source))}`;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'docusaurus-plugin-debug',
|
name: 'docusaurus-plugin-debug',
|
||||||
|
|
||||||
|
@ -30,14 +21,7 @@ export default function pluginDebug({
|
||||||
return '../src/theme';
|
return '../src/theme';
|
||||||
},
|
},
|
||||||
|
|
||||||
async allContentLoaded({actions: {createData, addRoute}, allContent}) {
|
async allContentLoaded({actions: {addRoute}, allContent}) {
|
||||||
const allContentPath = await createData(
|
|
||||||
// Note that this created data path must be in sync with
|
|
||||||
// metadataPath provided to mdx-loader.
|
|
||||||
`${docuHash('docusaurus-debug-allContent')}.json`,
|
|
||||||
JSON.stringify(allContent, null, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Home is config (duplicate for now)
|
// Home is config (duplicate for now)
|
||||||
addRoute({
|
addRoute({
|
||||||
path: normalizeUrl([baseUrl, '__docusaurus/debug']),
|
path: normalizeUrl([baseUrl, '__docusaurus/debug']),
|
||||||
|
@ -73,8 +57,8 @@ export default function pluginDebug({
|
||||||
path: normalizeUrl([baseUrl, '__docusaurus/debug/content']),
|
path: normalizeUrl([baseUrl, '__docusaurus/debug/content']),
|
||||||
component: '@theme/DebugContent',
|
component: '@theme/DebugContent',
|
||||||
exact: true,
|
exact: true,
|
||||||
modules: {
|
props: {
|
||||||
allContent: aliasedSource(allContentPath),
|
allContent,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -84,15 +68,5 @@ export default function pluginDebug({
|
||||||
exact: true,
|
exact: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
configureWebpack() {
|
|
||||||
return {
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'~debug': pluginDataDirRoot,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
4
packages/docusaurus-types/src/context.d.ts
vendored
4
packages/docusaurus-types/src/context.d.ts
vendored
|
@ -7,7 +7,7 @@
|
||||||
import type {DocusaurusConfig} from './config';
|
import type {DocusaurusConfig} from './config';
|
||||||
import type {CodeTranslations, I18n} from './i18n';
|
import type {CodeTranslations, I18n} from './i18n';
|
||||||
import type {LoadedPlugin, PluginVersionInformation} from './plugin';
|
import type {LoadedPlugin, PluginVersionInformation} from './plugin';
|
||||||
import type {RouteConfig} from './routing';
|
import type {PluginRouteConfig} from './routing';
|
||||||
|
|
||||||
export type DocusaurusContext = {
|
export type DocusaurusContext = {
|
||||||
siteConfig: DocusaurusConfig;
|
siteConfig: DocusaurusConfig;
|
||||||
|
@ -57,7 +57,7 @@ export type Props = LoadContext & {
|
||||||
preBodyTags: string;
|
preBodyTags: string;
|
||||||
postBodyTags: string;
|
postBodyTags: string;
|
||||||
siteMetadata: SiteMetadata;
|
siteMetadata: SiteMetadata;
|
||||||
routes: RouteConfig[];
|
routes: PluginRouteConfig[];
|
||||||
routesPaths: string[];
|
routesPaths: string[];
|
||||||
plugins: LoadedPlugin[];
|
plugins: LoadedPlugin[];
|
||||||
};
|
};
|
||||||
|
|
1
packages/docusaurus-types/src/index.d.ts
vendored
1
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -70,6 +70,7 @@ export {
|
||||||
|
|
||||||
export {
|
export {
|
||||||
RouteConfig,
|
RouteConfig,
|
||||||
|
PluginRouteConfig,
|
||||||
RouteMetadata,
|
RouteMetadata,
|
||||||
RouteContext,
|
RouteContext,
|
||||||
PluginRouteContext,
|
PluginRouteContext,
|
||||||
|
|
2
packages/docusaurus-types/src/plugin.d.ts
vendored
2
packages/docusaurus-types/src/plugin.d.ts
vendored
|
@ -49,7 +49,7 @@ export type PluginVersionInformation =
|
||||||
|
|
||||||
export type PluginContentLoadedActions = {
|
export type PluginContentLoadedActions = {
|
||||||
addRoute: (config: RouteConfig) => void;
|
addRoute: (config: RouteConfig) => void;
|
||||||
createData: (name: string, data: string) => Promise<string>;
|
createData: (name: string, data: string | object) => Promise<string>;
|
||||||
setGlobalData: (data: unknown) => void;
|
setGlobalData: (data: unknown) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
19
packages/docusaurus-types/src/routing.d.ts
vendored
19
packages/docusaurus-types/src/routing.d.ts
vendored
|
@ -6,6 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {ParsedUrlQueryInput} from 'querystring';
|
import type {ParsedUrlQueryInput} from 'querystring';
|
||||||
|
import type {PluginIdentifier} from './plugin';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A "module" represents a unit of serialized data emitted from the plugin. It
|
* A "module" represents a unit of serialized data emitted from the plugin. It
|
||||||
|
@ -110,9 +111,23 @@ export type RouteConfig = {
|
||||||
*/
|
*/
|
||||||
metadata?: RouteMetadata;
|
metadata?: RouteMetadata;
|
||||||
/**
|
/**
|
||||||
* Extra props; will be available on the client side.
|
* Optional props object; will be converted to a module and injected as props
|
||||||
|
* into the route component.
|
||||||
*/
|
*/
|
||||||
[propName: string]: unknown;
|
props?: {[propName: string]: unknown};
|
||||||
|
/**
|
||||||
|
* Extra route attribute; will be available on the client side route object.
|
||||||
|
*/
|
||||||
|
[attributeName: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PluginRouteConfig = RouteConfig & {
|
||||||
|
/**
|
||||||
|
* Routes are always created by Docusaurus plugins
|
||||||
|
* A plugin identifier is available at the top of a routing tree
|
||||||
|
* (child routes are implicitly created by the same plugin as their parent)
|
||||||
|
*/
|
||||||
|
plugin: PluginIdentifier;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RouteContext = {
|
export type RouteContext = {
|
||||||
|
|
|
@ -85,7 +85,8 @@ export default function ComponentCreator(
|
||||||
const loadedModules = JSON.parse(JSON.stringify(chunkNames)) as {
|
const loadedModules = JSON.parse(JSON.stringify(chunkNames)) as {
|
||||||
__comp?: React.ComponentType<object>;
|
__comp?: React.ComponentType<object>;
|
||||||
__context?: RouteContext;
|
__context?: RouteContext;
|
||||||
[propName: string]: unknown;
|
__props?: {[propName: string]: unknown};
|
||||||
|
[attributeName: string]: unknown;
|
||||||
};
|
};
|
||||||
Object.entries(loaded).forEach(([keyPath, loadedModule]) => {
|
Object.entries(loaded).forEach(([keyPath, loadedModule]) => {
|
||||||
// JSON modules are also loaded as `{ default: ... }` (`import()`
|
// JSON modules are also loaded as `{ default: ... }` (`import()`
|
||||||
|
@ -127,12 +128,18 @@ export default function ComponentCreator(
|
||||||
delete loadedModules.__comp;
|
delete loadedModules.__comp;
|
||||||
const routeContext = loadedModules.__context!;
|
const routeContext = loadedModules.__context!;
|
||||||
delete loadedModules.__context;
|
delete loadedModules.__context;
|
||||||
|
const routeProps = loadedModules.__props;
|
||||||
|
delete loadedModules.__props;
|
||||||
/* eslint-enable no-underscore-dangle */
|
/* eslint-enable no-underscore-dangle */
|
||||||
|
|
||||||
// Is there any way to put this RouteContextProvider upper in the tree?
|
// Is there any way to put this RouteContextProvider upper in the tree?
|
||||||
return (
|
return (
|
||||||
<RouteContextProvider value={routeContext}>
|
<RouteContextProvider value={routeContext}>
|
||||||
<Component {...loadedModules} {...(props as object)} />
|
<Component
|
||||||
|
{...loadedModules}
|
||||||
|
{...routeProps}
|
||||||
|
{...(props as object)}
|
||||||
|
/>
|
||||||
</RouteContextProvider>
|
</RouteContextProvider>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,7 +16,7 @@ import type {
|
||||||
DocusaurusConfig,
|
DocusaurusConfig,
|
||||||
GlobalData,
|
GlobalData,
|
||||||
I18n,
|
I18n,
|
||||||
RouteConfig,
|
PluginRouteConfig,
|
||||||
SiteMetadata,
|
SiteMetadata,
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
|
|
||||||
|
@ -140,7 +140,7 @@ type CodegenParams = {
|
||||||
i18n: I18n;
|
i18n: I18n;
|
||||||
codeTranslations: CodeTranslations;
|
codeTranslations: CodeTranslations;
|
||||||
siteMetadata: SiteMetadata;
|
siteMetadata: SiteMetadata;
|
||||||
routes: RouteConfig[];
|
routes: PluginRouteConfig[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function generateSiteFiles(params: CodegenParams): Promise<void> {
|
export async function generateSiteFiles(params: CodegenParams): Promise<void> {
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import query from 'querystring';
|
import query from 'querystring';
|
||||||
|
import path from 'path';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import {docuHash, simpleHash, escapePath, generate} from '@docusaurus/utils';
|
import {docuHash, simpleHash, escapePath, generate} from '@docusaurus/utils';
|
||||||
import type {
|
import type {
|
||||||
|
@ -14,6 +15,8 @@ import type {
|
||||||
RouteModules,
|
RouteModules,
|
||||||
ChunkNames,
|
ChunkNames,
|
||||||
RouteChunkNames,
|
RouteChunkNames,
|
||||||
|
PluginRouteConfig,
|
||||||
|
PluginIdentifier,
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
|
|
||||||
type RoutesCode = {
|
type RoutesCode = {
|
||||||
|
@ -88,13 +91,13 @@ function serializeRouteConfig({
|
||||||
routeHash,
|
routeHash,
|
||||||
exact,
|
exact,
|
||||||
subroutesCodeStrings,
|
subroutesCodeStrings,
|
||||||
props,
|
attributes,
|
||||||
}: {
|
}: {
|
||||||
routePath: string;
|
routePath: string;
|
||||||
routeHash: string;
|
routeHash: string;
|
||||||
exact?: boolean;
|
exact?: boolean;
|
||||||
subroutesCodeStrings?: string[];
|
subroutesCodeStrings?: string[];
|
||||||
props: {[propName: string]: unknown};
|
attributes: {[attributeName: string]: unknown};
|
||||||
}) {
|
}) {
|
||||||
const parts = [
|
const parts = [
|
||||||
`path: '${routePath}'`,
|
`path: '${routePath}'`,
|
||||||
|
@ -113,11 +116,11 @@ ${indent(subroutesCodeStrings.join(',\n'))}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.entries(props).forEach(([propName, propValue]) => {
|
Object.entries(attributes).forEach(([attrName, attrValue]) => {
|
||||||
const isIdentifier =
|
const isIdentifier =
|
||||||
/^[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*$/u.test(propName);
|
/^[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*$/u.test(attrName);
|
||||||
const key = isIdentifier ? propName : JSON.stringify(propName);
|
const key = isIdentifier ? attrName : JSON.stringify(attrName);
|
||||||
parts.push(`${key}: ${JSON.stringify(propValue)}`);
|
parts.push(`${key}: ${JSON.stringify(attrValue)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
return `{
|
return `{
|
||||||
|
@ -201,7 +204,9 @@ function genRouteCode(routeConfig: RouteConfig, res: RoutesCode): string {
|
||||||
priority,
|
priority,
|
||||||
exact,
|
exact,
|
||||||
metadata,
|
metadata,
|
||||||
...props
|
props,
|
||||||
|
plugin,
|
||||||
|
...attributes
|
||||||
} = routeConfig;
|
} = routeConfig;
|
||||||
|
|
||||||
if (typeof routePath !== 'string' || !component) {
|
if (typeof routePath !== 'string' || !component) {
|
||||||
|
@ -225,7 +230,7 @@ ${JSON.stringify(routeConfig)}`,
|
||||||
routeHash,
|
routeHash,
|
||||||
subroutesCodeStrings: subroutes?.map((r) => genRouteCode(r, res)),
|
subroutesCodeStrings: subroutes?.map((r) => genRouteCode(r, res)),
|
||||||
exact,
|
exact,
|
||||||
props,
|
attributes,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,14 +316,134 @@ const genRoutes = ({
|
||||||
|
|
||||||
type GenerateRouteFilesParams = {
|
type GenerateRouteFilesParams = {
|
||||||
generatedFilesDir: string;
|
generatedFilesDir: string;
|
||||||
routes: RouteConfig[];
|
routes: PluginRouteConfig[];
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function generateRouteFiles({
|
async function generateRoutePropModule({
|
||||||
|
generatedFilesDir,
|
||||||
|
route,
|
||||||
|
plugin,
|
||||||
|
}: {
|
||||||
|
generatedFilesDir: string;
|
||||||
|
route: RouteConfig;
|
||||||
|
plugin: PluginIdentifier;
|
||||||
|
}) {
|
||||||
|
ensureNoPropsConflict(route);
|
||||||
|
|
||||||
|
const moduleContent = JSON.stringify(route.props);
|
||||||
|
|
||||||
|
// TODO we should aim to reduce this path length
|
||||||
|
// This adds bytes to the global module registry
|
||||||
|
const relativePath = path.posix.join(
|
||||||
|
plugin.name,
|
||||||
|
plugin.id,
|
||||||
|
'p',
|
||||||
|
`${docuHash(route.path)}.json`,
|
||||||
|
);
|
||||||
|
const modulePath = path.posix.join(generatedFilesDir, relativePath);
|
||||||
|
const aliasedPath = path.posix.join('@generated', relativePath);
|
||||||
|
|
||||||
|
await generate(generatedFilesDir, modulePath, moduleContent);
|
||||||
|
return aliasedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNoPropsConflict(route: RouteConfig) {
|
||||||
|
if (!route.props && !route.modules) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const conflictingPropNames = _.intersection(
|
||||||
|
Object.keys(route.props ?? {}),
|
||||||
|
Object.keys(route.modules ?? {}),
|
||||||
|
);
|
||||||
|
if (conflictingPropNames.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Route ${
|
||||||
|
route.path
|
||||||
|
} has conflicting props declared using both route.modules and route.props APIs for keys: ${conflictingPropNames.join(
|
||||||
|
', ',
|
||||||
|
)}\nThis is not permitted, otherwise one prop would override the over.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function preprocessRouteProps({
|
||||||
|
generatedFilesDir,
|
||||||
|
route,
|
||||||
|
plugin,
|
||||||
|
}: {
|
||||||
|
generatedFilesDir: string;
|
||||||
|
route: RouteConfig;
|
||||||
|
plugin: PluginIdentifier;
|
||||||
|
}): Promise<RouteConfig> {
|
||||||
|
const propsModulePathPromise = route.props
|
||||||
|
? generateRoutePropModule({
|
||||||
|
generatedFilesDir,
|
||||||
|
route,
|
||||||
|
plugin,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const subRoutesPromise = route.routes
|
||||||
|
? Promise.all(
|
||||||
|
route.routes.map((subRoute: RouteConfig) => {
|
||||||
|
return preprocessRouteProps({
|
||||||
|
generatedFilesDir,
|
||||||
|
route: subRoute,
|
||||||
|
plugin,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const [propsModulePath, subRoutes] = await Promise.all([
|
||||||
|
propsModulePathPromise,
|
||||||
|
subRoutesPromise,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const newRoute: RouteConfig = {
|
||||||
|
...route,
|
||||||
|
modules: {
|
||||||
|
...route.modules,
|
||||||
|
...(propsModulePath && {__props: propsModulePath}),
|
||||||
|
},
|
||||||
|
routes: subRoutes,
|
||||||
|
props: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return newRoute;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For convenience, it's possible to pass a "route.props" object
|
||||||
|
// This method converts the props object to a regular module
|
||||||
|
// and assigns it to route.modules.__props attribute
|
||||||
|
async function preprocessAllPluginsRoutesProps({
|
||||||
generatedFilesDir,
|
generatedFilesDir,
|
||||||
routes,
|
routes,
|
||||||
|
}: {
|
||||||
|
generatedFilesDir: string;
|
||||||
|
routes: PluginRouteConfig[];
|
||||||
|
}) {
|
||||||
|
return Promise.all(
|
||||||
|
routes.map((route) => {
|
||||||
|
return preprocessRouteProps({
|
||||||
|
generatedFilesDir,
|
||||||
|
route,
|
||||||
|
plugin: route.plugin,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateRouteFiles({
|
||||||
|
generatedFilesDir,
|
||||||
|
routes: initialRoutes,
|
||||||
}: GenerateRouteFilesParams): Promise<void> {
|
}: GenerateRouteFilesParams): Promise<void> {
|
||||||
|
const routes = await preprocessAllPluginsRoutesProps({
|
||||||
|
generatedFilesDir,
|
||||||
|
routes: initialRoutes,
|
||||||
|
});
|
||||||
|
|
||||||
const {registry, routesChunkNames, routesConfig} = generateRoutesCode(routes);
|
const {registry, routesChunkNames, routesConfig} = generateRoutesCode(routes);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
genRegistry({generatedFilesDir, registry}),
|
genRegistry({generatedFilesDir, registry}),
|
||||||
|
|
|
@ -119,12 +119,16 @@ describe('loadPlugins', () => {
|
||||||
"data": {
|
"data": {
|
||||||
"someContext": "someContextPath",
|
"someContext": "someContextPath",
|
||||||
},
|
},
|
||||||
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name/default/plugin-route-context-module-100.json",
|
"plugin": "@generated/plugin-name/default/__plugin.json",
|
||||||
},
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"someModule": "someModulePath",
|
"someModule": "someModulePath",
|
||||||
},
|
},
|
||||||
"path": "/foo/",
|
"path": "/foo/",
|
||||||
|
"plugin": {
|
||||||
|
"id": "default",
|
||||||
|
"name": "plugin-name",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
@ -180,9 +184,13 @@ describe('loadPlugins', () => {
|
||||||
{
|
{
|
||||||
"component": "Comp",
|
"component": "Comp",
|
||||||
"context": {
|
"context": {
|
||||||
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name/plugin-id/plugin-route-context-module-100.json",
|
"plugin": "@generated/plugin-name/plugin-id/__plugin.json",
|
||||||
},
|
},
|
||||||
"path": "/foo/",
|
"path": "/foo/",
|
||||||
|
"plugin": {
|
||||||
|
"id": "plugin-id",
|
||||||
|
"name": "plugin-name",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
@ -275,23 +283,35 @@ describe('loadPlugins', () => {
|
||||||
{
|
{
|
||||||
"component": "Comp",
|
"component": "Comp",
|
||||||
"context": {
|
"context": {
|
||||||
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name/default/plugin-route-context-module-100.json",
|
"plugin": "@generated/plugin-name/default/__plugin.json",
|
||||||
},
|
},
|
||||||
"path": "/allContentLoadedRouteSingle/",
|
"path": "/allContentLoadedRouteSingle/",
|
||||||
|
"plugin": {
|
||||||
|
"id": "default",
|
||||||
|
"name": "plugin-name",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"component": "Comp",
|
"component": "Comp",
|
||||||
"context": {
|
"context": {
|
||||||
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name/default/plugin-route-context-module-100.json",
|
"plugin": "@generated/plugin-name/default/__plugin.json",
|
||||||
},
|
},
|
||||||
"path": "/contentLoadedRouteSingle/",
|
"path": "/contentLoadedRouteSingle/",
|
||||||
|
"plugin": {
|
||||||
|
"id": "default",
|
||||||
|
"name": "plugin-name",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"component": "Comp",
|
"component": "Comp",
|
||||||
"context": {
|
"context": {
|
||||||
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name/default/plugin-route-context-module-100.json",
|
"plugin": "@generated/plugin-name/default/__plugin.json",
|
||||||
},
|
},
|
||||||
"path": "/allContentLoadedRouteParent/",
|
"path": "/allContentLoadedRouteParent/",
|
||||||
|
"plugin": {
|
||||||
|
"id": "default",
|
||||||
|
"name": "plugin-name",
|
||||||
|
},
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"component": "Comp",
|
"component": "Comp",
|
||||||
|
@ -302,9 +322,13 @@ describe('loadPlugins', () => {
|
||||||
{
|
{
|
||||||
"component": "Comp",
|
"component": "Comp",
|
||||||
"context": {
|
"context": {
|
||||||
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name/default/plugin-route-context-module-100.json",
|
"plugin": "@generated/plugin-name/default/__plugin.json",
|
||||||
},
|
},
|
||||||
"path": "/contentLoadedRouteParent/",
|
"path": "/contentLoadedRouteParent/",
|
||||||
|
"plugin": {
|
||||||
|
"id": "default",
|
||||||
|
"name": "plugin-name",
|
||||||
|
},
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"component": "Comp",
|
"component": "Comp",
|
||||||
|
@ -386,23 +410,35 @@ describe('reloadPlugin', () => {
|
||||||
{
|
{
|
||||||
"component": "Comp",
|
"component": "Comp",
|
||||||
"context": {
|
"context": {
|
||||||
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name/default/plugin-route-context-module-100.json",
|
"plugin": "@generated/plugin-name/default/__plugin.json",
|
||||||
},
|
},
|
||||||
"path": "/allContentLoadedRouteSingle/",
|
"path": "/allContentLoadedRouteSingle/",
|
||||||
|
"plugin": {
|
||||||
|
"id": "default",
|
||||||
|
"name": "plugin-name",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"component": "Comp",
|
"component": "Comp",
|
||||||
"context": {
|
"context": {
|
||||||
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name/default/plugin-route-context-module-100.json",
|
"plugin": "@generated/plugin-name/default/__plugin.json",
|
||||||
},
|
},
|
||||||
"path": "/contentLoadedRouteSingle/",
|
"path": "/contentLoadedRouteSingle/",
|
||||||
|
"plugin": {
|
||||||
|
"id": "default",
|
||||||
|
"name": "plugin-name",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"component": "Comp",
|
"component": "Comp",
|
||||||
"context": {
|
"context": {
|
||||||
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name/default/plugin-route-context-module-100.json",
|
"plugin": "@generated/plugin-name/default/__plugin.json",
|
||||||
},
|
},
|
||||||
"path": "/allContentLoadedRouteParent/",
|
"path": "/allContentLoadedRouteParent/",
|
||||||
|
"plugin": {
|
||||||
|
"id": "default",
|
||||||
|
"name": "plugin-name",
|
||||||
|
},
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"component": "Comp",
|
"component": "Comp",
|
||||||
|
@ -413,9 +449,13 @@ describe('reloadPlugin', () => {
|
||||||
{
|
{
|
||||||
"component": "Comp",
|
"component": "Comp",
|
||||||
"context": {
|
"context": {
|
||||||
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name/default/plugin-route-context-module-100.json",
|
"plugin": "@generated/plugin-name/default/__plugin.json",
|
||||||
},
|
},
|
||||||
"path": "/contentLoadedRouteParent/",
|
"path": "/contentLoadedRouteParent/",
|
||||||
|
"plugin": {
|
||||||
|
"id": "default",
|
||||||
|
"name": "plugin-name",
|
||||||
|
},
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"component": "Comp",
|
"component": "Comp",
|
||||||
|
@ -502,23 +542,35 @@ describe('reloadPlugin', () => {
|
||||||
{
|
{
|
||||||
"component": "Comp",
|
"component": "Comp",
|
||||||
"context": {
|
"context": {
|
||||||
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name-1/default/plugin-route-context-module-100.json",
|
"plugin": "@generated/plugin-name-1/default/__plugin.json",
|
||||||
},
|
},
|
||||||
"path": "/allContentLoaded-route-initial/",
|
"path": "/allContentLoaded-route-initial/",
|
||||||
|
"plugin": {
|
||||||
|
"id": "default",
|
||||||
|
"name": "plugin-name-1",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"component": "Comp",
|
"component": "Comp",
|
||||||
"context": {
|
"context": {
|
||||||
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name-1/default/plugin-route-context-module-100.json",
|
"plugin": "@generated/plugin-name-1/default/__plugin.json",
|
||||||
},
|
},
|
||||||
"path": "/contentLoaded-route-initial/",
|
"path": "/contentLoaded-route-initial/",
|
||||||
|
"plugin": {
|
||||||
|
"id": "default",
|
||||||
|
"name": "plugin-name-1",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"component": "Comp",
|
"component": "Comp",
|
||||||
"context": {
|
"context": {
|
||||||
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name-2/default/plugin-route-context-module-100.json",
|
"plugin": "@generated/plugin-name-2/default/__plugin.json",
|
||||||
},
|
},
|
||||||
"path": "/plugin-2-route/",
|
"path": "/plugin-2-route/",
|
||||||
|
"plugin": {
|
||||||
|
"id": "default",
|
||||||
|
"name": "plugin-name-2",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
@ -542,23 +594,35 @@ describe('reloadPlugin', () => {
|
||||||
{
|
{
|
||||||
"component": "Comp",
|
"component": "Comp",
|
||||||
"context": {
|
"context": {
|
||||||
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name-1/default/plugin-route-context-module-100.json",
|
"plugin": "@generated/plugin-name-1/default/__plugin.json",
|
||||||
},
|
},
|
||||||
"path": "/allContentLoaded-route-reload/",
|
"path": "/allContentLoaded-route-reload/",
|
||||||
|
"plugin": {
|
||||||
|
"id": "default",
|
||||||
|
"name": "plugin-name-1",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"component": "Comp",
|
"component": "Comp",
|
||||||
"context": {
|
"context": {
|
||||||
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name-1/default/plugin-route-context-module-100.json",
|
"plugin": "@generated/plugin-name-1/default/__plugin.json",
|
||||||
},
|
},
|
||||||
"path": "/contentLoaded-route-reload/",
|
"path": "/contentLoaded-route-reload/",
|
||||||
|
"plugin": {
|
||||||
|
"id": "default",
|
||||||
|
"name": "plugin-name-1",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"component": "Comp",
|
"component": "Comp",
|
||||||
"context": {
|
"context": {
|
||||||
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name-2/default/plugin-route-context-module-100.json",
|
"plugin": "@generated/plugin-name-2/default/__plugin.json",
|
||||||
},
|
},
|
||||||
"path": "/plugin-2-route/",
|
"path": "/plugin-2-route/",
|
||||||
|
"plugin": {
|
||||||
|
"id": "default",
|
||||||
|
"name": "plugin-name-2",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {docuHash, generate} from '@docusaurus/utils';
|
import {generate, posixPath} from '@docusaurus/utils';
|
||||||
import {applyRouteTrailingSlash} from './routeConfig';
|
import {applyRouteTrailingSlash} from './routeConfig';
|
||||||
import type {
|
import type {
|
||||||
InitializedPlugin,
|
InitializedPlugin,
|
||||||
|
@ -38,17 +38,26 @@ export async function createPluginActionsUtils({
|
||||||
}): Promise<PluginActionUtils> {
|
}): Promise<PluginActionUtils> {
|
||||||
const pluginId = plugin.options.id;
|
const pluginId = plugin.options.id;
|
||||||
// Plugins data files are namespaced by pluginName/pluginId
|
// Plugins data files are namespaced by pluginName/pluginId
|
||||||
|
|
||||||
|
// TODO Docusaurus v4 breaking change
|
||||||
|
// module aliasing should be automatic
|
||||||
|
// we should never find local absolute FS paths in the codegen registry
|
||||||
|
const aliasedSource = (source: string) =>
|
||||||
|
`@generated/${posixPath(path.relative(generatedFilesDir, source))}`;
|
||||||
|
|
||||||
|
// TODO use @generated data dir here!
|
||||||
|
// The module registry should not contain absolute paths
|
||||||
const dataDir = path.join(generatedFilesDir, plugin.name, pluginId);
|
const dataDir = path.join(generatedFilesDir, plugin.name, pluginId);
|
||||||
|
|
||||||
const pluginRouteContext: PluginRouteContext['plugin'] = {
|
const pluginRouteContext: PluginRouteContext['plugin'] = {
|
||||||
name: plugin.name,
|
name: plugin.name,
|
||||||
id: pluginId,
|
id: pluginId,
|
||||||
};
|
};
|
||||||
const pluginRouteContextModulePath = path.join(
|
|
||||||
dataDir,
|
const pluginRouteContextModulePath = path.join(dataDir, `__plugin.json`);
|
||||||
`${docuHash('pluginRouteContextModule')}.json`,
|
|
||||||
);
|
|
||||||
// TODO not ideal place to generate that file
|
// TODO not ideal place to generate that file
|
||||||
|
// move to codegen step instead!
|
||||||
await generate(
|
await generate(
|
||||||
'/',
|
'/',
|
||||||
pluginRouteContextModulePath,
|
pluginRouteContextModulePath,
|
||||||
|
@ -69,13 +78,15 @@ export async function createPluginActionsUtils({
|
||||||
...finalRouteConfig,
|
...finalRouteConfig,
|
||||||
context: {
|
context: {
|
||||||
...(finalRouteConfig.context && {data: finalRouteConfig.context}),
|
...(finalRouteConfig.context && {data: finalRouteConfig.context}),
|
||||||
plugin: pluginRouteContextModulePath,
|
plugin: aliasedSource(pluginRouteContextModulePath),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async createData(name, data) {
|
async createData(name, data) {
|
||||||
const modulePath = path.join(dataDir, name);
|
const modulePath = path.join(dataDir, name);
|
||||||
await generate(dataDir, name, data);
|
const dataString =
|
||||||
|
typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
||||||
|
await generate(dataDir, name, dataString);
|
||||||
return modulePath;
|
return modulePath;
|
||||||
},
|
},
|
||||||
setGlobalData(data) {
|
setGlobalData(data) {
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
formatPluginName,
|
formatPluginName,
|
||||||
getPluginByIdentifier,
|
getPluginByIdentifier,
|
||||||
mergeGlobalData,
|
mergeGlobalData,
|
||||||
|
toPluginRoute,
|
||||||
} from './pluginsUtils';
|
} from './pluginsUtils';
|
||||||
import type {
|
import type {
|
||||||
LoadContext,
|
LoadContext,
|
||||||
|
@ -27,6 +28,7 @@ import type {
|
||||||
PluginIdentifier,
|
PluginIdentifier,
|
||||||
LoadedPlugin,
|
LoadedPlugin,
|
||||||
InitializedPlugin,
|
InitializedPlugin,
|
||||||
|
PluginRouteConfig,
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
|
|
||||||
async function translatePluginContent({
|
async function translatePluginContent({
|
||||||
|
@ -174,7 +176,10 @@ async function executePluginAllContentLoaded({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type AllContentLoadedResult = {routes: RouteConfig[]; globalData: GlobalData};
|
type AllContentLoadedResult = {
|
||||||
|
routes: PluginRouteConfig[];
|
||||||
|
globalData: GlobalData;
|
||||||
|
};
|
||||||
|
|
||||||
async function executeAllPluginsAllContentLoaded({
|
async function executeAllPluginsAllContentLoaded({
|
||||||
plugins,
|
plugins,
|
||||||
|
@ -186,28 +191,32 @@ async function executeAllPluginsAllContentLoaded({
|
||||||
return PerfLogger.async(`allContentLoaded()`, async () => {
|
return PerfLogger.async(`allContentLoaded()`, async () => {
|
||||||
const allContent = aggregateAllContent(plugins);
|
const allContent = aggregateAllContent(plugins);
|
||||||
|
|
||||||
const routes: RouteConfig[] = [];
|
const allRoutes: PluginRouteConfig[] = [];
|
||||||
const globalData: GlobalData = {};
|
const allGlobalData: GlobalData = {};
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
plugins.map(async (plugin) => {
|
plugins.map(async (plugin) => {
|
||||||
const {routes: pluginRoutes, globalData: pluginGlobalData} =
|
const {routes, globalData: pluginGlobalData} =
|
||||||
await executePluginAllContentLoaded({
|
await executePluginAllContentLoaded({
|
||||||
plugin,
|
plugin,
|
||||||
context,
|
context,
|
||||||
allContent,
|
allContent,
|
||||||
});
|
});
|
||||||
|
|
||||||
routes.push(...pluginRoutes);
|
const pluginRoutes = routes.map((route) =>
|
||||||
|
toPluginRoute({plugin, route}),
|
||||||
|
);
|
||||||
|
|
||||||
|
allRoutes.push(...pluginRoutes);
|
||||||
|
|
||||||
if (pluginGlobalData !== undefined) {
|
if (pluginGlobalData !== undefined) {
|
||||||
globalData[plugin.name] ??= {};
|
allGlobalData[plugin.name] ??= {};
|
||||||
globalData[plugin.name]![plugin.options.id] = pluginGlobalData;
|
allGlobalData[plugin.name]![plugin.options.id] = pluginGlobalData;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {routes, globalData};
|
return {routes: allRoutes, globalData: allGlobalData};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,7 +230,7 @@ function mergeResults({
|
||||||
plugins: LoadedPlugin[];
|
plugins: LoadedPlugin[];
|
||||||
allContentLoadedResult: AllContentLoadedResult;
|
allContentLoadedResult: AllContentLoadedResult;
|
||||||
}) {
|
}) {
|
||||||
const routes: RouteConfig[] = [
|
const routes: PluginRouteConfig[] = [
|
||||||
...aggregateRoutes(plugins),
|
...aggregateRoutes(plugins),
|
||||||
...allContentLoadedResult.routes,
|
...allContentLoadedResult.routes,
|
||||||
];
|
];
|
||||||
|
@ -237,7 +246,7 @@ function mergeResults({
|
||||||
|
|
||||||
export type LoadPluginsResult = {
|
export type LoadPluginsResult = {
|
||||||
plugins: LoadedPlugin[];
|
plugins: LoadedPlugin[];
|
||||||
routes: RouteConfig[];
|
routes: PluginRouteConfig[];
|
||||||
globalData: GlobalData;
|
globalData: GlobalData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import type {
|
||||||
InitializedPlugin,
|
InitializedPlugin,
|
||||||
LoadedPlugin,
|
LoadedPlugin,
|
||||||
PluginIdentifier,
|
PluginIdentifier,
|
||||||
|
PluginRouteConfig,
|
||||||
RouteConfig,
|
RouteConfig,
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
|
|
||||||
|
@ -49,8 +50,22 @@ export function aggregateAllContent(loadedPlugins: LoadedPlugin[]): AllContent {
|
||||||
.value();
|
.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function aggregateRoutes(loadedPlugins: LoadedPlugin[]): RouteConfig[] {
|
export function toPluginRoute({
|
||||||
return loadedPlugins.flatMap((p) => p.routes);
|
plugin,
|
||||||
|
route,
|
||||||
|
}: {
|
||||||
|
plugin: LoadedPlugin;
|
||||||
|
route: RouteConfig;
|
||||||
|
}): PluginRouteConfig {
|
||||||
|
return {plugin: {name: plugin.name, id: plugin.options.id}, ...route};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function aggregateRoutes(
|
||||||
|
loadedPlugins: LoadedPlugin[],
|
||||||
|
): PluginRouteConfig[] {
|
||||||
|
return loadedPlugins.flatMap((plugin) =>
|
||||||
|
plugin.routes.map((route) => toPluginRoute({plugin, route})),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function aggregateGlobalData(loadedPlugins: LoadedPlugin[]): GlobalData {
|
export function aggregateGlobalData(loadedPlugins: LoadedPlugin[]): GlobalData {
|
||||||
|
|
|
@ -12,10 +12,10 @@ import {
|
||||||
import type {RouteConfig} from '@docusaurus/types';
|
import type {RouteConfig} from '@docusaurus/types';
|
||||||
|
|
||||||
/** Recursively applies trailing slash config to all nested routes. */
|
/** Recursively applies trailing slash config to all nested routes. */
|
||||||
export function applyRouteTrailingSlash(
|
export function applyRouteTrailingSlash<Route extends RouteConfig>(
|
||||||
route: RouteConfig,
|
route: Route,
|
||||||
params: ApplyTrailingSlashParams,
|
params: ApplyTrailingSlashParams,
|
||||||
): RouteConfig {
|
): Route {
|
||||||
return {
|
return {
|
||||||
...route,
|
...route,
|
||||||
path: applyTrailingSlash(route.path, params),
|
path: applyTrailingSlash(route.path, params),
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {normalizeUrl} from '@docusaurus/utils';
|
import path from 'path';
|
||||||
|
import {normalizeUrl, posixPath} from '@docusaurus/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('@docusaurus/types').LoadContext} context
|
* @param {import('@docusaurus/types').LoadContext} context
|
||||||
|
@ -20,12 +21,21 @@ export default function FeatureRequestsPlugin(context) {
|
||||||
'paths.json',
|
'paths.json',
|
||||||
JSON.stringify(basePath),
|
JSON.stringify(basePath),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO Docusaurus v4 breaking change
|
||||||
|
// module aliasing should be automatic
|
||||||
|
// we should never find local absolute FS paths in the codegen registry
|
||||||
|
const aliasedSource = (source) =>
|
||||||
|
`@generated/${posixPath(
|
||||||
|
path.relative(context.generatedFilesDir, source),
|
||||||
|
)}`;
|
||||||
|
|
||||||
actions.addRoute({
|
actions.addRoute({
|
||||||
path: basePath,
|
path: basePath,
|
||||||
exact: false,
|
exact: false,
|
||||||
component: '@site/src/plugins/featureRequests/FeatureRequestsPage',
|
component: '@site/src/plugins/featureRequests/FeatureRequestsPage',
|
||||||
modules: {
|
modules: {
|
||||||
basePath: paths,
|
basePath: aliasedSource(paths),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue