mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-19 12:07:00 +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 }}
|
||||
build-script: build:website:en
|
||||
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})\.'
|
||||
minimum-change-threshold: 30
|
||||
compression: none
|
||||
|
|
|
@ -56,6 +56,7 @@ exports[`translateContent falls back when translation is incomplete 1`] = `
|
|||
"source": "/blog/2021/06/19/hello",
|
||||
"tags": [],
|
||||
"title": "Hello",
|
||||
"unlisted": false,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -99,6 +100,7 @@ exports[`translateContent returns translated loaded 1`] = `
|
|||
"source": "/blog/2021/06/19/hello",
|
||||
"tags": [],
|
||||
"title": "Hello",
|
||||
"unlisted": false,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -34,6 +34,7 @@ const sampleBlogPosts: BlogPost[] = [
|
|||
hasTruncateMarker: true,
|
||||
authors: [],
|
||||
frontMatter: {},
|
||||
unlisted: false,
|
||||
},
|
||||
content: '',
|
||||
},
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
normalizeUrl,
|
||||
docuHash,
|
||||
aliasedSitePath,
|
||||
aliasedSitePathToRelativePath,
|
||||
getPluginI18nPath,
|
||||
posixPath,
|
||||
addTrailingPathSeparator,
|
||||
|
@ -32,24 +31,17 @@ import footnoteIDFixer from './remark/footnoteIDFixer';
|
|||
import {translateContent, getTranslationFiles} from './translations';
|
||||
import {createBlogFeedFiles} from './feed';
|
||||
|
||||
import {toTagProp, toTagsProp} from './props';
|
||||
import {createAllRoutes} from './routes';
|
||||
import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';
|
||||
import type {
|
||||
LoadContext,
|
||||
Plugin,
|
||||
HtmlTags,
|
||||
RouteMetadata,
|
||||
} from '@docusaurus/types';
|
||||
import type {LoadContext, Plugin, HtmlTags} from '@docusaurus/types';
|
||||
import type {
|
||||
PluginOptions,
|
||||
BlogPostFrontMatter,
|
||||
BlogPostMetadata,
|
||||
Assets,
|
||||
BlogTag,
|
||||
BlogTags,
|
||||
BlogContent,
|
||||
BlogPaginated,
|
||||
BlogMetadata,
|
||||
} from '@docusaurus/plugin-content-blog';
|
||||
|
||||
export default async function pluginContentBlog(
|
||||
|
@ -80,6 +72,9 @@ export default async function pluginContentBlog(
|
|||
'docusaurus-plugin-content-blog',
|
||||
);
|
||||
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) =>
|
||||
`~blog/${posixPath(path.relative(pluginDataDirRoot, source))}`;
|
||||
|
||||
|
@ -185,213 +180,14 @@ export default async function pluginContentBlog(
|
|||
};
|
||||
},
|
||||
|
||||
async contentLoaded({content: blogContents, actions}) {
|
||||
const {
|
||||
blogListComponent,
|
||||
blogPostComponent,
|
||||
blogTagsListComponent,
|
||||
blogTagsPostsComponent,
|
||||
blogArchiveComponent,
|
||||
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));
|
||||
async contentLoaded({content, actions}) {
|
||||
await createAllRoutes({
|
||||
baseUrl,
|
||||
content,
|
||||
actions,
|
||||
options,
|
||||
aliasedSource,
|
||||
});
|
||||
},
|
||||
|
||||
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,
|
||||
PluginOptions,
|
||||
PropSidebarItemLink,
|
||||
PropSidebars,
|
||||
} from '@docusaurus/plugin-content-docs';
|
||||
import type {
|
||||
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!
|
||||
const utils = {
|
||||
getGlobalData: () => globalDataContainer,
|
||||
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: () => {
|
||||
// Sort the route config like in src/server/plugins/index.ts for
|
||||
// consistent snapshot ordering
|
||||
|
@ -335,11 +309,8 @@ describe('simple website', () => {
|
|||
await plugin.contentLoaded!({
|
||||
content,
|
||||
actions,
|
||||
allContent: {},
|
||||
});
|
||||
|
||||
utils.checkVersionMetadataPropCreated(currentVersion);
|
||||
|
||||
utils.expectSnapshot();
|
||||
|
||||
expect(utils.getGlobalData()).toMatchSnapshot();
|
||||
|
@ -464,14 +435,8 @@ describe('versioned website', () => {
|
|||
await plugin.contentLoaded!({
|
||||
content,
|
||||
actions,
|
||||
allContent: {},
|
||||
});
|
||||
|
||||
utils.checkVersionMetadataPropCreated(currentVersion);
|
||||
utils.checkVersionMetadataPropCreated(version101);
|
||||
utils.checkVersionMetadataPropCreated(version100);
|
||||
utils.checkVersionMetadataPropCreated(versionWithSlugs);
|
||||
|
||||
utils.expectSnapshot();
|
||||
});
|
||||
});
|
||||
|
@ -569,12 +534,8 @@ describe('versioned website (community)', () => {
|
|||
await plugin.contentLoaded!({
|
||||
content,
|
||||
actions,
|
||||
allContent: {},
|
||||
});
|
||||
|
||||
utils.checkVersionMetadataPropCreated(currentVersion);
|
||||
utils.checkVersionMetadataPropCreated(version100);
|
||||
|
||||
utils.expectSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -73,6 +73,9 @@ export default async function pluginContentDocs(
|
|||
'docusaurus-plugin-content-docs',
|
||||
);
|
||||
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) =>
|
||||
`~docs/${posixPath(path.relative(pluginDataDirRoot, source))}`;
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ import _ from 'lodash';
|
|||
import logger from '@docusaurus/logger';
|
||||
import {
|
||||
docuHash,
|
||||
createSlugger,
|
||||
normalizeUrl,
|
||||
aliasedSitePathToRelativePath,
|
||||
} from '@docusaurus/utils';
|
||||
|
@ -29,7 +28,6 @@ import type {
|
|||
CategoryGeneratedIndexMetadata,
|
||||
DocMetadata,
|
||||
PluginOptions,
|
||||
PropTagsListPage,
|
||||
} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
function createDocRouteMetadata(docMeta: DocMetadata): RouteMetadata {
|
||||
|
@ -41,36 +39,23 @@ function createDocRouteMetadata(docMeta: DocMetadata): RouteMetadata {
|
|||
|
||||
async function buildVersionCategoryGeneratedIndexRoutes({
|
||||
version,
|
||||
actions,
|
||||
options,
|
||||
aliasedSource,
|
||||
}: BuildVersionRoutesParam): Promise<RouteConfig[]> {
|
||||
const slugs = createSlugger();
|
||||
|
||||
async function buildCategoryGeneratedIndexRoute(
|
||||
categoryGeneratedIndex: CategoryGeneratedIndexMetadata,
|
||||
): 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 {
|
||||
path: categoryGeneratedIndex.permalink,
|
||||
component: options.docCategoryGeneratedIndexComponent,
|
||||
exact: true,
|
||||
modules: {
|
||||
categoryGeneratedIndex: aliasedSource(propData),
|
||||
props: {
|
||||
categoryGeneratedIndex,
|
||||
},
|
||||
// Same as doc, this sidebar route attribute permits to associate this
|
||||
// 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
|
||||
// metadataPath provided to mdx-loader.
|
||||
`${docuHash(doc.source)}.json`,
|
||||
JSON.stringify(doc, null, 2),
|
||||
doc,
|
||||
);
|
||||
|
||||
const docRoute: RouteConfig = {
|
||||
|
@ -131,7 +116,7 @@ async function buildVersionSidebarRoute(param: BuildVersionRoutesParam) {
|
|||
async function buildVersionTagsRoutes(
|
||||
param: BuildVersionRoutesParam,
|
||||
): Promise<RouteConfig[]> {
|
||||
const {version, options, actions, aliasedSource} = param;
|
||||
const {version, options} = param;
|
||||
const versionTags = getVersionTags(version.docs);
|
||||
|
||||
async function buildTagsListRoute(): Promise<RouteConfig | null> {
|
||||
|
@ -139,37 +124,27 @@ async function buildVersionTagsRoutes(
|
|||
if (Object.keys(versionTags).length === 0) {
|
||||
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 {
|
||||
path: version.tagsPath,
|
||||
exact: true,
|
||||
component: options.docTagsListComponent,
|
||||
modules: {
|
||||
tags: aliasedSource(tagsPropPath),
|
||||
props: {
|
||||
tags: toTagsListTagsProp(versionTags),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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 {
|
||||
path: tag.permalink,
|
||||
component: options.docTagDocListComponent,
|
||||
exact: true,
|
||||
modules: {
|
||||
tag: aliasedSource(tagPropPath),
|
||||
props: {
|
||||
tag: toTagDocListProp({
|
||||
allTagsPath: version.tagsPath,
|
||||
tag,
|
||||
docs: version.docs,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -189,7 +164,7 @@ type BuildVersionRoutesParam = Omit<BuildAllRoutesParam, 'versions'> & {
|
|||
async function buildVersionRoutes(
|
||||
param: BuildVersionRoutesParam,
|
||||
): Promise<RouteConfig> {
|
||||
const {version, actions, options, aliasedSource} = param;
|
||||
const {version, options} = param;
|
||||
|
||||
async function buildVersionSubRoutes() {
|
||||
const [sidebarRoute, tagsRoutes] = await Promise.all([
|
||||
|
@ -201,19 +176,15 @@ async function buildVersionRoutes(
|
|||
}
|
||||
|
||||
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 {
|
||||
path: version.path,
|
||||
exact: false,
|
||||
component: options.docVersionRootComponent,
|
||||
routes: subRoutes,
|
||||
modules: {
|
||||
version: aliasedSource(versionPropPath),
|
||||
routes: await buildVersionSubRoutes(),
|
||||
props: {
|
||||
// TODO Docusaurus v4 breaking change?
|
||||
// expose version metadata as route context instead of props
|
||||
version: toVersionMetadataProp(options.id, version),
|
||||
},
|
||||
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 path from 'path';
|
||||
import {
|
||||
encodePath,
|
||||
fileToPath,
|
||||
aliasedSitePath,
|
||||
aliasedSitePathToRelativePath,
|
||||
docuHash,
|
||||
getPluginI18nPath,
|
||||
getFolderContainingFile,
|
||||
addTrailingPathSeparator,
|
||||
Globby,
|
||||
createAbsoluteFilePathMatcher,
|
||||
normalizeUrl,
|
||||
DEFAULT_PLUGIN_ID,
|
||||
parseMarkdownFile,
|
||||
isUnlisted,
|
||||
isDraft,
|
||||
readLastUpdateData,
|
||||
getEditUrl,
|
||||
posixPath,
|
||||
} from '@docusaurus/utils';
|
||||
import {validatePageFrontMatter} from './frontMatter';
|
||||
import type {LoadContext, Plugin, RouteMetadata} from '@docusaurus/types';
|
||||
import type {PagesContentPaths} from './types';
|
||||
import {createAllRoutes} from './routes';
|
||||
import {
|
||||
createPagesContentPaths,
|
||||
getContentPathList,
|
||||
loadPagesContent,
|
||||
} from './content';
|
||||
import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||
import type {
|
||||
PluginOptions,
|
||||
Metadata,
|
||||
LoadedContent,
|
||||
PageFrontMatter,
|
||||
} 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(
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Plugin<LoadedContent | null> {
|
||||
const {siteConfig, siteDir, generatedFilesDir, localizationDir, i18n} =
|
||||
context;
|
||||
const {siteConfig, siteDir, generatedFilesDir} = context;
|
||||
|
||||
const contentPaths: PagesContentPaths = {
|
||||
contentPath: path.resolve(siteDir, options.path),
|
||||
contentPathLocalized: getPluginI18nPath({
|
||||
localizationDir,
|
||||
pluginName: 'docusaurus-plugin-content-pages',
|
||||
pluginId: options.id,
|
||||
}),
|
||||
};
|
||||
const contentPaths = createPagesContentPaths({context, options});
|
||||
|
||||
const pluginDataDirRoot = path.join(
|
||||
generatedFilesDir,
|
||||
|
@ -77,182 +52,17 @@ export default function pluginContentPages(
|
|||
},
|
||||
|
||||
async loadContent() {
|
||||
const {include, editUrl} = options;
|
||||
|
||||
if (!(await fs.pathExists(contentPaths.contentPath))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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[];
|
||||
return loadPagesContent({context, options, contentPaths});
|
||||
},
|
||||
|
||||
async contentLoaded({content, actions}) {
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
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`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
await createAllRoutes({content, options, actions});
|
||||
},
|
||||
|
||||
configureWebpack() {
|
||||
|
@ -265,11 +75,6 @@ export default function pluginContentPages(
|
|||
} = options;
|
||||
const contentDirs = getContentPathList(contentPaths);
|
||||
return {
|
||||
resolve: {
|
||||
alias: {
|
||||
'~pages': pluginDataDirRoot,
|
||||
},
|
||||
},
|
||||
module: {
|
||||
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.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import {docuHash, normalizeUrl, posixPath} from '@docusaurus/utils';
|
||||
import {normalizeUrl} from '@docusaurus/utils';
|
||||
import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||
|
||||
export default function pluginDebug({
|
||||
siteConfig: {baseUrl},
|
||||
generatedFilesDir,
|
||||
}: LoadContext): Plugin<undefined> {
|
||||
const pluginDataDirRoot = path.join(
|
||||
generatedFilesDir,
|
||||
'docusaurus-plugin-debug',
|
||||
);
|
||||
const aliasedSource = (source: string) =>
|
||||
`~debug/${posixPath(path.relative(pluginDataDirRoot, source))}`;
|
||||
|
||||
return {
|
||||
name: 'docusaurus-plugin-debug',
|
||||
|
||||
|
@ -30,14 +21,7 @@ export default function pluginDebug({
|
|||
return '../src/theme';
|
||||
},
|
||||
|
||||
async allContentLoaded({actions: {createData, 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),
|
||||
);
|
||||
|
||||
async allContentLoaded({actions: {addRoute}, allContent}) {
|
||||
// Home is config (duplicate for now)
|
||||
addRoute({
|
||||
path: normalizeUrl([baseUrl, '__docusaurus/debug']),
|
||||
|
@ -73,8 +57,8 @@ export default function pluginDebug({
|
|||
path: normalizeUrl([baseUrl, '__docusaurus/debug/content']),
|
||||
component: '@theme/DebugContent',
|
||||
exact: true,
|
||||
modules: {
|
||||
allContent: aliasedSource(allContentPath),
|
||||
props: {
|
||||
allContent,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -84,15 +68,5 @@ export default function pluginDebug({
|
|||
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 {CodeTranslations, I18n} from './i18n';
|
||||
import type {LoadedPlugin, PluginVersionInformation} from './plugin';
|
||||
import type {RouteConfig} from './routing';
|
||||
import type {PluginRouteConfig} from './routing';
|
||||
|
||||
export type DocusaurusContext = {
|
||||
siteConfig: DocusaurusConfig;
|
||||
|
@ -57,7 +57,7 @@ export type Props = LoadContext & {
|
|||
preBodyTags: string;
|
||||
postBodyTags: string;
|
||||
siteMetadata: SiteMetadata;
|
||||
routes: RouteConfig[];
|
||||
routes: PluginRouteConfig[];
|
||||
routesPaths: string[];
|
||||
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 {
|
||||
RouteConfig,
|
||||
PluginRouteConfig,
|
||||
RouteMetadata,
|
||||
RouteContext,
|
||||
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 = {
|
||||
addRoute: (config: RouteConfig) => void;
|
||||
createData: (name: string, data: string) => Promise<string>;
|
||||
createData: (name: string, data: string | object) => Promise<string>;
|
||||
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 {PluginIdentifier} from './plugin';
|
||||
|
||||
/**
|
||||
* A "module" represents a unit of serialized data emitted from the plugin. It
|
||||
|
@ -110,9 +111,23 @@ export type RouteConfig = {
|
|||
*/
|
||||
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 = {
|
||||
|
|
|
@ -85,7 +85,8 @@ export default function ComponentCreator(
|
|||
const loadedModules = JSON.parse(JSON.stringify(chunkNames)) as {
|
||||
__comp?: React.ComponentType<object>;
|
||||
__context?: RouteContext;
|
||||
[propName: string]: unknown;
|
||||
__props?: {[propName: string]: unknown};
|
||||
[attributeName: string]: unknown;
|
||||
};
|
||||
Object.entries(loaded).forEach(([keyPath, loadedModule]) => {
|
||||
// JSON modules are also loaded as `{ default: ... }` (`import()`
|
||||
|
@ -127,12 +128,18 @@ export default function ComponentCreator(
|
|||
delete loadedModules.__comp;
|
||||
const routeContext = loadedModules.__context!;
|
||||
delete loadedModules.__context;
|
||||
const routeProps = loadedModules.__props;
|
||||
delete loadedModules.__props;
|
||||
/* eslint-enable no-underscore-dangle */
|
||||
|
||||
// Is there any way to put this RouteContextProvider upper in the tree?
|
||||
return (
|
||||
<RouteContextProvider value={routeContext}>
|
||||
<Component {...loadedModules} {...(props as object)} />
|
||||
<Component
|
||||
{...loadedModules}
|
||||
{...routeProps}
|
||||
{...(props as object)}
|
||||
/>
|
||||
</RouteContextProvider>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -16,7 +16,7 @@ import type {
|
|||
DocusaurusConfig,
|
||||
GlobalData,
|
||||
I18n,
|
||||
RouteConfig,
|
||||
PluginRouteConfig,
|
||||
SiteMetadata,
|
||||
} from '@docusaurus/types';
|
||||
|
||||
|
@ -140,7 +140,7 @@ type CodegenParams = {
|
|||
i18n: I18n;
|
||||
codeTranslations: CodeTranslations;
|
||||
siteMetadata: SiteMetadata;
|
||||
routes: RouteConfig[];
|
||||
routes: PluginRouteConfig[];
|
||||
};
|
||||
|
||||
export async function generateSiteFiles(params: CodegenParams): Promise<void> {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import query from 'querystring';
|
||||
import path from 'path';
|
||||
import _ from 'lodash';
|
||||
import {docuHash, simpleHash, escapePath, generate} from '@docusaurus/utils';
|
||||
import type {
|
||||
|
@ -14,6 +15,8 @@ import type {
|
|||
RouteModules,
|
||||
ChunkNames,
|
||||
RouteChunkNames,
|
||||
PluginRouteConfig,
|
||||
PluginIdentifier,
|
||||
} from '@docusaurus/types';
|
||||
|
||||
type RoutesCode = {
|
||||
|
@ -88,13 +91,13 @@ function serializeRouteConfig({
|
|||
routeHash,
|
||||
exact,
|
||||
subroutesCodeStrings,
|
||||
props,
|
||||
attributes,
|
||||
}: {
|
||||
routePath: string;
|
||||
routeHash: string;
|
||||
exact?: boolean;
|
||||
subroutesCodeStrings?: string[];
|
||||
props: {[propName: string]: unknown};
|
||||
attributes: {[attributeName: string]: unknown};
|
||||
}) {
|
||||
const parts = [
|
||||
`path: '${routePath}'`,
|
||||
|
@ -113,11 +116,11 @@ ${indent(subroutesCodeStrings.join(',\n'))}
|
|||
);
|
||||
}
|
||||
|
||||
Object.entries(props).forEach(([propName, propValue]) => {
|
||||
Object.entries(attributes).forEach(([attrName, attrValue]) => {
|
||||
const isIdentifier =
|
||||
/^[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*$/u.test(propName);
|
||||
const key = isIdentifier ? propName : JSON.stringify(propName);
|
||||
parts.push(`${key}: ${JSON.stringify(propValue)}`);
|
||||
/^[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*$/u.test(attrName);
|
||||
const key = isIdentifier ? attrName : JSON.stringify(attrName);
|
||||
parts.push(`${key}: ${JSON.stringify(attrValue)}`);
|
||||
});
|
||||
|
||||
return `{
|
||||
|
@ -201,7 +204,9 @@ function genRouteCode(routeConfig: RouteConfig, res: RoutesCode): string {
|
|||
priority,
|
||||
exact,
|
||||
metadata,
|
||||
...props
|
||||
props,
|
||||
plugin,
|
||||
...attributes
|
||||
} = routeConfig;
|
||||
|
||||
if (typeof routePath !== 'string' || !component) {
|
||||
|
@ -225,7 +230,7 @@ ${JSON.stringify(routeConfig)}`,
|
|||
routeHash,
|
||||
subroutesCodeStrings: subroutes?.map((r) => genRouteCode(r, res)),
|
||||
exact,
|
||||
props,
|
||||
attributes,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -311,14 +316,134 @@ const genRoutes = ({
|
|||
|
||||
type GenerateRouteFilesParams = {
|
||||
generatedFilesDir: string;
|
||||
routes: RouteConfig[];
|
||||
routes: PluginRouteConfig[];
|
||||
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,
|
||||
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> {
|
||||
const routes = await preprocessAllPluginsRoutesProps({
|
||||
generatedFilesDir,
|
||||
routes: initialRoutes,
|
||||
});
|
||||
|
||||
const {registry, routesChunkNames, routesConfig} = generateRoutesCode(routes);
|
||||
await Promise.all([
|
||||
genRegistry({generatedFilesDir, registry}),
|
||||
|
|
|
@ -119,12 +119,16 @@ describe('loadPlugins', () => {
|
|||
"data": {
|
||||
"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": {
|
||||
"someModule": "someModulePath",
|
||||
},
|
||||
"path": "/foo/",
|
||||
"plugin": {
|
||||
"id": "default",
|
||||
"name": "plugin-name",
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
@ -180,9 +184,13 @@ describe('loadPlugins', () => {
|
|||
{
|
||||
"component": "Comp",
|
||||
"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/",
|
||||
"plugin": {
|
||||
"id": "plugin-id",
|
||||
"name": "plugin-name",
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
@ -275,23 +283,35 @@ describe('loadPlugins', () => {
|
|||
{
|
||||
"component": "Comp",
|
||||
"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/",
|
||||
"plugin": {
|
||||
"id": "default",
|
||||
"name": "plugin-name",
|
||||
},
|
||||
},
|
||||
{
|
||||
"component": "Comp",
|
||||
"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/",
|
||||
"plugin": {
|
||||
"id": "default",
|
||||
"name": "plugin-name",
|
||||
},
|
||||
},
|
||||
{
|
||||
"component": "Comp",
|
||||
"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/",
|
||||
"plugin": {
|
||||
"id": "default",
|
||||
"name": "plugin-name",
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"component": "Comp",
|
||||
|
@ -302,9 +322,13 @@ describe('loadPlugins', () => {
|
|||
{
|
||||
"component": "Comp",
|
||||
"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/",
|
||||
"plugin": {
|
||||
"id": "default",
|
||||
"name": "plugin-name",
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"component": "Comp",
|
||||
|
@ -386,23 +410,35 @@ describe('reloadPlugin', () => {
|
|||
{
|
||||
"component": "Comp",
|
||||
"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/",
|
||||
"plugin": {
|
||||
"id": "default",
|
||||
"name": "plugin-name",
|
||||
},
|
||||
},
|
||||
{
|
||||
"component": "Comp",
|
||||
"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/",
|
||||
"plugin": {
|
||||
"id": "default",
|
||||
"name": "plugin-name",
|
||||
},
|
||||
},
|
||||
{
|
||||
"component": "Comp",
|
||||
"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/",
|
||||
"plugin": {
|
||||
"id": "default",
|
||||
"name": "plugin-name",
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"component": "Comp",
|
||||
|
@ -413,9 +449,13 @@ describe('reloadPlugin', () => {
|
|||
{
|
||||
"component": "Comp",
|
||||
"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/",
|
||||
"plugin": {
|
||||
"id": "default",
|
||||
"name": "plugin-name",
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"component": "Comp",
|
||||
|
@ -502,23 +542,35 @@ describe('reloadPlugin', () => {
|
|||
{
|
||||
"component": "Comp",
|
||||
"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/",
|
||||
"plugin": {
|
||||
"id": "default",
|
||||
"name": "plugin-name-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
"component": "Comp",
|
||||
"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/",
|
||||
"plugin": {
|
||||
"id": "default",
|
||||
"name": "plugin-name-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
"component": "Comp",
|
||||
"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/",
|
||||
"plugin": {
|
||||
"id": "default",
|
||||
"name": "plugin-name-2",
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
@ -542,23 +594,35 @@ describe('reloadPlugin', () => {
|
|||
{
|
||||
"component": "Comp",
|
||||
"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/",
|
||||
"plugin": {
|
||||
"id": "default",
|
||||
"name": "plugin-name-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
"component": "Comp",
|
||||
"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/",
|
||||
"plugin": {
|
||||
"id": "default",
|
||||
"name": "plugin-name-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
"component": "Comp",
|
||||
"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/",
|
||||
"plugin": {
|
||||
"id": "default",
|
||||
"name": "plugin-name-2",
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import path from 'path';
|
||||
import {docuHash, generate} from '@docusaurus/utils';
|
||||
import {generate, posixPath} from '@docusaurus/utils';
|
||||
import {applyRouteTrailingSlash} from './routeConfig';
|
||||
import type {
|
||||
InitializedPlugin,
|
||||
|
@ -38,17 +38,26 @@ export async function createPluginActionsUtils({
|
|||
}): Promise<PluginActionUtils> {
|
||||
const pluginId = plugin.options.id;
|
||||
// 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 pluginRouteContext: PluginRouteContext['plugin'] = {
|
||||
name: plugin.name,
|
||||
id: pluginId,
|
||||
};
|
||||
const pluginRouteContextModulePath = path.join(
|
||||
dataDir,
|
||||
`${docuHash('pluginRouteContextModule')}.json`,
|
||||
);
|
||||
|
||||
const pluginRouteContextModulePath = path.join(dataDir, `__plugin.json`);
|
||||
|
||||
// TODO not ideal place to generate that file
|
||||
// move to codegen step instead!
|
||||
await generate(
|
||||
'/',
|
||||
pluginRouteContextModulePath,
|
||||
|
@ -69,13 +78,15 @@ export async function createPluginActionsUtils({
|
|||
...finalRouteConfig,
|
||||
context: {
|
||||
...(finalRouteConfig.context && {data: finalRouteConfig.context}),
|
||||
plugin: pluginRouteContextModulePath,
|
||||
plugin: aliasedSource(pluginRouteContextModulePath),
|
||||
},
|
||||
});
|
||||
},
|
||||
async createData(name, data) {
|
||||
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;
|
||||
},
|
||||
setGlobalData(data) {
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
formatPluginName,
|
||||
getPluginByIdentifier,
|
||||
mergeGlobalData,
|
||||
toPluginRoute,
|
||||
} from './pluginsUtils';
|
||||
import type {
|
||||
LoadContext,
|
||||
|
@ -27,6 +28,7 @@ import type {
|
|||
PluginIdentifier,
|
||||
LoadedPlugin,
|
||||
InitializedPlugin,
|
||||
PluginRouteConfig,
|
||||
} from '@docusaurus/types';
|
||||
|
||||
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({
|
||||
plugins,
|
||||
|
@ -186,28 +191,32 @@ async function executeAllPluginsAllContentLoaded({
|
|||
return PerfLogger.async(`allContentLoaded()`, async () => {
|
||||
const allContent = aggregateAllContent(plugins);
|
||||
|
||||
const routes: RouteConfig[] = [];
|
||||
const globalData: GlobalData = {};
|
||||
const allRoutes: PluginRouteConfig[] = [];
|
||||
const allGlobalData: GlobalData = {};
|
||||
|
||||
await Promise.all(
|
||||
plugins.map(async (plugin) => {
|
||||
const {routes: pluginRoutes, globalData: pluginGlobalData} =
|
||||
const {routes, globalData: pluginGlobalData} =
|
||||
await executePluginAllContentLoaded({
|
||||
plugin,
|
||||
context,
|
||||
allContent,
|
||||
});
|
||||
|
||||
routes.push(...pluginRoutes);
|
||||
const pluginRoutes = routes.map((route) =>
|
||||
toPluginRoute({plugin, route}),
|
||||
);
|
||||
|
||||
allRoutes.push(...pluginRoutes);
|
||||
|
||||
if (pluginGlobalData !== undefined) {
|
||||
globalData[plugin.name] ??= {};
|
||||
globalData[plugin.name]![plugin.options.id] = pluginGlobalData;
|
||||
allGlobalData[plugin.name] ??= {};
|
||||
allGlobalData[plugin.name]![plugin.options.id] = pluginGlobalData;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return {routes, globalData};
|
||||
return {routes: allRoutes, globalData: allGlobalData};
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -221,7 +230,7 @@ function mergeResults({
|
|||
plugins: LoadedPlugin[];
|
||||
allContentLoadedResult: AllContentLoadedResult;
|
||||
}) {
|
||||
const routes: RouteConfig[] = [
|
||||
const routes: PluginRouteConfig[] = [
|
||||
...aggregateRoutes(plugins),
|
||||
...allContentLoadedResult.routes,
|
||||
];
|
||||
|
@ -237,7 +246,7 @@ function mergeResults({
|
|||
|
||||
export type LoadPluginsResult = {
|
||||
plugins: LoadedPlugin[];
|
||||
routes: RouteConfig[];
|
||||
routes: PluginRouteConfig[];
|
||||
globalData: GlobalData;
|
||||
};
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import type {
|
|||
InitializedPlugin,
|
||||
LoadedPlugin,
|
||||
PluginIdentifier,
|
||||
PluginRouteConfig,
|
||||
RouteConfig,
|
||||
} from '@docusaurus/types';
|
||||
|
||||
|
@ -49,8 +50,22 @@ export function aggregateAllContent(loadedPlugins: LoadedPlugin[]): AllContent {
|
|||
.value();
|
||||
}
|
||||
|
||||
export function aggregateRoutes(loadedPlugins: LoadedPlugin[]): RouteConfig[] {
|
||||
return loadedPlugins.flatMap((p) => p.routes);
|
||||
export function toPluginRoute({
|
||||
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 {
|
||||
|
|
|
@ -12,10 +12,10 @@ import {
|
|||
import type {RouteConfig} from '@docusaurus/types';
|
||||
|
||||
/** Recursively applies trailing slash config to all nested routes. */
|
||||
export function applyRouteTrailingSlash(
|
||||
route: RouteConfig,
|
||||
export function applyRouteTrailingSlash<Route extends RouteConfig>(
|
||||
route: Route,
|
||||
params: ApplyTrailingSlashParams,
|
||||
): RouteConfig {
|
||||
): Route {
|
||||
return {
|
||||
...route,
|
||||
path: applyTrailingSlash(route.path, params),
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 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
|
||||
|
@ -20,12 +21,21 @@ export default function FeatureRequestsPlugin(context) {
|
|||
'paths.json',
|
||||
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({
|
||||
path: basePath,
|
||||
exact: false,
|
||||
component: '@site/src/plugins/featureRequests/FeatureRequestsPage',
|
||||
modules: {
|
||||
basePath: paths,
|
||||
basePath: aliasedSource(paths),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue