feat(v2): pluginify pages (#1278)

* feat(v2): convert pages into a plugin

* fix: update tests
This commit is contained in:
Yangshun Tay 2019-03-13 13:08:12 -07:00 committed by GitHub
parent 2da59ed4af
commit 398d7c7ae4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 287 additions and 257 deletions

View file

@ -1,4 +1,3 @@
__fixtures__
dist
node_modules
build

View file

@ -37,5 +37,8 @@ module.exports = {
path: '../v1/website/blog',
},
},
{
name: 'docusaurus-plugin-content-pages',
},
],
};

View file

@ -11,7 +11,6 @@ const path = require('path');
const loadConfig = require('./config');
const loadDocs = require('./docs');
const loadEnv = require('./env');
const loadPages = require('./pages');
const loadTheme = require('./theme');
const {generate} = require('./utils');
const loadRoutes = require('./routes');
@ -71,25 +70,16 @@ module.exports = async function load(siteDir) {
},
);
// Pages.
const pagesDir = path.resolve(siteDir, 'pages');
const pagesMetadatas = await loadPages({pagesDir, env, siteConfig});
await generate(
generatedFilesDir,
'pagesMetadatas.js',
`export default ${JSON.stringify(pagesMetadatas, null, 2)};`,
);
// Process plugins.
const pluginConfigs = siteConfig.plugins || [];
const context = {env, siteDir, siteConfig};
// Initialize plugins.
const plugins = pluginConfigs.map(({name, options: opts}) => {
const plugins = pluginConfigs.map(({name, options}) => {
// TODO: Resolve using node_modules as well.
// eslint-disable-next-line
const Plugin = require(path.resolve(__dirname, '../../plugins', name));
return new Plugin(opts, context);
return new Plugin(options, context);
});
// Plugin lifecycle - loadContents().
@ -156,7 +146,6 @@ module.exports = async function load(siteDir) {
const {routesConfig, routesPaths} = await loadRoutes({
siteConfig,
docsMetadatas,
pagesMetadatas,
pluginRouteConfigs,
});
await generate(generatedFilesDir, 'routes.js', routesConfig);
@ -189,10 +178,6 @@ module.exports = async function load(siteDir) {
name: 'docsSidebars',
path: '@generated/docsSidebars',
},
{
name: 'pagesMetadatas',
path: '@generated/pagesMetadatas',
},
],
});
await generate(generatedFilesDir, 'metadata.js', metadataFile);
@ -204,8 +189,6 @@ module.exports = async function load(siteDir) {
docsMetadatas,
docsSidebars,
env,
pagesDir,
pagesMetadatas,
outDir,
themePath,
baseUrl,

View file

@ -1,65 +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 globby = require('globby');
const path = require('path');
const {encodePath, fileToPath, idx} = require('./utils');
async function loadPages({pagesDir, env, siteConfig}) {
const pagesFiles = await globby(['**/*.js'], {
cwd: pagesDir,
});
const {baseUrl} = siteConfig;
// Prepare metadata container.
const pagesMetadatas = [];
// Translation.
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']);
await Promise.all(
pagesFiles.map(async relativeSource => {
const source = path.join(pagesDir, relativeSource);
const pathName = encodePath(fileToPath(relativeSource));
if (translationEnabled && enabledLangTags.length > 0) {
enabledLangTags.forEach(langTag => {
// Default lang should also be available. E.g: /en/users and /users is the same.
if (langTag === defaultLangTag) {
pagesMetadatas.push({
permalink: pathName.replace(/^\//, baseUrl),
language: langTag,
source,
});
}
const metadata = {
permalink: pathName.replace(/^\//, `${baseUrl}${langTag}/`),
language: langTag,
source,
};
pagesMetadatas.push(metadata);
});
} else {
// Default Language.
const metadata = {
permalink: pathName.replace(/^\//, baseUrl),
source,
};
pagesMetadatas.push(metadata);
}
}),
);
return pagesMetadatas;
}
module.exports = loadPages;

View file

@ -10,7 +10,6 @@ const {normalizeUrl} = require('./utils');
async function loadRoutes({
siteConfig = {},
docsMetadatas = {},
pagesMetadatas = [],
pluginRouteConfigs = [],
}) {
const imports = [
@ -19,7 +18,6 @@ async function loadRoutes({
`import Loading from '@theme/Loading';`,
`import Doc from '@theme/Doc';`,
`import DocBody from '@theme/DocBody';`,
`import Pages from '@theme/Pages';`,
`import NotFound from '@theme/NotFound';`,
];
@ -64,29 +62,6 @@ async function loadRoutes({
.join(',')}],
}`;
// Pages.
function genPagesRoute(metadata) {
const {permalink, source} = metadata;
addRoutesPath(permalink);
return `
{
path: '${permalink}',
exact: true,
component: Loadable({
loader: () => import('${source}'),
loading: Loading,
render(loaded, props) {
let Content = loaded.default;
return (
<Pages {...props} metadata={${JSON.stringify(metadata)}}>
<Content {...props} metadata={${JSON.stringify(metadata)}} />
</Pages>
);
}
})
}`;
}
const notFoundRoute = `
{
path: '*',
@ -131,9 +106,7 @@ ${modules
${imports.join('\n')}
const routes = [
// Docs.${pagesMetadatas.map(genPagesRoute).join(',')},
// Pages.${docsRoutes},
// Docs.${docsRoutes},
// Plugins.${routes.join(',')},

View file

@ -14,8 +14,6 @@ import styles from './styles.module.css';
function Footer() {
const context = useContext(DocusaurusContext);
const {pagesMetadatas} = context;
return (
<footer className={styles.footer}>
<section className={styles.footerRow}>
@ -79,11 +77,11 @@ function Footer() {
</li>
</ul>
</div>
{/* This is for v2 development only to know which are the available page */}
{/* This is for v2 development only to know the available pages. */}
<div className={styles.footerColumn}>
<h3 className={styles.footerColumnTitle}>Pages</h3>
<ul className={styles.footerList}>
{pagesMetadatas.map(metadata => (
{context.pagesMetadata.map(metadata => (
<li key={metadata.permalink} className={styles.footerListItem}>
<Link className={styles.footerLink} to={metadata.permalink}>
{metadata.permalink}

View file

@ -12,11 +12,12 @@ import Layout from '@theme/Layout'; // eslint-disable-line
import DocusaurusContext from '@docusaurus/context';
function Pages({children}) {
function Pages({modules}) {
const context = useContext(DocusaurusContext);
const {metadata = {}, siteConfig = {}} = context;
const {baseUrl, favicon} = siteConfig;
const {language} = metadata;
const PageContents = modules[0];
return (
<Layout>
@ -25,7 +26,7 @@ function Pages({children}) {
{language && <html lang={language} />}
{language && <meta name="docsearch:language" content={language} />}
</Head>
{children}
<PageContents />
<Footer />
</Layout>
);

View file

@ -40,7 +40,6 @@ module.exports = function createBaseConfig(props, isServer) {
outDir,
themePath,
docsDir,
pagesDir,
siteDir,
sourceToMetadata,
versionedDir,
@ -69,7 +68,6 @@ module.exports = function createBaseConfig(props, isServer) {
.set('@versioned_docs', versionedDir)
.set('@translated_docs', translatedDir)
.set('@docs', docsDir)
.set('@pages', pagesDir)
.set('@build', outDir)
.set('@generated', generatedFilesDir)
.set('@core', path.resolve(__dirname, '../core'))

View file

@ -22,8 +22,8 @@
"scripts": {
"docusaurus": "node bin/docusaurus",
"start": "yarn docusaurus start ../v2-website",
"prettier": "prettier --config ../.prettierrc --write \"**/*.js\"",
"lint": "eslint --cache \"{lib,bin,test}/**/*.js\"",
"prettier": "prettier --config ../.prettierrc --ignore-path ../.prettierignore --write \"**/*.js\"",
"lint": "eslint --cache \"{lib,bin,test,plugins}/**/*.js\"",
"test": "jest --config test/jest.config.js"
},
"bugs": {

View file

@ -9,7 +9,7 @@ const globby = require('globby');
const path = require('path');
const fs = require('fs-extra');
// TODO: Do not make it relative because plugins can be from node_modules.
const {parse, idx, normalizeUrl} = require('../lib/load/utils');
const {parse, idx, normalizeUrl} = require('../../lib/load/utils');
function fileToUrl(fileName) {
return fileName

View file

@ -0,0 +1,125 @@
/**
* 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 path from 'path';
import loadSetup from '@test/loadSetup';
import DocusaurusPluginContentPages from '../index';
describe('docusaurus-plugin-content-pages', () => {
describe('loadContents', () => {
test.each([
[
'simple',
pagesDir => [
{
permalink: '/',
source: path.join(pagesDir, 'index.js'),
},
{
permalink: '/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
],
],
[
'versioned',
pagesDir => [
{
permalink: '/',
source: path.join(pagesDir, 'index.js'),
},
{
permalink: '/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
],
],
[
'translated',
pagesDir => [
{
language: 'en',
permalink: '/',
source: path.join(pagesDir, 'index.js'),
},
{
language: 'en',
permalink: '/en/',
source: path.join(pagesDir, 'index.js'),
},
{
language: 'ko',
permalink: '/ko/',
source: path.join(pagesDir, 'index.js'),
},
{
language: 'en',
permalink: '/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
{
language: 'en',
permalink: '/en/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
{
language: 'ko',
permalink: '/ko/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
],
[
'transversioned',
pagesDir => [
{
language: 'en',
permalink: '/',
source: path.join(pagesDir, 'index.js'),
},
{
language: 'en',
permalink: '/en/',
source: path.join(pagesDir, 'index.js'),
},
{
language: 'ko',
permalink: '/ko/',
source: path.join(pagesDir, 'index.js'),
},
{
language: 'en',
permalink: '/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
{
language: 'en',
permalink: '/en/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
{
language: 'ko',
permalink: '/ko/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
],
],
],
])('%s website', async (type, expected) => {
const {env, siteDir, siteConfig} = await loadSetup(type);
const plugin = new DocusaurusPluginContentPages(null, {
env,
siteDir,
siteConfig,
});
const pagesMetadatas = await plugin.loadContents();
const pagesDir = plugin.contentPath;
expect(pagesMetadatas).toEqual(expected(pagesDir));
});
});
});

View file

@ -0,0 +1,112 @@
/**
* 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 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('../../lib/load/utils');
const DEFAULT_OPTIONS = {
metadataKey: 'pagesMetadata',
metadataFileName: 'pagesMetadata.json',
path: 'pages', // Path to data on filesystem.
routeBasePath: '', // URL Route.
include: ['**/*.{js,jsx}'], // Extensions to include.
component: '@theme/Pages',
};
class DocusaurusPluginContentPages {
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-pages';
}
async loadContents() {
const {include} = this.options;
const {env, siteConfig} = this.context;
const pagesDir = this.contentPath;
const {baseUrl} = siteConfig;
const pagesFiles = await globby(include, {
cwd: pagesDir,
});
// Prepare metadata container.
const pagesMetadatas = [];
// Translation.
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']);
await Promise.all(
pagesFiles.map(async relativeSource => {
const source = path.join(pagesDir, relativeSource);
const pathName = encodePath(fileToPath(relativeSource));
if (translationEnabled && enabledLangTags.length > 0) {
enabledLangTags.forEach(langTag => {
// Default lang should also be available. E.g: /en/users and /users is the same.
if (langTag === defaultLangTag) {
pagesMetadatas.push({
permalink: pathName.replace(/^\//, baseUrl),
language: langTag,
source,
});
}
const metadata = {
permalink: pathName.replace(/^\//, `${baseUrl}${langTag}/`),
language: langTag,
source,
};
pagesMetadatas.push(metadata);
});
} else {
// Default Language.
const metadata = {
permalink: pathName.replace(/^\//, baseUrl),
source,
};
pagesMetadatas.push(metadata);
}
}),
);
return pagesMetadatas;
}
async generateRoutes({metadata, actions}) {
const {component} = this.options;
const {addRoute} = actions;
metadata.forEach(metadataItem => {
const {permalink, source} = metadataItem;
addRoute({
path: permalink,
component,
metadata: metadataItem,
modules: [source],
});
});
}
getPathsToWatch() {
// Not needed, all pages are loaded as modules and watched by webpack.
return [];
}
}
module.exports = DocusaurusPluginContentPages;

View file

@ -18,4 +18,9 @@ module.exports = {
],
headerIcon: 'img/docusaurus.svg',
favicon: 'img/docusaurus.ico',
plugins: [
{
name: 'docusaurus-plugin-content-pages',
},
],
};

View file

@ -18,4 +18,9 @@ module.exports = {
],
headerIcon: 'img/docusaurus.svg',
favicon: 'img/docusaurus.ico',
plugins: [
{
name: 'docusaurus-plugin-content-pages',
},
],
};

View file

@ -19,4 +19,9 @@ module.exports = {
],
headerIcon: 'img/docusaurus.svg',
favicon: 'img/docusaurus.ico',
plugins: [
{
name: 'docusaurus-plugin-content-pages',
},
],
};

View file

@ -19,4 +19,9 @@ module.exports = {
],
headerIcon: 'img/docusaurus.svg',
favicon: 'img/docusaurus.ico',
plugins: [
{
name: 'docusaurus-plugin-content-pages',
},
],
};

View file

@ -18,4 +18,9 @@ module.exports = {
],
headerIcon: 'img/docusaurus.svg',
favicon: 'img/docusaurus.ico',
plugins: [
{
name: 'docusaurus-plugin-content-pages',
},
],
};

View file

@ -14,6 +14,7 @@ module.exports = {
testEnvironment: 'node',
moduleNameMapper: {
'^@lib/(.*)$': '<rootDir>/lib/$1',
'^@test/(.*)$': '<rootDir>/test/$1',
},
testPathIgnorePatterns: ['/node_modules/', '__fixtures__', 'v1'],
transform: {

View file

@ -34,6 +34,11 @@ Object {
},
],
"organizationName": "endiliey",
"plugins": Array [
Object {
"name": "docusaurus-plugin-content-pages",
},
],
"projectName": "hello",
"tagline": "Hello World",
"title": "Hello",

View file

@ -1,123 +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 loadPages from '@lib/load/pages';
import path from 'path';
import loadSetup from '../loadSetup';
describe('loadPages', () => {
test('simple website', async () => {
const {pagesDir, env, siteConfig} = await loadSetup('simple');
const pagesMetadatas = await loadPages({pagesDir, env, siteConfig});
expect(pagesMetadatas).toEqual([
{
permalink: '/',
source: path.join(pagesDir, 'index.js'),
},
{
permalink: '/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
]);
});
test('versioned website', async () => {
const {pagesDir, env, siteConfig} = await loadSetup('versioned');
const pagesMetadatas = await loadPages({pagesDir, env, siteConfig});
expect(pagesMetadatas).toEqual([
{
permalink: '/',
source: path.join(pagesDir, 'index.js'),
},
{
permalink: '/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
]);
});
test('versioned & translated website', async () => {
const {pagesDir, env, siteConfig} = await loadSetup('transversioned');
const pagesMetadatas = await loadPages({pagesDir, env, siteConfig});
expect(pagesMetadatas).toEqual([
{
language: 'en',
permalink: '/',
source: path.join(pagesDir, 'index.js'),
},
{
language: 'en',
permalink: '/en/',
source: path.join(pagesDir, 'index.js'),
},
{
language: 'ko',
permalink: '/ko/',
source: path.join(pagesDir, 'index.js'),
},
{
language: 'en',
permalink: '/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
{
language: 'en',
permalink: '/en/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
{
language: 'ko',
permalink: '/ko/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
]);
});
test('translated website', async () => {
const {pagesDir, env, siteConfig} = await loadSetup('translated');
const pagesMetadatas = await loadPages({pagesDir, env, siteConfig});
expect(pagesMetadatas).toEqual([
{
language: 'en',
permalink: '/',
source: path.join(pagesDir, 'index.js'),
},
{
language: 'en',
permalink: '/en/',
source: path.join(pagesDir, 'index.js'),
},
{
language: 'ko',
permalink: '/ko/',
source: path.join(pagesDir, 'index.js'),
},
{
language: 'en',
permalink: '/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
{
language: 'en',
permalink: '/en/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
{
language: 'ko',
permalink: '/ko/hello/world',
source: path.join(pagesDir, 'hello', 'world.js'),
},
]);
});
test('invalid pages', async () => {
const {env, siteConfig} = await loadSetup('simple');
const pagesDir = path.join(__dirname, '__fixtures__', 'nonExisting');
const pagesMetadatas = await loadPages({pagesDir, env, siteConfig});
expect(pagesMetadatas).toEqual([]);
});
});

View file

@ -5,13 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
import loadRoutes from '@lib/load/routes';
import loadSetup from '../loadSetup';
describe('loadRoutes', () => {
test('simple website', async () => {
const props = await loadSetup('simple');
const {routesPaths} = await loadRoutes(props);
const {routesPaths} = await loadSetup('simple');
expect(routesPaths.length).toBeGreaterThan(0);
expect(routesPaths.sort()).toMatchInlineSnapshot(`
Array [
@ -26,8 +24,7 @@ Array [
});
test('versioned website', async () => {
const props = await loadSetup('versioned');
const {routesPaths} = await loadRoutes(props);
const {routesPaths} = await loadSetup('versioned');
expect(routesPaths.length).toBeGreaterThan(0);
expect(routesPaths.sort()).toMatchInlineSnapshot(`
Array [
@ -48,8 +45,7 @@ Array [
});
test('versioned & translated website', async () => {
const props = await loadSetup('transversioned');
const {routesPaths} = await loadRoutes(props);
const {routesPaths} = await loadSetup('transversioned');
expect(routesPaths.length).toBeGreaterThan(0);
expect(routesPaths.sort()).toMatchInlineSnapshot(`
Array [
@ -83,8 +79,7 @@ Array [
});
test('translated website', async () => {
const props = await loadSetup('translated');
const {routesPaths} = await loadRoutes(props);
const {routesPaths} = await loadSetup('translated');
expect(routesPaths.length).toBeGreaterThan(0);
expect(routesPaths.sort()).toMatchInlineSnapshot(`
Array [