refactor(v2): change plugin api (#1547)

* misc(v2): new plugin format example

* refactor(v2): make all plugins a function returning objects

* misc: add CHANGELOG

* misc(v2): update CHANGELOG

* misc(v2): fix tests

* misc(v2): convert swizzle command

* misc(v2): convert sitemap back to commonjs
This commit is contained in:
Yangshun Tay 2019-06-02 20:37:22 -07:00 committed by GitHub
parent 9feb7b2c64
commit 6a814ac64a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 709 additions and 725 deletions

View file

@ -31,336 +31,334 @@ const DEFAULT_OPTIONS = {
blogTagsPostsComponent: '@theme/BlogTagsPostsPage', blogTagsPostsComponent: '@theme/BlogTagsPostsPage',
}; };
class DocusaurusPluginContentBlog { module.exports = function(context, opts) {
constructor(context, opts) { const options = {...DEFAULT_OPTIONS, ...opts};
this.options = {...DEFAULT_OPTIONS, ...opts}; const contentPath = path.resolve(context.siteDir, options.path);
this.context = context;
this.contentPath = path.resolve(this.context.siteDir, this.options.path);
}
getName() { return {
return 'docusaurus-plugin-content-blog'; name: 'docusaurus-plugin-content-blog',
}
getPathsToWatch() { getPathsToWatch() {
const {include = []} = this.options; const {include = []} = options;
const globPattern = include.map( const globPattern = include.map(pattern => `${contentPath}/${pattern}`);
pattern => `${this.contentPath}/${pattern}`, return [...globPattern];
); },
return [...globPattern];
}
// Fetches blog contents and returns metadata for the necessary routes. // Fetches blog contents and returns metadata for the necessary routes.
async loadContent() { async loadContent() {
const {postsPerPage, include, routeBasePath} = this.options; const {postsPerPage, include, routeBasePath} = options;
const {siteConfig} = this.context; const {siteConfig} = context;
const blogDir = this.contentPath; const blogDir = contentPath;
if (!fs.existsSync(blogDir)) { if (!fs.existsSync(blogDir)) {
return null; return null;
} }
const {baseUrl} = siteConfig; const {baseUrl} = siteConfig;
const blogFiles = await globby(include, { const blogFiles = await globby(include, {
cwd: blogDir, cwd: blogDir,
});
const blogPosts = [];
await Promise.all(
blogFiles.map(async relativeSource => {
const source = path.join(blogDir, relativeSource);
const blogFileName = path.basename(relativeSource);
// Extract, YYYY, MM, DD from the file name.
const filePathDateArr = blogFileName.split('-');
const date = new Date(
`${filePathDateArr[0]}-${filePathDateArr[1]}-${
filePathDateArr[2]
}T06:00:00.000Z`,
);
const fileString = await fs.readFile(source, 'utf-8');
const {frontMatter, excerpt} = parse(fileString);
blogPosts.push({
id: blogFileName,
metadata: {
permalink: normalizeUrl([
baseUrl,
routeBasePath,
fileToUrl(blogFileName),
]),
source,
description: frontMatter.description || excerpt,
date,
tags: frontMatter.tags,
title: frontMatter.title || blogFileName,
},
});
}),
);
blogPosts.sort((a, b) => b.metadata.date - a.metadata.date);
// Blog pagination routes.
// Example: `/blog`, `/blog/page/1`, `/blog/page/2`
const totalCount = blogPosts.length;
const numberOfPages = Math.ceil(totalCount / postsPerPage);
const basePageUrl = normalizeUrl([baseUrl, routeBasePath]);
const blogListPaginated = [];
function blogPaginationPermalink(page) {
return page > 0
? normalizeUrl([basePageUrl, `page/${page + 1}`])
: basePageUrl;
}
for (let page = 0; page < numberOfPages; page += 1) {
blogListPaginated.push({
metadata: {
permalink: blogPaginationPermalink(page),
page: page + 1,
postsPerPage,
totalPages: numberOfPages,
totalCount,
previousPage: page !== 0 ? blogPaginationPermalink(page - 1) : null,
nextPage:
page < numberOfPages - 1 ? blogPaginationPermalink(page + 1) : null,
},
items: blogPosts
.slice(page * postsPerPage, (page + 1) * postsPerPage)
.map(item => item.id),
}); });
}
const blogTags = {}; const blogPosts = [];
const tagsPath = normalizeUrl([basePageUrl, 'tags']);
blogPosts.forEach(blogPost => { await Promise.all(
const {tags} = blogPost.metadata; blogFiles.map(async relativeSource => {
if (!tags || tags.length === 0) { const source = path.join(blogDir, relativeSource);
// TODO: Extract tags out into a separate plugin.
const blogFileName = path.basename(relativeSource);
// Extract, YYYY, MM, DD from the file name.
const filePathDateArr = blogFileName.split('-');
const date = new Date(
`${filePathDateArr[0]}-${filePathDateArr[1]}-${
filePathDateArr[2]
}T06:00:00.000Z`,
);
const fileString = await fs.readFile(source, 'utf-8');
const {frontMatter, excerpt} = parse(fileString);
blogPosts.push({
id: blogFileName,
metadata: {
permalink: normalizeUrl([
baseUrl,
routeBasePath,
fileToUrl(blogFileName),
]),
source,
description: frontMatter.description || excerpt,
date,
tags: frontMatter.tags,
title: frontMatter.title || blogFileName,
},
});
}),
);
blogPosts.sort((a, b) => b.metadata.date - a.metadata.date);
// Blog pagination routes.
// Example: `/blog`, `/blog/page/1`, `/blog/page/2`
const totalCount = blogPosts.length;
const numberOfPages = Math.ceil(totalCount / postsPerPage);
const basePageUrl = normalizeUrl([baseUrl, routeBasePath]);
const blogListPaginated = [];
function blogPaginationPermalink(page) {
return page > 0
? normalizeUrl([basePageUrl, `page/${page + 1}`])
: basePageUrl;
}
for (let page = 0; page < numberOfPages; page += 1) {
blogListPaginated.push({
metadata: {
permalink: blogPaginationPermalink(page),
page: page + 1,
postsPerPage,
totalPages: numberOfPages,
totalCount,
previousPage: page !== 0 ? blogPaginationPermalink(page - 1) : null,
nextPage:
page < numberOfPages - 1
? blogPaginationPermalink(page + 1)
: null,
},
items: blogPosts
.slice(page * postsPerPage, (page + 1) * postsPerPage)
.map(item => item.id),
});
}
const blogTags = {};
const tagsPath = normalizeUrl([basePageUrl, 'tags']);
blogPosts.forEach(blogPost => {
const {tags} = blogPost.metadata;
if (!tags || tags.length === 0) {
// TODO: Extract tags out into a separate plugin.
// eslint-disable-next-line no-param-reassign
blogPost.metadata.tags = [];
return;
}
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
blogPost.metadata.tags = []; blogPost.metadata.tags = tags.map(tag => {
const normalizedTag = _.kebabCase(tag);
const permalink = normalizeUrl([tagsPath, normalizedTag]);
if (!blogTags[normalizedTag]) {
blogTags[normalizedTag] = {
name: tag.toLowerCase(), // Will only use the name of the first occurrence of the tag.
items: [],
permalink,
};
}
blogTags[normalizedTag].items.push(blogPost.id);
return {
label: tag,
permalink,
};
});
});
const blogTagsListPath =
Object.keys(blogTags).length > 0 ? tagsPath : null;
return {
blogPosts,
blogListPaginated,
blogTags,
blogTagsListPath,
};
},
async contentLoaded({content: blogContents, actions}) {
if (!blogContents) {
return; return;
} }
// eslint-disable-next-line no-param-reassign const {
blogPost.metadata.tags = tags.map(tag => { blogListComponent,
const normalizedTag = _.kebabCase(tag); blogPostComponent,
const permalink = normalizeUrl([tagsPath, normalizedTag]); blogTagsListComponent,
if (!blogTags[normalizedTag]) { blogTagsPostsComponent,
blogTags[normalizedTag] = { } = options;
name: tag.toLowerCase(), // Will only use the name of the first occurrence of the tag.
items: [], const {addRoute, createData} = actions;
permalink, const {
blogPosts,
blogListPaginated,
blogTags,
blogTagsListPath,
} = blogContents;
const blogItemsToModules = {};
// Create routes for blog entries.
const blogItems = await Promise.all(
blogPosts.map(async blogPost => {
const {id, metadata} = blogPost;
const {permalink} = metadata;
const metadataPath = await createData(
`${docuHash(permalink)}.json`,
JSON.stringify(metadata, null, 2),
);
const temp = {
metadata,
metadataPath,
}; };
}
blogTags[normalizedTag].items.push(blogPost.id); blogItemsToModules[id] = temp;
return temp;
return { }),
label: tag,
permalink,
};
});
});
const blogTagsListPath = Object.keys(blogTags).length > 0 ? tagsPath : null;
return {
blogPosts,
blogListPaginated,
blogTags,
blogTagsListPath,
};
}
async contentLoaded({content: blogContents, actions}) {
if (!blogContents) {
return;
}
const {
blogListComponent,
blogPostComponent,
blogTagsListComponent,
blogTagsPostsComponent,
} = this.options;
const {addRoute, createData} = actions;
const {
blogPosts,
blogListPaginated,
blogTags,
blogTagsListPath,
} = blogContents;
const blogItemsToModules = {};
// Create routes for blog entries.
const blogItems = await Promise.all(
blogPosts.map(async blogPost => {
const {id, metadata} = blogPost;
const {permalink} = metadata;
const metadataPath = await createData(
`${docuHash(permalink)}.json`,
JSON.stringify(metadata, null, 2),
);
const temp = {
metadata,
metadataPath,
};
blogItemsToModules[id] = temp;
return temp;
}),
);
blogItems.forEach((blogItem, index) => {
const prevItem = index > 0 ? blogItems[index - 1] : null;
const nextItem =
index < blogItems.length - 1 ? blogItems[index + 1] : null;
const {metadata, metadataPath} = blogItem;
const {source, permalink} = metadata;
addRoute({
path: permalink,
component: blogPostComponent,
exact: true,
modules: {
content: source,
metadata: metadataPath,
prevItem: prevItem && prevItem.metadataPath,
nextItem: nextItem && nextItem.metadataPath,
},
});
});
// 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: {
items: items.map(postID => {
const {metadata: postMetadata, metadataPath} = blogItemsToModules[
postID
];
// To tell routes.js this is an import and not a nested object to recurse.
return {
content: {
__import: true,
path: postMetadata.source,
query: {
truncated: true,
},
},
metadata: metadataPath,
};
}),
metadata: pageMetadataPath,
},
});
}),
);
// Tags.
const tagsModule = {};
await Promise.all(
Object.keys(blogTags).map(async tag => {
const {name, items, permalink} = blogTags[tag];
tagsModule[tag] = {
allTagsPath: blogTagsListPath,
slug: tag,
name,
count: items.length,
permalink,
};
const tagsMetadataPath = await createData(
`${docuHash(permalink)}.json`,
JSON.stringify(tagsModule[tag], null, 2),
);
addRoute({
path: permalink,
component: blogTagsPostsComponent,
exact: true,
modules: {
items: items.map(postID => {
const {metadata: postMetadata, metadataPath} = blogItemsToModules[
postID
];
return {
content: {
__import: true,
path: postMetadata.source,
query: {
truncated: true,
},
},
metadata: metadataPath,
};
}),
metadata: tagsMetadataPath,
},
});
}),
);
// Only create /tags page if there are tags.
if (Object.keys(blogTags).length > 0) {
const tagsListPath = await createData(
`${docuHash(`${blogTagsListPath}-tags`)}.json`,
JSON.stringify(tagsModule, null, 2),
); );
addRoute({ blogItems.forEach((blogItem, index) => {
path: blogTagsListPath, const prevItem = index > 0 ? blogItems[index - 1] : null;
component: blogTagsListComponent, const nextItem =
exact: true, index < blogItems.length - 1 ? blogItems[index + 1] : null;
modules: { const {metadata, metadataPath} = blogItem;
tags: tagsListPath, const {source, permalink} = metadata;
},
});
}
}
getThemePath() { addRoute({
return path.resolve(__dirname, './theme'); path: permalink,
} component: blogPostComponent,
exact: true,
configureWebpack(config, isServer, {getBabelLoader, getCacheLoader}) { modules: {
return { content: source,
module: { metadata: metadataPath,
rules: [ prevItem: prevItem && prevItem.metadataPath,
{ nextItem: nextItem && nextItem.metadataPath,
test: /(\.mdx?)$/,
include: [this.contentPath],
use: [
getCacheLoader(isServer),
getBabelLoader(isServer),
'@docusaurus/mdx-loader',
{
loader: path.resolve(__dirname, './markdownLoader.js'),
},
],
}, },
], });
}, });
};
}
}
module.exports = DocusaurusPluginContentBlog; // 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: {
items: items.map(postID => {
const {
metadata: postMetadata,
metadataPath,
} = blogItemsToModules[postID];
// To tell routes.js this is an import and not a nested object to recurse.
return {
content: {
__import: true,
path: postMetadata.source,
query: {
truncated: true,
},
},
metadata: metadataPath,
};
}),
metadata: pageMetadataPath,
},
});
}),
);
// Tags.
const tagsModule = {};
await Promise.all(
Object.keys(blogTags).map(async tag => {
const {name, items, permalink} = blogTags[tag];
tagsModule[tag] = {
allTagsPath: blogTagsListPath,
slug: tag,
name,
count: items.length,
permalink,
};
const tagsMetadataPath = await createData(
`${docuHash(permalink)}.json`,
JSON.stringify(tagsModule[tag], null, 2),
);
addRoute({
path: permalink,
component: blogTagsPostsComponent,
exact: true,
modules: {
items: items.map(postID => {
const {
metadata: postMetadata,
metadataPath,
} = blogItemsToModules[postID];
return {
content: {
__import: true,
path: postMetadata.source,
query: {
truncated: true,
},
},
metadata: metadataPath,
};
}),
metadata: tagsMetadataPath,
},
});
}),
);
// Only create /tags page if there are tags.
if (Object.keys(blogTags).length > 0) {
const tagsListPath = await createData(
`${docuHash(`${blogTagsListPath}-tags`)}.json`,
JSON.stringify(tagsModule, null, 2),
);
addRoute({
path: blogTagsListPath,
component: blogTagsListComponent,
exact: true,
modules: {
tags: tagsListPath,
},
});
}
},
getThemePath() {
return path.resolve(__dirname, './theme');
},
configureWebpack(config, isServer, {getBabelLoader, getCacheLoader}) {
return {
module: {
rules: [
{
test: /(\.mdx?)$/,
include: [contentPath],
use: [
getCacheLoader(isServer),
getBabelLoader(isServer),
'@docusaurus/mdx-loader',
{
loader: path.resolve(__dirname, './markdownLoader.js'),
},
],
},
],
},
};
},
};
};

View file

@ -6,7 +6,7 @@
*/ */
import path from 'path'; import path from 'path';
import DocusaurusPluginContentDocs from '../index'; import pluginContentDocs from '../index';
describe('loadDocs', () => { describe('loadDocs', () => {
test('simple website', async () => { test('simple website', async () => {
@ -17,7 +17,7 @@ describe('loadDocs', () => {
url: 'https://docusaurus.io', url: 'https://docusaurus.io',
}; };
const sidebarPath = path.join(siteDir, 'sidebars.json'); const sidebarPath = path.join(siteDir, 'sidebars.json');
const plugin = new DocusaurusPluginContentDocs( const plugin = pluginContentDocs(
{ {
siteDir, siteDir,
siteConfig, siteConfig,
@ -41,6 +41,7 @@ describe('loadDocs', () => {
title: 'Hello, World !', title: 'Hello, World !',
description: `Hi, Endilie here :)`, description: `Hi, Endilie here :)`,
}); });
expect(docsMetadata['foo/bar']).toEqual({ expect(docsMetadata['foo/bar']).toEqual({
category: 'Test', category: 'Test',
id: 'foo/bar', id: 'foo/bar',

View file

@ -25,166 +25,162 @@ const DEFAULT_OPTIONS = {
docItemComponent: '@theme/DocItem', docItemComponent: '@theme/DocItem',
}; };
class DocusaurusPluginContentDocs { module.exports = function(context, opts) {
constructor(context, opts) { const options = {...DEFAULT_OPTIONS, ...opts};
this.options = {...DEFAULT_OPTIONS, ...opts}; const contentPath = path.resolve(context.siteDir, options.path);
this.context = context; let globalContents = {};
this.contentPath = path.resolve(this.context.siteDir, this.options.path);
this.content = {};
}
getName() { return {
return 'docusaurus-plugin-content-docs'; name: 'docusaurus-plugin-content-docs',
}
getPathsToWatch() { contentPath,
const {include = []} = this.options;
const globPattern = include.map(
pattern => `${this.contentPath}/${pattern}`,
);
return [...globPattern, this.options.sidebarPath];
}
// Fetches blog contents and returns metadata for the contents. getPathsToWatch() {
async loadContent() { const {include = []} = options;
const {include, routeBasePath, sidebarPath} = this.options; const globPattern = include.map(pattern => `${contentPath}/${pattern}`);
const {siteConfig} = this.context; return [...globPattern, options.sidebarPath];
const docsDir = this.contentPath; },
if (!fs.existsSync(docsDir)) { // Fetches blog contents and returns metadata for the contents.
return null; async loadContent() {
} const {include, routeBasePath, sidebarPath} = options;
const {siteConfig} = context;
const docsDir = contentPath;
const docsSidebars = loadSidebars(sidebarPath); if (!fs.existsSync(docsDir)) {
return null;
// Build the docs ordering such as next, previous, category and sidebar
const order = createOrder(docsSidebars);
// Prepare metadata container.
const docs = {};
// Metadata for default docs files.
const docsFiles = await globby(include, {
cwd: docsDir,
});
await Promise.all(
docsFiles.map(async source => {
const metadata = await processMetadata(
source,
docsDir,
order,
siteConfig,
routeBasePath,
);
docs[metadata.id] = metadata;
}),
);
// Get the titles of the previous and next ids so that we can use them.
Object.keys(docs).forEach(currentID => {
const previousID = idx(docs, [currentID, 'previous']);
if (previousID) {
const previousTitle = idx(docs, [previousID, 'title']);
docs[currentID].previous_title = previousTitle || 'Previous';
} }
const nextID = idx(docs, [currentID, 'next']);
if (nextID) { const docsSidebars = loadSidebars(sidebarPath);
const nextTitle = idx(docs, [nextID, 'title']);
docs[currentID].next_title = nextTitle || 'Next'; // Build the docs ordering such as next, previous, category and sidebar
const order = createOrder(docsSidebars);
// Prepare metadata container.
const docs = {};
// Metadata for default docs files.
const docsFiles = await globby(include, {
cwd: docsDir,
});
await Promise.all(
docsFiles.map(async source => {
const metadata = await processMetadata(
source,
docsDir,
order,
siteConfig,
routeBasePath,
);
docs[metadata.id] = metadata;
}),
);
// Get the titles of the previous and next ids so that we can use them.
Object.keys(docs).forEach(currentID => {
const previousID = idx(docs, [currentID, 'previous']);
if (previousID) {
const previousTitle = idx(docs, [previousID, 'title']);
docs[currentID].previous_title = previousTitle || 'Previous';
}
const nextID = idx(docs, [currentID, 'next']);
if (nextID) {
const nextTitle = idx(docs, [nextID, 'title']);
docs[currentID].next_title = nextTitle || 'Next';
}
});
const sourceToPermalink = {};
const permalinkToId = {};
Object.values(docs).forEach(({id, source, permalink}) => {
sourceToPermalink[source] = permalink;
permalinkToId[permalink] = id;
});
globalContents = {
docs,
docsDir,
docsSidebars,
sourceToPermalink,
permalinkToId,
};
return globalContents;
},
async contentLoaded({content, actions}) {
if (!content) {
return;
} }
});
const sourceToPermalink = {}; const {docLayoutComponent, docItemComponent, routeBasePath} = options;
const permalinkToId = {}; const {addRoute, createData} = actions;
Object.values(docs).forEach(({id, source, permalink}) => {
sourceToPermalink[source] = permalink;
permalinkToId[permalink] = id;
});
this.content = { const routes = await Promise.all(
docs, Object.values(content.docs).map(async metadataItem => {
docsDir, const metadataPath = await createData(
docsSidebars, `${docuHash(metadataItem.permalink)}.json`,
sourceToPermalink, JSON.stringify(metadataItem, null, 2),
permalinkToId, );
}; return {
path: metadataItem.permalink,
component: docItemComponent,
exact: true,
modules: {
content: metadataItem.source,
metadata: metadataPath,
},
};
}),
);
return this.content; const docsBaseRoute = normalizeUrl([
} context.siteConfig.baseUrl,
routeBasePath,
]);
const docsMetadataPath = await createData(
`${docuHash(docsBaseRoute)}.json`,
JSON.stringify(content, null, 2),
);
async contentLoaded({content, actions}) { addRoute({
if (!content) { path: docsBaseRoute,
return; component: docLayoutComponent,
} routes,
const {docLayoutComponent, docItemComponent, routeBasePath} = this.options; modules: {
const {addRoute, createData} = actions; docsMetadata: docsMetadataPath,
},
});
},
const routes = await Promise.all( getThemePath() {
Object.values(content.docs).map(async metadataItem => { return path.resolve(__dirname, './theme');
const metadataPath = await createData( },
`${docuHash(metadataItem.permalink)}.json`,
JSON.stringify(metadataItem, null, 2),
);
return {
path: metadataItem.permalink,
component: docItemComponent,
exact: true,
modules: {
content: metadataItem.source,
metadata: metadataPath,
},
};
}),
);
const docsBaseRoute = normalizeUrl([ configureWebpack(config, isServer, {getBabelLoader, getCacheLoader}) {
this.context.siteConfig.baseUrl, return {
routeBasePath, module: {
]); rules: [
const docsMetadataPath = await createData( {
`${docuHash(docsBaseRoute)}.json`, test: /(\.mdx?)$/,
JSON.stringify(content, null, 2), include: [contentPath],
); use: [
getCacheLoader(isServer),
addRoute({ getBabelLoader(isServer),
path: docsBaseRoute, '@docusaurus/mdx-loader',
component: docLayoutComponent, {
routes, loader: path.resolve(__dirname, './markdown/index.js'),
modules: { options: {
docsMetadata: docsMetadataPath, siteConfig: context.siteConfig,
}, docsDir: globalContents.docsDir,
}); sourceToPermalink: globalContents.sourceToPermalink,
} },
getThemePath() {
return path.resolve(__dirname, './theme');
}
configureWebpack(config, isServer, {getBabelLoader, getCacheLoader}) {
return {
module: {
rules: [
{
test: /(\.mdx?)$/,
include: [this.contentPath],
use: [
getCacheLoader(isServer),
getBabelLoader(isServer),
'@docusaurus/mdx-loader',
{
loader: path.resolve(__dirname, './markdown/index.js'),
options: {
siteConfig: this.context.siteConfig,
docsDir: this.content.docsDir,
sourceToPermalink: this.content.sourceToPermalink,
}, },
}, ],
], },
}, ],
], },
}, };
}; },
} };
} };
module.exports = DocusaurusPluginContentDocs;

View file

@ -7,7 +7,7 @@
import path from 'path'; import path from 'path';
import DocusaurusPluginContentPages from '../index'; import pluginContentPages from '../index';
describe('docusaurus-plugin-content-pages', () => { describe('docusaurus-plugin-content-pages', () => {
test('simple pages', async () => { test('simple pages', async () => {
@ -17,7 +17,7 @@ describe('docusaurus-plugin-content-pages', () => {
url: 'https://docusaurus.io', url: 'https://docusaurus.io',
}; };
const siteDir = path.join(__dirname, '__fixtures__', 'website'); const siteDir = path.join(__dirname, '__fixtures__', 'website');
const plugin = new DocusaurusPluginContentPages({ const plugin = pluginContentPages({
siteDir, siteDir,
siteConfig, siteConfig,
}); });

View file

@ -16,75 +16,70 @@ const DEFAULT_OPTIONS = {
include: ['**/*.{js,jsx}'], // Extensions to include. include: ['**/*.{js,jsx}'], // Extensions to include.
}; };
class DocusaurusPluginContentPages { module.exports = function(context, opts) {
constructor(context, opts) { const options = {...DEFAULT_OPTIONS, ...opts};
this.options = {...DEFAULT_OPTIONS, ...opts}; const contentPath = path.resolve(context.siteDir, options.path);
this.context = context;
this.contentPath = path.resolve(this.context.siteDir, this.options.path);
}
getName() { return {
return 'docusaurus-plugin-content-pages'; name: 'docusaurus-plugin-content-pages',
}
getPathsToWatch() { contentPath,
const {include = []} = this.options;
const globPattern = include.map(
pattern => `${this.contentPath}/${pattern}`,
);
return [...globPattern];
}
async loadContent() { getPathsToWatch() {
const {include} = this.options; const {include = []} = options;
const {siteConfig} = this.context; const globPattern = include.map(pattern => `${contentPath}/${pattern}`);
const pagesDir = this.contentPath; return [...globPattern];
},
if (!fs.existsSync(pagesDir)) { async loadContent() {
return null; const {include} = options;
} const {siteConfig} = context;
const pagesDir = contentPath;
const {baseUrl} = siteConfig; if (!fs.existsSync(pagesDir)) {
const pagesFiles = await globby(include, { return null;
cwd: pagesDir, }
});
return pagesFiles.map(relativeSource => { const {baseUrl} = siteConfig;
const source = path.join(pagesDir, relativeSource); const pagesFiles = await globby(include, {
const pathName = encodePath(fileToPath(relativeSource)); cwd: pagesDir,
// Default Language. });
return {
permalink: pathName.replace(/^\//, baseUrl),
source,
};
});
}
async contentLoaded({content, actions}) { return pagesFiles.map(relativeSource => {
if (!content) { const source = path.join(pagesDir, relativeSource);
return; const pathName = encodePath(fileToPath(relativeSource));
} // Default Language.
return {
permalink: pathName.replace(/^\//, baseUrl),
source,
};
});
},
const {addRoute, createData} = actions; async contentLoaded({content, actions}) {
if (!content) {
return;
}
await Promise.all( const {addRoute, createData} = actions;
content.map(async metadataItem => {
const {permalink, source} = metadataItem;
const metadataPath = await createData(
`${docuHash(permalink)}.json`,
JSON.stringify(metadataItem, null, 2),
);
addRoute({
path: permalink,
component: source,
exact: true,
modules: {
metadata: metadataPath,
},
});
}),
);
}
}
module.exports = DocusaurusPluginContentPages; await Promise.all(
content.map(async metadataItem => {
const {permalink, source} = metadataItem;
const metadataPath = await createData(
`${docuHash(permalink)}.json`,
JSON.stringify(metadataItem, null, 2),
);
addRoute({
path: permalink,
component: source,
exact: true,
modules: {
metadata: metadataPath,
},
});
}),
);
},
};
};

View file

@ -0,0 +1,30 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import createSitemap from '../createSitemap';
describe('createSitemap', () => {
test('simple site', () => {
const sitemap = createSitemap({
siteConfig: {
url: 'https://example.com',
},
routesPaths: ['/', '/test'],
});
expect(sitemap).toContain(
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">`,
);
});
test('empty site', () => {
expect(() => {
createSitemap({});
}).toThrowErrorMatchingInlineSnapshot(
`"Url in docusaurus.config.js cannot be empty/undefined"`,
);
});
});

View file

@ -1,36 +0,0 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import DocusaurusPluginSitemap from '../index';
describe('docusaurus-plugin-sitemap', () => {
describe('createSitemap', () => {
test('simple site', async () => {
const context = {
siteConfig: {
url: 'https://example.com',
},
routesPaths: ['/', '/test'],
};
const plugin = new DocusaurusPluginSitemap(context, null);
const sitemap = await plugin.createSitemap(context);
expect(sitemap).toContain(
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">`,
);
});
test('empty site', async () => {
const context = {};
const plugin = new DocusaurusPluginSitemap(context, null);
expect(
plugin.createSitemap(context),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Url in docusaurus.config.js cannot be empty/undefined"`,
);
});
});
});

View file

@ -0,0 +1,33 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const sitemap = require('sitemap');
module.exports = function createSitemap({
siteConfig = {},
routesPaths,
options = {},
}) {
const {url: hostname} = siteConfig;
if (!hostname) {
throw new Error('Url in docusaurus.config.js cannot be empty/undefined');
}
const urls = routesPaths.map(routesPath => ({
url: routesPath,
changefreq: options.changefreq,
priority: options.priority,
}));
return sitemap
.createSitemap({
hostname,
cacheTime: options.cacheTime,
urls,
})
.toString();
};

View file

@ -6,61 +6,37 @@
*/ */
const fs = require('fs'); const fs = require('fs');
const sitemap = require('sitemap');
const path = require('path'); const path = require('path');
const createSitemap = require('./createSitemap');
const DEFAULT_OPTIONS = { const DEFAULT_OPTIONS = {
cacheTime: 600 * 1000, // 600 sec - cache purge period cacheTime: 600 * 1000, // 600 sec - cache purge period
changefreq: 'weekly', changefreq: 'weekly',
priority: 0.5, priority: 0.5,
}; };
class DocusaurusPluginSitemap { module.exports = function(context, opts) {
constructor(context, opts) { const options = {...DEFAULT_OPTIONS, ...opts};
this.options = {...DEFAULT_OPTIONS, ...opts};
this.context = context;
}
getName() { return {
return 'docusaurus-plugin-sitemap'; name: 'docusaurus-plugin-sitemap',
}
async createSitemap({siteConfig = {}, routesPaths}) { async postBuild({siteConfig = {}, routesPaths = [], outDir}) {
const {url: hostname} = siteConfig; // Generate sitemap
if (!hostname) { const generatedSitemap = createSitemap({
throw new Error(`Url in docusaurus.config.js cannot be empty/undefined`); siteConfig,
} routesPaths,
options,
}).toString();
const urls = routesPaths.map(routesPath => ({ // Write sitemap file
url: routesPath, const sitemapPath = path.join(outDir, 'sitemap.xml');
changefreq: this.changefreq, fs.writeFile(sitemapPath, generatedSitemap, err => {
priority: this.priority, if (err) {
})); throw new Error(`Sitemap error: ${err}`);
}
return sitemap });
.createSitemap({ },
hostname, };
cacheTime: this.cacheTime, };
urls,
})
.toString();
}
async postBuild({siteConfig = {}, routesPaths = [], outDir}) {
// Generate sitemap
const generatedSitemap = await this.createSitemap({
siteConfig,
routesPaths,
});
// Write sitemap file
const sitemapPath = path.join(outDir, 'sitemap.xml');
fs.writeFile(sitemapPath, generatedSitemap, err => {
if (err) {
throw new Error(`Sitemap error: ${err}`);
}
});
}
}
module.exports = DocusaurusPluginSitemap;

View file

@ -9,27 +9,27 @@ module.exports = function preset(context, opts = {}) {
return { return {
themes: [ themes: [
{ {
name: '@docusaurus/theme-classic', module: '@docusaurus/theme-classic',
}, },
{ {
name: '@docusaurus/theme-search-algolia', module: '@docusaurus/theme-search-algolia',
}, },
], ],
plugins: [ plugins: [
{ {
name: '@docusaurus/plugin-content-docs', module: '@docusaurus/plugin-content-docs',
options: opts.docs, options: opts.docs,
}, },
{ {
name: '@docusaurus/plugin-content-blog', module: '@docusaurus/plugin-content-blog',
options: opts.blog, options: opts.blog,
}, },
{ {
name: '@docusaurus/plugin-content-pages', module: '@docusaurus/plugin-content-pages',
options: opts.pages, options: opts.pages,
}, },
{ {
name: '@docusaurus/plugin-sitemap', module: '@docusaurus/plugin-sitemap',
options: opts.sitemap, options: opts.sitemap,
}, },
], ],

View file

@ -7,21 +7,12 @@
const path = require('path'); const path = require('path');
const DEFAULT_OPTIONS = {}; module.exports = function() {
return {
name: 'docusaurus-theme-classic',
class DocusaurusThemeClassic { getThemePath() {
constructor(context, opts) { return path.resolve(__dirname, './theme');
this.options = {...DEFAULT_OPTIONS, ...opts}; },
this.context = context; };
} };
getName() {
return 'docusaurus-theme-classic';
}
getThemePath() {
return path.resolve(__dirname, './theme');
}
}
module.exports = DocusaurusThemeClassic;

View file

@ -7,21 +7,12 @@
const path = require('path'); const path = require('path');
const DEFAULT_OPTIONS = {}; module.exports = function() {
return {
name: 'docusaurus-theme-search-algolia',
class DocusaurusThemeSearchAlgolia { getThemePath() {
constructor(context, opts) { return path.resolve(__dirname, './theme');
this.options = {...DEFAULT_OPTIONS, ...opts}; },
this.context = context; };
} };
getName() {
return 'docusaurus-theme-search-algolia';
}
getThemePath() {
return path.resolve(__dirname, './theme');
}
}
module.exports = DocusaurusThemeSearchAlgolia;

View file

@ -1,4 +1,12 @@
# Breaking Changes # Docusaurus 2 Changelog
## 2.0.0-alpha.19
- Changed plugin definitions from classes to functions. Refer to the new plugin docs.
- Added sun and moon emoji to the dark mode toggle.
- Add a sensible default for browserslist config.
## V2 Changelog
### `siteConfig.js` changes ### `siteConfig.js` changes

View file

@ -15,10 +15,9 @@ export async function swizzle(
themeName: string, themeName: string,
componentName?: string, componentName?: string,
): Promise<void> { ): Promise<void> {
const Plugin = importFresh(themeName); const plugin = importFresh(themeName);
const context = {siteDir}; const pluginInstance = plugin({siteDir});
const PluginInstance = new Plugin(context); let fromPath = pluginInstance.getThemePath();
let fromPath = PluginInstance.getThemePath();
if (fromPath) { if (fromPath) {
let toPath = path.resolve(siteDir, 'theme'); let toPath = path.resolve(siteDir, 'theme');

View file

@ -15,14 +15,14 @@ module.exports = {
favicon: 'img/docusaurus.ico', favicon: 'img/docusaurus.ico',
plugins: [ plugins: [
{ {
name: '@docusaurus/plugin-content-docs', module: '@docusaurus/plugin-content-docs',
options: { options: {
path: '../docs', path: '../docs',
sidebarPath: require.resolve('./sidebars.json'), sidebarPath: require.resolve('./sidebars.json'),
}, },
}, },
{ {
name: '@docusaurus/plugin-content-pages', module: '@docusaurus/plugin-content-pages',
}, },
], ],
}; };

View file

@ -15,14 +15,14 @@ module.exports = {
favicon: 'img/docusaurus.ico', favicon: 'img/docusaurus.ico',
plugins: [ plugins: [
{ {
name: '@docusaurus/plugin-content-docs', module: '@docusaurus/plugin-content-docs',
options: { options: {
path: '../docs', path: '../docs',
sidebarPath: require.resolve('./sidebars.json'), sidebarPath: require.resolve('./sidebars.json'),
}, },
}, },
{ {
name: '@docusaurus/plugin-content-pages', module: '@docusaurus/plugin-content-pages',
}, },
], ],
}; };

View file

@ -19,14 +19,10 @@ export async function loadPlugins({
context: LoadContext; context: LoadContext;
}) { }) {
// 1. Plugin Lifecycle - Initialization/Constructor // 1. Plugin Lifecycle - Initialization/Constructor
const plugins = pluginConfigs.map(({name, path: pluginPath, options}) => { const plugins = pluginConfigs.map(({module, options}) => {
let Plugin; // module is any valid module identifier - npm package or locally-resolved path.
if (pluginPath && fs.existsSync(pluginPath)) { const plugin = importFresh(module);
Plugin = importFresh(pluginPath); return plugin(context, options);
} else {
Plugin = importFresh(name);
}
return new Plugin(context, options);
}); });
// 2. Plugin lifecycle - loadContent // 2. Plugin lifecycle - loadContent
@ -51,8 +47,11 @@ export async function loadPlugins({
if (!plugin.contentLoaded) { if (!plugin.contentLoaded) {
return; return;
} }
const pluginName = plugin.getName();
const pluginContentDir = path.join(context.generatedFilesDir, pluginName); const pluginContentDir = path.join(
context.generatedFilesDir,
plugin.name,
);
const actions = { const actions = {
addRoute: config => pluginsRouteConfigs.push(config), addRoute: config => pluginsRouteConfigs.push(config),
createData: async (name, content) => { createData: async (name, content) => {

View file

@ -3,7 +3,7 @@ id: plugins-api
title: Plugins title: Plugins
--- ---
Plugins are one of the best ways to add functionality to our Docusaurus. Plugins allow third-party developers to extend or modify the default functionality that Docusaurus provides. Plugins are one of the best ways to add functionality to our Docusaurus. Plugins allow third-party developers to extend or modify the default functionality that Docusaurus provides.
## Installing a Plugin ## Installing a Plugin
@ -19,11 +19,11 @@ Then you add it in your site's `docusaurus.config.js` plugin arrays:
module.exports = { module.exports = {
plugins: [ plugins: [
{ {
name: '@docusaurus/plugin-content-pages', module: '@docusaurus/plugin-content-pages',
}, },
{ {
// Plugin with options // Plugin with options
name: '@docusaurus/plugin-content-blog', module: '@docusaurus/plugin-content-blog',
options: { options: {
include: ['*.md', '*.mdx'], include: ['*.md', '*.mdx'],
path: 'blog', path: 'blog',
@ -33,81 +33,84 @@ module.exports = {
}; };
``` ```
Docusaurus can also load plugins from your local folder, you can do something like below: Docusaurus can also load plugins from your local directory, you can do something like the following:
```js ```js
const path = require('path');
module.exports = { module.exports = {
plugins: [ plugins: [
{ {
path: '/path/to/docusaurus-local-plugin', module: path.resolve(__dirname, '/path/to/docusaurus-local-plugin'),
}, },
], ],
} };
``` ```
## Basic Plugin Architecture ## Basic Plugin Definition
Plugins are modules which export a function that takes in the context, options and returns a plain JavaScript object that has some properties defined.
For examples, please refer to several official plugins created. For examples, please refer to several official plugins created.
```js ```js
// A JavaScript class const DEFAULT_OPTIONS = {
class DocusaurusPlugin { // Some defaults.
constructor(context, options) { };
// Initialization hook
// options are the plugin options set on config file
this.options = {...options};
// context are provided from docusaurus. Example: siteConfig can be accessed from context
this.context = context;
}
getName() { // A JavaScript function that returns an object.
// plugin name identifier // `context` is provided by Docusaurus. Example: siteConfig can be accessed from context.
} // `opts` is the user-defined options.
module.exports = function(context, opts) {
// Merge defaults with user-defined options.
const options = {...DEFAULT_OPTIONS, ...options};
return {
// Namespace used for directories to cache the intermediate data for each plugin.
name: 'docusaurus-cool-plugin',
async loadContent()) { async loadContent() {
// The loadContent hook is executed after siteConfig and env has been loaded // The loadContent hook is executed after siteConfig and env has been loaded
// You can return a JavaScript object that will be passed to contentLoaded hook // You can return a JavaScript object that will be passed to contentLoaded hook
} },
async contentLoaded({content, actions}) { async contentLoaded({content, actions}) {
// contentLoaded hook is done after loadContent hook is done // contentLoaded hook is done after loadContent hook is done
// actions are set of functional API provided by Docusaurus. e.g: addRoute // actions are set of functional API provided by Docusaurus. e.g: addRoute
} },
async postBuild(props) { async postBuild(props) {
// after docusaurus <build> finish // after docusaurus <build> finish
} },
// TODO // TODO
async postStart(props) { async postStart(props) {
// docusaurus <start> finish // docusaurus <start> finish
} },
// TODO // TODO
afterDevServer(app, server) { afterDevServer(app, server) {
// https://webpack.js.org/configuration/dev-server/#devserverbefore // https://webpack.js.org/configuration/dev-server/#devserverbefore
} },
// TODO // TODO
beforeDevServer(app, server) { beforeDevServer(app, server) {
// https://webpack.js.org/configuration/dev-server/#devserverafter // https://webpack.js.org/configuration/dev-server/#devserverafter
},
} configureWebpack(config, isServer) {
// Modify internal webpack config. If returned value is an Object, it
// will be merged into the final config using webpack-merge;
// If the returned value is a function, it will receive the config as the 1st argument and an isServer flag as the 2nd argument.
},
configureWebpack(config, isServer) { getPathsToWatch() {
// Modify internal webpack config. If returned value is an Object, it will be merged into the final config using webpack-merge; If returned value is a function, it will receive the config as the 1st argument and an isServer flag as the 2nd argument. // Path to watch
} },
};
getPathsToWatch() { };
// path to watch
}
}
``` ```
#### References #### References
- https://v1.vuepress.vuejs.org/plugin/option-api.html - https://v1.vuepress.vuejs.org/plugin/option-api.html