feat(v2): docs plugin initial work (#1327)

* feat(v2): pluginify docs

* feat(v2): implement docs plugin

* fix(v2): fix bugs in docs plugin for translation and versioning
This commit is contained in:
Yangshun Tay 2019-03-31 11:37:35 -07:00 committed by GitHub
parent c33e874e1c
commit a70d9b6720
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 576 additions and 371 deletions

View file

@ -10,12 +10,13 @@ const path = require('path');
const fs = require('fs-extra');
const {parse, idx, normalizeUrl, generate} = require('@docusaurus/utils');
// TODO: Use a better slugify function that doesn't rely on a specific file extension.
function fileToUrl(fileName) {
return fileName
.replace('-', '/')
.replace('-', '/')
.replace('-', '/')
.replace(/\.md$/, '');
.replace(/\.mdx?$/, '');
}
const DEFAULT_OPTIONS = {
@ -42,6 +43,10 @@ class DocusaurusPluginContentBlog {
return 'docusaurus-plugin-content-blog';
}
getPathsToWatch() {
return [this.contentPath];
}
// Fetches blog contents and returns metadata for the contents.
async loadContent() {
const {pageCount, include, routeBasePath} = this.options;
@ -150,10 +155,6 @@ class DocusaurusPluginContentBlog {
});
});
}
getPathsToWatch() {
return [this.contentPath];
}
}
module.exports = DocusaurusPluginContentBlog;

View file

@ -6,7 +6,6 @@
"license": "MIT",
"dependencies": {
"@docusaurus/utils": "^1.0.0",
"classnames": "^2.2.6",
"fs-extra": "^7.0.1",
"globby": "^9.1.0"
},

View file

@ -7,15 +7,26 @@
import '@babel/polyfill';
import path from 'path';
import loadDocs from '@lib/load/docs';
import loadSetup from '../../loadSetup';
import loadSetup from '../../docusaurus/test/loadSetup';
import DocusaurusPluginContentDocs from '../index';
describe('loadDocs', () => {
test('simple website', async () => {
const props = await loadSetup('simple');
const {siteDir, docsDir, env, siteConfig} = props;
const {docsMetadatas} = await loadDocs({siteDir, docsDir, env, siteConfig});
expect(docsMetadatas.hello).toEqual({
test.only('simple website', async () => {
const {env, siteDir, siteConfig} = await loadSetup('simple');
const plugin = new DocusaurusPluginContentDocs(
{
path: '../docs',
},
{
env,
siteDir,
siteConfig,
},
);
const {docs: docsMetadata} = await plugin.loadContent();
const docsDir = plugin.contentPath;
expect(docsMetadata.hello).toEqual({
category: 'Guides',
id: 'hello',
language: null,
@ -29,7 +40,7 @@ describe('loadDocs', () => {
title: 'Hello, World !',
version: null,
});
expect(docsMetadatas['foo/bar']).toEqual({
expect(docsMetadata['foo/bar']).toEqual({
category: 'Test',
id: 'foo/bar',
language: null,
@ -46,10 +57,23 @@ describe('loadDocs', () => {
});
test('versioned website', async () => {
const props = await loadSetup('versioned');
const {siteDir, docsDir, versionedDir, env, siteConfig} = props;
const {docsMetadatas} = await loadDocs({siteDir, docsDir, env, siteConfig});
expect(docsMetadatas['version-1.0.0-foo/bar']).toEqual({
const {env, siteDir, siteConfig, versionedDir} = await loadSetup(
'versioned',
);
const plugin = new DocusaurusPluginContentDocs(
{
path: '../docs',
},
{
env,
siteDir,
siteConfig,
},
);
const {docs: docsMetadata} = await plugin.loadContent();
const docsDir = plugin.contentPath;
expect(docsMetadata['version-1.0.0-foo/bar']).toEqual({
category: 'Test',
id: 'version-1.0.0-foo/bar',
language: null,
@ -63,7 +87,7 @@ describe('loadDocs', () => {
title: 'Bar',
version: '1.0.0',
});
expect(docsMetadatas['foo/bar']).toEqual({
expect(docsMetadata['foo/bar']).toEqual({
category: 'Test',
id: 'foo/bar',
language: null,
@ -80,17 +104,27 @@ describe('loadDocs', () => {
});
test('versioned & translated website', async () => {
const props = await loadSetup('transversioned');
const {
siteDir,
docsDir,
env,
siteDir,
siteConfig,
translatedDir,
versionedDir,
} = await loadSetup('transversioned');
const plugin = new DocusaurusPluginContentDocs(
{
path: '../docs',
},
{
env,
siteDir,
siteConfig,
} = props;
const {docsMetadatas} = await loadDocs({siteDir, docsDir, env, siteConfig});
expect(docsMetadatas['ko-version-1.0.0-foo/bar']).toEqual({
},
);
const {docs: docsMetadata} = await plugin.loadContent();
const docsDir = plugin.contentPath;
expect(docsMetadata['ko-version-1.0.0-foo/bar']).toEqual({
category: 'Test',
id: 'ko-version-1.0.0-foo/bar',
language: 'ko',
@ -104,7 +138,7 @@ describe('loadDocs', () => {
title: 'Bar',
version: '1.0.0',
});
expect(docsMetadatas['en-version-1.0.0-foo/baz']).toEqual({
expect(docsMetadata['en-version-1.0.0-foo/baz']).toEqual({
category: 'Test',
id: 'en-version-1.0.0-foo/baz',
language: 'en',
@ -121,7 +155,7 @@ describe('loadDocs', () => {
title: 'Baz',
version: '1.0.0',
});
expect(docsMetadatas['en-hello']).toEqual({
expect(docsMetadata['en-hello']).toEqual({
category: 'Guides',
id: 'en-hello',
language: 'en',
@ -138,10 +172,23 @@ describe('loadDocs', () => {
});
test('translated website', async () => {
const props = await loadSetup('translated');
const {siteDir, translatedDir, docsDir, env, siteConfig} = props;
const {docsMetadatas} = await loadDocs({siteDir, docsDir, env, siteConfig});
expect(docsMetadatas['ko-foo/baz']).toEqual({
const {env, siteDir, siteConfig, translatedDir} = await loadSetup(
'translated',
);
const plugin = new DocusaurusPluginContentDocs(
{
path: '../docs',
},
{
env,
siteDir,
siteConfig,
},
);
const {docs: docsMetadata} = await plugin.loadContent();
const docsDir = plugin.contentPath;
expect(docsMetadata['ko-foo/baz']).toEqual({
category: 'Test',
id: 'ko-foo/baz',
language: 'ko',
@ -158,7 +205,7 @@ describe('loadDocs', () => {
title: 'baz',
version: null,
});
expect(docsMetadatas['en-foo/bar']).toEqual({
expect(docsMetadata['en-foo/bar']).toEqual({
category: 'Test',
id: 'en-foo/bar',
language: 'en',
@ -175,16 +222,23 @@ describe('loadDocs', () => {
});
test('versioned website with skip next release', async () => {
const props = await loadSetup('versioned');
const {siteDir, docsDir, versionedDir, env, siteConfig} = props;
const {docsMetadatas} = await loadDocs({
siteDir,
docsDir,
const {env, siteDir, siteConfig, versionedDir} = await loadSetup(
'versioned',
);
const plugin = new DocusaurusPluginContentDocs(
{
path: '../docs',
},
{
env,
siteDir,
siteConfig,
skipNextRelease: true,
});
expect(docsMetadatas['version-1.0.0-foo/bar']).toEqual({
cliOptions: {skipNextRelease: true},
},
);
const {docs: docsMetadata} = await plugin.loadContent();
expect(docsMetadata['version-1.0.0-foo/bar']).toEqual({
category: 'Test',
id: 'version-1.0.0-foo/bar',
language: null,
@ -198,6 +252,6 @@ describe('loadDocs', () => {
title: 'Bar',
version: '1.0.0',
});
expect(docsMetadatas['foo/bar']).toBeUndefined();
expect(docsMetadata['foo/bar']).toBeUndefined();
});
});

View file

@ -7,8 +7,8 @@
import '@babel/polyfill';
import path from 'path';
import processMetadata from '@lib/load/docs/metadata';
import loadSetup from '../../loadSetup';
import processMetadata from '../src/metadata';
import loadSetup from '../../docusaurus/test/loadSetup';
describe('processMetadata', () => {
test('normal docs', async () => {
@ -16,8 +16,22 @@ describe('processMetadata', () => {
const {docsDir, env, siteConfig} = props;
const sourceA = path.join('foo', 'bar.md');
const sourceB = path.join('hello.md');
const dataA = await processMetadata(sourceA, docsDir, env, {}, siteConfig);
const dataB = await processMetadata(sourceB, docsDir, env, {}, siteConfig);
const dataA = await processMetadata(
sourceA,
docsDir,
env,
{},
siteConfig,
'docs',
);
const dataB = await processMetadata(
sourceB,
docsDir,
env,
{},
siteConfig,
'docs',
);
expect(dataA).toEqual({
id: 'foo/bar',
language: null,
@ -42,7 +56,14 @@ describe('processMetadata', () => {
const props = await loadSetup('simple');
const {docsDir, env, siteConfig} = props;
const source = path.join('permalink.md');
const data = await processMetadata(source, docsDir, env, {}, siteConfig);
const data = await processMetadata(
source,
docsDir,
env,
{},
siteConfig,
'docs',
);
expect(data).toEqual({
id: 'permalink',
language: null,
@ -68,6 +89,7 @@ describe('processMetadata', () => {
env,
{},
siteConfig,
'docs',
);
const dataB = await processMetadata(
sourceB,
@ -75,9 +97,24 @@ describe('processMetadata', () => {
env,
{},
siteConfig,
'docs',
);
const dataC = await processMetadata(
sourceC,
docsDir,
env,
{},
siteConfig,
'docs',
);
const dataD = await processMetadata(
sourceD,
docsDir,
env,
{},
siteConfig,
'docs',
);
const dataC = await processMetadata(sourceC, docsDir, env, {}, siteConfig);
const dataD = await processMetadata(sourceD, docsDir, env, {}, siteConfig);
expect(dataA).toEqual({
id: 'version-1.0.0-foo/bar',
language: null,
@ -133,6 +170,7 @@ describe('processMetadata', () => {
env,
{},
siteConfig,
'docs',
);
const dataB = await processMetadata(
sourceB,
@ -140,6 +178,7 @@ describe('processMetadata', () => {
env,
{},
siteConfig,
'docs',
);
const dataC = await processMetadata(
sourceC,
@ -147,6 +186,7 @@ describe('processMetadata', () => {
env,
{},
siteConfig,
'docs',
);
const dataD = await processMetadata(
sourceD,
@ -154,15 +194,31 @@ describe('processMetadata', () => {
env,
{},
siteConfig,
'docs',
);
const dataE = await processMetadata(
sourceE,
docsDir,
env,
{},
siteConfig,
'docs',
);
const dataF = await processMetadata(
sourceF,
docsDir,
env,
{},
siteConfig,
'docs',
);
const dataE = await processMetadata(sourceE, docsDir, env, {}, siteConfig);
const dataF = await processMetadata(sourceF, docsDir, env, {}, siteConfig);
const dataG = await processMetadata(
sourceG,
versionedDir,
env,
{},
siteConfig,
'docs',
);
const dataH = await processMetadata(
sourceH,
@ -170,6 +226,7 @@ describe('processMetadata', () => {
env,
{},
siteConfig,
'docs',
);
expect(dataA).toEqual({
id: 'ko-version-1.0.0-foo/bar',
@ -258,6 +315,7 @@ describe('processMetadata', () => {
env,
{},
siteConfig,
'docs',
);
const dataB = await processMetadata(
sourceB,
@ -265,9 +323,24 @@ describe('processMetadata', () => {
env,
{},
siteConfig,
'docs',
);
const dataC = await processMetadata(
sourceC,
docsDir,
env,
{},
siteConfig,
'docs',
);
const dataD = await processMetadata(
sourceD,
docsDir,
env,
{},
siteConfig,
'docs',
);
const dataC = await processMetadata(sourceC, docsDir, env, {}, siteConfig);
const dataD = await processMetadata(sourceD, docsDir, env, {}, siteConfig);
expect(dataA).toEqual({
id: 'ko-foo/bar',
language: 'ko',

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import createOrder from '@lib/load/docs/order';
import createOrder from '../src/order';
describe('createOrder', () => {
test('multiple sidebars with subcategory', () => {

View file

@ -6,8 +6,8 @@
*/
import path from 'path';
import loadSidebars from '@lib/load/docs/sidebars';
import loadSetup from '../../loadSetup';
import loadSidebars from '../src/sidebars';
import loadSetup from '../../docusaurus/test/loadSetup';
describe('loadSidebars', () => {
const fixtures = path.join(__dirname, '..', '__fixtures__');

View file

@ -0,0 +1,210 @@
/**
* 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 path = require('path');
const globby = require('globby');
const {getSubFolder, idx, normalizeUrl} = require('@docusaurus/utils');
const createOrder = require('./src/order');
const loadSidebars = require('./src/sidebars');
const processMetadata = require('./src/metadata');
const DEFAULT_OPTIONS = {
metadataKey: 'docsMetadata',
metadataFileName: 'docsMetadata.json',
path: 'docs', // Path to data on filesystem, relative to site dir.
routeBasePath: 'docs', // URL Route.
include: ['**/*.md', '**/*.mdx'], // Extensions to include.
// TODO: Read from props rather than hardcoded sidebar.json.
sidebar: [], // Sidebar configuration for showing a list of documentation pages.
// TODO: Settle themeing.
docLayoutComponent: '@theme/Doc',
docItemComponent: '@theme/DocBody',
};
class DocusaurusPluginContentDocs {
constructor(opts, context) {
this.options = {...DEFAULT_OPTIONS, ...opts};
this.context = context;
this.contentPath = path.resolve(this.context.siteDir, this.options.path);
}
getName() {
return 'docusaurus-plugin-content-docs';
}
getPathsToWatch() {
return [this.contentPath];
}
// Fetches blog contents and returns metadata for the contents.
async loadContent() {
const {include, routeBasePath} = this.options;
const {siteDir, env, siteConfig, cliOptions = {}} = this.context;
const {skipNextRelease} = cliOptions;
const docsDir = this.contentPath;
// @tested - load all sidebars including versioned sidebars
const docsSidebars = loadSidebars({siteDir, env});
// @tested - build the docs ordering such as next, previous, category and sidebar
const order = createOrder(docsSidebars);
// Settle versions & translations from environment.
const translationEnabled = idx(env, ['translation', 'enabled']);
const enabledLanguages =
translationEnabled && idx(env, ['translation', 'enabledLanguages']);
const enabledLangTags =
(enabledLanguages && enabledLanguages.map(lang => lang.tag)) || [];
const defaultLangTag = idx(env, ['translation', 'defaultLanguage', 'tag']);
const versioningEnabled = idx(env, ['versioning', 'enabled']);
const versions =
(versioningEnabled && idx(env, ['versioning', 'versions'])) || [];
// Prepare metadata container.
const docs = {};
if (!(versioningEnabled && skipNextRelease)) {
// Metadata for default docs files.
const docsFiles = await globby(include, {
cwd: docsDir,
});
await Promise.all(
docsFiles.map(async source => {
// Do not allow reserved version/ translated folder name in 'docs'
// e.g: 'docs/version-1.0.0/' should not be allowed as it can cause unwanted bug
const subFolder = getSubFolder(
path.resolve(docsDir, source),
docsDir,
);
const versionsFolders = versions.map(version => `version-${version}`);
if ([...enabledLangTags, ...versionsFolders].includes(subFolder)) {
throw new Error(
`You cannot have a folder named 'docs/${subFolder}/'`,
);
}
const metadata = await processMetadata(
source,
docsDir,
env,
order,
siteConfig,
routeBasePath,
);
docs[metadata.id] = metadata;
}),
);
}
// Metadata for non-default-language docs.
if (translationEnabled) {
const translatedDir = path.join(siteDir, 'translated_docs');
const translatedFiles = await globby(include, {
cwd: translatedDir,
});
await Promise.all(
translatedFiles.map(async source => {
/*
Do not process disabled & default languages folder in `translated_docs`
e.g: 'translated_docs/ja/**' should not be processed if lang 'ja' is disabled
*/
const translatedFilePath = path.resolve(translatedDir, source);
const detectedLangTag = getSubFolder(
translatedFilePath,
translatedDir,
);
if (
detectedLangTag === defaultLangTag ||
!enabledLangTags.includes(detectedLangTag)
) {
return;
}
const metadata = await processMetadata(
source,
translatedDir,
env,
order,
siteConfig,
routeBasePath,
);
docs[metadata.id] = metadata;
}),
);
}
// Metadata for versioned docs.
if (versioningEnabled) {
const versionedDir = path.join(siteDir, 'versioned_docs');
const versionedFiles = await globby(include, {
cwd: versionedDir,
});
await Promise.all(
versionedFiles.map(async source => {
const metadata = await processMetadata(
source,
versionedDir,
env,
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';
}
});
// Create source to metadata mapping.
const sourceToMetadata = {};
Object.values(docs).forEach(({source, version, permalink, language}) => {
sourceToMetadata[source] = {
version,
permalink,
language,
};
});
return {
docs,
docsDir,
docsSidebars,
sourceToMetadata,
};
}
async contentLoaded({content, actions}) {
const {docLayoutComponent, docItemComponent, routeBasePath} = this.options;
const {addRoute} = actions;
addRoute({
path: normalizeUrl([this.context.siteConfig.baseUrl, routeBasePath]),
component: docLayoutComponent,
routes: Object.values(content.docs).map(metadataItem => ({
path: metadataItem.permalink,
component: docItemComponent,
metadata: metadataItem,
modules: [metadataItem.source],
})),
});
}
}
module.exports = DocusaurusPluginContentDocs;

View file

@ -0,0 +1,16 @@
{
"name": "@docusaurus/plugin-content-docs",
"version": "1.0.0",
"description": "Documentation plugin for Docusaurus",
"main": "index.js",
"license": "MIT",
"dependencies": {
"@babel/polyfill": "^7.4.0",
"@docusaurus/utils": "^1.0.0",
"fs-extra": "^7.0.1",
"globby": "^9.1.0"
},
"peerDependencies": {
"@docusaurus/core": "^2.0.0"
}
}

View file

@ -56,6 +56,7 @@ module.exports = async function processMetadata(
env,
order,
siteConfig,
docsBasePath,
) {
const filepath = path.resolve(refDir, source);
const fileString = await fs.readFile(filepath, 'utf-8');
@ -122,7 +123,7 @@ module.exports = async function processMetadata(
metadata.source = path.join(refDir, source);
// Build the permalink.
const {baseUrl, docsUrl} = siteConfig;
const {baseUrl} = siteConfig;
// If user has own custom permalink defined in frontmatter
// e.g: :baseUrl:docsUrl/:langPart/:versionPart/endiliey/:id
@ -130,7 +131,7 @@ module.exports = async function processMetadata(
metadata.permalink = path.resolve(
metadata.permalink
.replace(/:baseUrl/, baseUrl)
.replace(/:docsUrl/, docsUrl)
.replace(/:docsUrl/, docsBasePath)
.replace(/:langPart/, langPart)
.replace(/:versionPart/, versionPart)
.replace(/:id/, metadata.id),
@ -138,7 +139,7 @@ module.exports = async function processMetadata(
} else {
metadata.permalink = normalizeUrl([
baseUrl,
docsUrl,
docsBasePath,
langPart,
versionPart,
metadata.id,

View file

@ -7,14 +7,12 @@
const globby = require('globby');
const path = require('path');
// TODO: Do not make it relative because plugins can be from node_modules.
const {encodePath, fileToPath, idx} = require('@docusaurus/utils');
const DEFAULT_OPTIONS = {
metadataKey: 'pagesMetadata',
metadataFileName: 'pagesMetadata.json',
path: 'pages', // Path to data on filesystem.
path: 'pages', // Path to data on filesystem, relative to site dir.
routeBasePath: '', // URL Route.
include: ['**/*.{js,jsx}'], // Extensions to include.
component: '@theme/Pages',
@ -31,6 +29,10 @@ class DocusaurusPluginContentPages {
return 'docusaurus-plugin-content-pages';
}
getPathsToWatch() {
return [this.contentPath];
}
async loadContent() {
const {include} = this.options;
const {env, siteConfig} = this.context;
@ -102,10 +104,6 @@ class DocusaurusPluginContentPages {
});
});
}
getPathsToWatch() {
return [this.contentPath];
}
}
module.exports = DocusaurusPluginContentPages;

View file

@ -1,3 +1,8 @@
# Breaking Changes
### `siteConfig.js` changes
- `siteConfig.js` renamed to `docusaurus.config.js`.
- Removed the following config options:
- `docsUrl`. Use the plugin option on `docusaurus-plugin-content-blog` instead.
- `customDocsPath`. Use the plugin option on `docusaurus-plugin-content-blog` instead.

View file

@ -46,7 +46,6 @@ module.exports = async function start(siteDir, cliOptions = {}) {
});
};
const {plugins} = props;
const docsRelativeDir = props.siteConfig.customDocsPath;
const pluginPaths = _.compact(
_.flatten(
plugins.map(
@ -55,12 +54,7 @@ module.exports = async function start(siteDir, cliOptions = {}) {
),
);
const fsWatcher = chokidar.watch(
[
...pluginPaths,
`../${docsRelativeDir}/**/*.md`,
loadConfig.configFileName,
'sidebars.json',
],
[...pluginPaths, loadConfig.configFileName, 'sidebars.json'],
{
cwd: siteDir,
ignoreInitial: true,

View file

@ -25,11 +25,9 @@ const REQUIRED_FIELDS = [
const OPTIONAL_FIELDS = [
'algolia',
'customDocsPath',
'customFields',
'defaultLanguage',
'disableHeaderTitle',
'docsUrl',
'githubHost',
'highlight',
'markdownPlugins',
@ -37,8 +35,7 @@ const OPTIONAL_FIELDS = [
];
const DEFAULT_CONFIG = {
customDocsPath: 'docs',
docsUrl: 'docs',
plugins: [],
};
function formatFields(fields) {

View file

@ -1,144 +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.
*/
const path = require('path');
const globby = require('globby');
const {getSubFolder, idx} = require('@docusaurus/utils');
const createOrder = require('./order');
const loadSidebars = require('./sidebars');
const processMetadata = require('./metadata');
async function loadDocs({
siteDir,
docsDir,
env,
siteConfig,
skipNextRelease = false,
}) {
// @tested - load all sidebars including versioned sidebars
const docsSidebars = loadSidebars({siteDir, env});
// @tested - build the docs ordering such as next, previous, category and sidebar
const order = createOrder(docsSidebars);
// Settle versions & translations from environment.
const translationEnabled = idx(env, ['translation', 'enabled']);
const enabledLanguages =
translationEnabled && idx(env, ['translation', 'enabledLanguages']);
const enabledLangTags =
(enabledLanguages && enabledLanguages.map(lang => lang.tag)) || [];
const defaultLangTag = idx(env, ['translation', 'defaultLanguage', 'tag']);
const versioningEnabled = idx(env, ['versioning', 'enabled']);
const versions =
(versioningEnabled && idx(env, ['versioning', 'versions'])) || [];
// Prepare metadata container.
const docsMetadatas = {};
if (!(versioningEnabled && skipNextRelease)) {
// Metadata for default docs files.
const docsFiles = await globby(['**/*.md'], {
cwd: docsDir,
});
await Promise.all(
docsFiles.map(async source => {
// Do not allow reserved version/ translated folder name in 'docs'
// e.g: 'docs/version-1.0.0/' should not be allowed as it can cause unwanted bug
const subFolder = getSubFolder(path.resolve(docsDir, source), docsDir);
const versionsFolders = versions.map(version => `version-${version}`);
if ([...enabledLangTags, ...versionsFolders].includes(subFolder)) {
throw new Error(
`You cannot have a folder named 'docs/${subFolder}/'`,
);
}
const metadata = await processMetadata(
source,
docsDir,
env,
order,
siteConfig,
);
docsMetadatas[metadata.id] = metadata;
}),
);
}
// Metadata for non-default-language docs.
if (translationEnabled) {
const translatedDir = path.join(siteDir, 'translated_docs');
const translatedFiles = await globby(['**/*.md'], {
cwd: translatedDir,
});
await Promise.all(
translatedFiles.map(async source => {
/*
Do not process disabled & default languages folder in `translated_docs`
e.g: 'translated_docs/ja/**' should not be processed if lang 'ja' is disabled
*/
const translatedFilePath = path.resolve(translatedDir, source);
const detectedLangTag = getSubFolder(translatedFilePath, translatedDir);
if (
detectedLangTag === defaultLangTag ||
!enabledLangTags.includes(detectedLangTag)
) {
return;
}
const metadata = await processMetadata(
source,
translatedDir,
env,
order,
siteConfig,
);
docsMetadatas[metadata.id] = metadata;
}),
);
}
// Metadata for versioned docs.
if (versioningEnabled) {
const versionedDir = path.join(siteDir, 'versioned_docs');
const versionedFiles = await globby(['**/*.md'], {
cwd: versionedDir,
});
await Promise.all(
versionedFiles.map(async source => {
const metadata = await processMetadata(
source,
versionedDir,
env,
order,
siteConfig,
);
docsMetadatas[metadata.id] = metadata;
}),
);
}
// Get the titles of the previous and next ids so that we can use them.
Object.keys(docsMetadatas).forEach(currentID => {
const previousID = idx(docsMetadatas, [currentID, 'previous']);
if (previousID) {
const previousTitle = idx(docsMetadatas, [previousID, 'title']);
docsMetadatas[currentID].previous_title = previousTitle || 'Previous';
}
const nextID = idx(docsMetadatas, [currentID, 'next']);
if (nextID) {
const nextTitle = idx(docsMetadatas, [nextID, 'title']);
docsMetadatas[currentID].next_title = nextTitle || 'Next';
}
});
return {
docsSidebars,
docsMetadatas,
};
}
module.exports = loadDocs;

View file

@ -7,10 +7,11 @@
const ejs = require('ejs');
const fs = require('fs-extra');
const _ = require('lodash');
const path = require('path');
const {generate} = require('@docusaurus/utils');
const loadConfig = require('./config');
const loadDocs = require('./docs');
const loadEnv = require('./env');
const loadTheme = require('./theme');
const loadRoutes = require('./routes');
@ -40,43 +41,14 @@ module.exports = async function load(siteDir, cliOptions = {}) {
`export default ${JSON.stringify(env, null, 2)};`,
);
// Docs
const docsDir = path.resolve(siteDir, '..', siteConfig.customDocsPath);
const {skipNextRelease} = cliOptions;
const {docsMetadatas, docsSidebars} = await loadDocs({
siteDir,
docsDir,
env,
siteConfig,
skipNextRelease,
});
await generate(
generatedFilesDir,
'docsMetadatas.js',
`export default ${JSON.stringify(docsMetadatas, null, 2)};`,
);
await generate(
generatedFilesDir,
'docsSidebars.js',
`export default ${JSON.stringify(docsSidebars, null, 2)};`,
);
// Create source to metadata mapping.
const sourceToMetadata = {};
Object.values(docsMetadatas).forEach(
({source, version, permalink, language}) => {
sourceToMetadata[source] = {
version,
permalink,
language,
};
},
);
// Process plugins.
const pluginConfigs = siteConfig.plugins || [];
const context = {env, siteDir, generatedFilesDir, siteConfig};
const {plugins, pluginRouteConfigs} = await loadPlugins({
const context = {env, siteDir, generatedFilesDir, siteConfig, cliOptions};
const {
plugins,
pluginsRouteConfigs,
pluginsLoadedContent,
} = await loadPlugins({
pluginConfigs,
context,
});
@ -91,12 +63,16 @@ module.exports = async function load(siteDir, cliOptions = {}) {
const versionedDir = path.join(siteDir, 'versioned_docs');
const translatedDir = path.join(siteDir, 'translated_docs');
// TODO: Make doc dependents use the plugin's content instead
// of passing in via props.
const {
docsDir,
docs: docsMetadata,
sourceToMetadata,
} = pluginsLoadedContent[0].content;
// Generate React Router Config.
const {routesConfig, routesPaths} = await loadRoutes({
siteConfig,
docsMetadatas,
pluginRouteConfigs,
});
const {routesConfig, routesPaths} = await loadRoutes(pluginsRouteConfigs);
await generate(generatedFilesDir, 'routes.js', routesConfig);
// Generate contents metadata.
@ -105,21 +81,20 @@ module.exports = async function load(siteDir, cliOptions = {}) {
'../core/templates/metadata.template.ejs',
);
const metadataTemplate = fs.readFileSync(metadataTemplateFile).toString();
const pluginMetadataImports = _.compact(pluginsLoadedContent).map(
({metadataKey, contentPath}) => ({
name: metadataKey,
path: contentPath,
}),
);
const metadataFile = ejs.render(metadataTemplate, {
imports: [
{
name: 'docsMetadatas',
path: '@generated/docsMetadatas',
},
...pluginMetadataImports,
{
name: 'env',
path: '@generated/env',
},
{
name: 'docsSidebars',
path: '@generated/docsSidebars',
},
],
});
await generate(generatedFilesDir, 'metadata.js', metadataFile);
@ -128,8 +103,7 @@ module.exports = async function load(siteDir, cliOptions = {}) {
siteConfig,
siteDir,
docsDir,
docsMetadatas,
docsSidebars,
docsMetadata,
env,
outDir,
themePath,

View file

@ -6,9 +6,11 @@
*/
const fs = require('fs-extra');
const path = require('path');
const {generate} = require('@docusaurus/utils');
module.exports = async function loadPlugins({pluginConfigs = [], context}) {
/* 1. Plugin Lifecycle - Initializiation/Constructor */
// 1. Plugin Lifecycle - Initialization/Constructor
const plugins = pluginConfigs.map(({name, path: pluginPath, options}) => {
let Plugin;
if (pluginPath && fs.existsSync(pluginPath)) {
@ -25,46 +27,59 @@ module.exports = async function loadPlugins({pluginConfigs = [], context}) {
return new Plugin(options, context);
});
// Do not allow plugin with duplicate name
const pluginNames = new Set();
plugins.forEach(plugin => {
const name = plugin.getName();
if (pluginNames.has(name)) {
throw new Error(`Duplicate plugin with name '${name}' found`);
}
pluginNames.add(name);
});
/* 2. Plugin lifecycle - LoadContent */
const pluginsLoadedContent = {};
await Promise.all(
// 2. Plugin lifecycle - loadContent
// Currently plugins run lifecycle in parallel and are not order-dependent. We could change
// this in future if there are plugins which need to run in certain order or depend on
// others for data.
const pluginsLoadedContent = await Promise.all(
plugins.map(async plugin => {
if (!plugin.loadContent) {
return;
return null;
}
const name = plugin.getName();
pluginsLoadedContent[name] = await plugin.loadContent();
const {options} = plugin;
const {metadataKey, metadataFileName} = options;
const content = await plugin.loadContent();
const pluginContentPath = path.join(name, metadataFileName);
const pluginContentDir = path.join(context.generatedFilesDir, name);
fs.ensureDirSync(pluginContentDir);
await generate(
pluginContentDir,
metadataFileName,
JSON.stringify(content, null, 2),
);
const contentPath = path.join('@generated', pluginContentPath);
return {
metadataKey,
contentPath,
content,
};
}),
);
/* 3. Plugin lifecycle - contentLoaded */
const pluginRouteConfigs = [];
// 3. Plugin lifecycle - contentLoaded
const pluginsRouteConfigs = [];
const actions = {
addRoute: config => pluginRouteConfigs.push(config),
addRoute: config => pluginsRouteConfigs.push(config),
};
await Promise.all(
plugins.map(async plugin => {
plugins.map(async (plugin, index) => {
if (!plugin.contentLoaded) {
return;
}
const name = plugin.getName();
const content = pluginsLoadedContent[name];
await plugin.contentLoaded({content, actions});
const loadedContent = pluginsLoadedContent[index];
await plugin.contentLoaded({
content: loadedContent.content,
actions,
});
}),
);
return {
plugins,
pluginRouteConfigs,
pluginsRouteConfigs,
pluginsLoadedContent,
};
};

View file

@ -5,19 +5,13 @@
* LICENSE file in the root directory of this source tree.
*/
const {normalizeUrl, generateChunkName} = require('@docusaurus/utils');
const {generateChunkName} = require('@docusaurus/utils');
async function loadRoutes({
siteConfig = {},
docsMetadatas = {},
pluginRouteConfigs = [],
}) {
async function loadRoutes(pluginsRouteConfigs) {
const imports = [
`import React from 'react';`,
`import Loadable from 'react-loadable';`,
`import Loading from '@theme/Loading';`,
`import Doc from '@theme/Doc';`,
`import DocBody from '@theme/DocBody';`,
`import NotFound from '@theme/NotFound';`,
];
@ -28,50 +22,29 @@ async function loadRoutes({
}
};
// Docs.
const {docsUrl, baseUrl} = siteConfig;
function genDocsRoute(metadata) {
const {permalink, source} = metadata;
addRoutesPath(permalink);
return `
{
path: '${permalink}',
exact: true,
component: Loadable({
loader: () => import(/* webpackChunkName: '${generateChunkName(
permalink,
)}' */ '${source}'),
loading: Loading,
render(loaded, props) {
let Content = loaded.default;
return (
<DocBody {...props} metadata={${JSON.stringify(metadata)}}>
<Content />
</DocBody>
);
}
})
}`;
}
const rootDocsUrl = normalizeUrl([baseUrl, docsUrl]);
const docsRoutes = `
{
path: '${rootDocsUrl}',
component: Doc,
routes: [${Object.values(docsMetadatas)
.map(genDocsRoute)
.join(',')}],
}`;
const notFoundRoute = `
{
path: '*',
component: NotFound,
}`;
const routes = pluginRouteConfigs.map(pluginRouteConfig => {
const {path, component, metadata, modules} = pluginRouteConfig;
function generateRouteCode(pluginRouteConfig) {
const {path, component, metadata, modules, routes} = pluginRouteConfig;
if (routes) {
return `
{
path: '${path}',
component: Loadable({
loader: () => import(/* webpackChunkName: '${generateChunkName(
component,
'component',
)}' */'${component}'),
loading: Loading,
}),
routes: [${routes.map(generateRouteCode).join(',')}],
}`;
}
addRoutesPath(path);
return `
{
@ -109,14 +82,14 @@ ${modules
}
})
}`;
});
}
const routes = pluginsRouteConfigs.map(generateRouteCode);
const routesConfig = `
${imports.join('\n')}
const routes = [
// Docs.${docsRoutes},
// Plugins.${routes.join(',')},
// Not Found.${notFoundRoute},

View file

@ -6,9 +6,9 @@
*/
import React, {useContext} from 'react';
import Head from '@docusaurus/Head';
import Layout from '@theme/Layout'; // eslint-disable-line
import DocusaurusContext from '@docusaurus/context';
import Post from '../Post';
import styles from './styles.module.css';

View file

@ -13,17 +13,19 @@ import DocusaurusContext from '@docusaurus/context';
import styles from './styles.module.css';
function DocBody(props) {
const {children, metadata} = props;
const {metadata, modules} = props;
const context = useContext(DocusaurusContext);
useEffect(() => {
context.setContext({metadata});
}, []);
const DocContents = modules[0];
return (
<div>
<div className={styles.docContent}>
<h1>{metadata.title}</h1>
{children}
<DocContents />
</div>
<div className={styles.paginatorContainer}>
<DocsPaginator />

View file

@ -14,18 +14,19 @@ import styles from './styles.module.css';
function DocsPaginator() {
const context = useContext(DocusaurusContext);
const {docsMetadatas, metadata} = context;
if (!metadata) {
const {docsMetadata, metadata} = context;
if (!metadata || !docsMetadata) {
return null;
}
const {docs} = docsMetadata;
return (
<div className={styles.paginatorContainer}>
<div>
{metadata.previous && docsMetadatas[metadata.previous] && (
{metadata.previous && docs[metadata.previous] && (
<Link
className={styles.paginatorLink}
to={docsMetadatas[metadata.previous].permalink}>
to={docs[metadata.previous].permalink}>
<svg className={styles.arrow} viewBox="0 0 24 24">
<g>
<line x1="19" y1="12" x2="5" y2="12" />
@ -37,10 +38,10 @@ function DocsPaginator() {
)}
</div>
<div className={styles.paginatorRightContainer}>
{metadata.next && docsMetadatas[metadata.next] && (
{metadata.next && docs[metadata.next] && (
<Link
className={styles.paginatorLink}
to={docsMetadatas[metadata.next].permalink}>
to={docs[metadata.next].permalink}>
<span className={styles.label}>{metadata.next_title}</span>{' '}
<svg className={styles.arrow} viewBox="0 0 24 24">
<g>

View file

@ -14,12 +14,7 @@ import styles from './styles.module.css';
function Navbar(props) {
const context = useContext(DocusaurusContext);
const {
siteConfig = {},
env = {},
metadata = {},
docsMetadatas = {},
} = context;
const {siteConfig = {}, env = {}, metadata = {}, docsMetadata} = context;
const {
baseUrl,
headerLinks,
@ -28,6 +23,7 @@ function Navbar(props) {
title,
disableHeaderTitle,
} = siteConfig;
const {language: thisLanguage, version: thisVersion} = metadata;
const translationEnabled = env.translation.enabled;
@ -56,8 +52,9 @@ function Navbar(props) {
? `version-${thisVersion || env.versioning.defaultVersion}-`
: '';
const id = langPart + versionPart + link.doc;
if (!docsMetadatas[id]) {
const errorStr = `We could not find the doc wih id: ${id}. Please check your headerLinks correctly\n`;
const {docs} = docsMetadata;
if (!docs[id]) {
const errorStr = `We could not find the doc with id: ${id}. Please check your headerLinks correctly\n`;
throw new Error(errorStr);
}
return (
@ -65,7 +62,7 @@ function Navbar(props) {
<Link
activeClassName={styles.navLinkActive}
className={styles.navLink}
to={docsMetadatas[id].permalink}>
to={docs[id].permalink}>
{link.label}
</Link>
</li>

View file

@ -15,14 +15,14 @@ import styles from './styles.module.css';
function Sidebar() {
const context = useContext(DocusaurusContext);
const {metadata = {}, docsSidebars, docsMetadatas} = context;
const {metadata = {}, docsMetadata} = context;
const {sidebar, language} = metadata;
if (!sidebar) {
return null;
}
const thisSidebar = docsSidebars[sidebar];
const thisSidebar = docsMetadata.docsSidebars[sidebar];
if (!thisSidebar) {
throw new Error(`Can not find ${sidebar} config`);
@ -30,7 +30,7 @@ function Sidebar() {
const convertDocLink = item => {
const linkID = (language ? `${language}-` : '') + item.id;
const linkMetadata = docsMetadatas[linkID];
const linkMetadata = docsMetadata.docs[linkID];
if (!linkMetadata) {
throw new Error(

View file

@ -19,6 +19,12 @@ module.exports = {
headerIcon: 'img/docusaurus.svg',
favicon: 'img/docusaurus.ico',
plugins: [
{
name: '@docusaurus/plugin-content-docs',
options: {
path: '../docs',
},
},
{
name: '@docusaurus/plugin-content-pages',
},

View file

@ -19,6 +19,12 @@ module.exports = {
headerIcon: 'img/docusaurus.svg',
favicon: 'img/docusaurus.ico',
plugins: [
{
name: '@docusaurus/plugin-content-docs',
options: {
path: '../docs',
},
},
{
name: '@docusaurus/plugin-content-pages',
},

View file

@ -20,6 +20,12 @@ module.exports = {
headerIcon: 'img/docusaurus.svg',
favicon: 'img/docusaurus.ico',
plugins: [
{
name: '@docusaurus/plugin-content-docs',
options: {
path: '../docs',
},
},
{
name: '@docusaurus/plugin-content-pages',
},

View file

@ -20,6 +20,12 @@ module.exports = {
headerIcon: 'img/docusaurus.svg',
favicon: 'img/docusaurus.ico',
plugins: [
{
name: '@docusaurus/plugin-content-docs',
options: {
path: '../docs',
},
},
{
name: '@docusaurus/plugin-content-pages',
},

View file

@ -19,6 +19,12 @@ module.exports = {
headerIcon: 'img/docusaurus.svg',
favicon: 'img/docusaurus.ico',
plugins: [
{
name: '@docusaurus/plugin-content-docs',
options: {
path: '../docs',
},
},
{
name: '@docusaurus/plugin-content-pages',
},

View file

@ -16,8 +16,6 @@ describe('loadConfig', () => {
expect(config).toMatchInlineSnapshot(`
Object {
"baseUrl": "/",
"customDocsPath": "docs",
"docsUrl": "docs",
"favicon": "img/docusaurus.ico",
"headerIcon": "img/docusaurus.svg",
"headerLinks": Array [
@ -35,6 +33,12 @@ Object {
],
"organizationName": "endiliey",
"plugins": Array [
Object {
"name": "@docusaurus/plugin-content-docs",
"options": Object {
"path": "../docs",
},
},
Object {
"name": "@docusaurus/plugin-content-pages",
},

View file

@ -11,7 +11,6 @@ module.exports = {
organizationName: 'facebook',
projectName: 'docusaurus',
baseUrl: '/',
customDocsPath: './docs',
url: 'https://docusaurus.io',
headerLinks: [
{doc: 'installation', label: 'Docs'},
@ -30,6 +29,12 @@ module.exports = {
algoliaOptions: {},
},
plugins: [
{
name: '@docusaurus/plugin-content-docs',
options: {
path: '../docs',
},
},
{
name: '@docusaurus/plugin-content-blog',
options: {