mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-01 18:32:52 +02:00
v2: prepare to move
This commit is contained in:
parent
dc7ef96849
commit
45736200b0
172 changed files with 0 additions and 0 deletions
77
v2/lib/commands/build.js
Normal file
77
v2/lib/commands/build.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
const webpack = require('webpack');
|
||||
const path = require('path');
|
||||
const chalk = require('chalk');
|
||||
const fs = require('fs-extra');
|
||||
const globby = require('globby');
|
||||
const load = require('../load');
|
||||
const createServerConfig = require('../webpack/server');
|
||||
const createClientConfig = require('../webpack/client');
|
||||
const {applyConfigureWebpack} = require('../webpack/utils');
|
||||
|
||||
function compile(config) {
|
||||
return new Promise((resolve, reject) => {
|
||||
webpack(config, (err, stats) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
if (stats.hasErrors()) {
|
||||
stats.toJson().errors.forEach(e => {
|
||||
console.error(e);
|
||||
});
|
||||
reject(new Error(`Failed to compile with errors.`));
|
||||
}
|
||||
if (stats.hasWarnings()) {
|
||||
stats.toJson().warnings.forEach(warning => {
|
||||
console.warn(warning);
|
||||
});
|
||||
}
|
||||
resolve(stats.toJson({modules: false}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = async function build(siteDir, cliOptions = {}) {
|
||||
process.env.NODE_ENV = 'production';
|
||||
console.log('Build command invoked ...');
|
||||
console.log(cliOptions);
|
||||
|
||||
const props = await load(siteDir);
|
||||
|
||||
let serverConfig = createServerConfig(props).toConfig();
|
||||
let clientConfig = createClientConfig(props).toConfig();
|
||||
|
||||
// apply user webpack config
|
||||
const {
|
||||
siteConfig: {configureWebpack}
|
||||
} = props;
|
||||
clientConfig = applyConfigureWebpack(configureWebpack, clientConfig, false);
|
||||
serverConfig = applyConfigureWebpack(configureWebpack, serverConfig, true);
|
||||
|
||||
// Build the client bundles first.
|
||||
// We cannot run them in parallel because the server need to pickup the correct client bundle name
|
||||
await compile(clientConfig);
|
||||
|
||||
// Build the server bundles (render the static HTML and pick client bundle)
|
||||
await compile(serverConfig);
|
||||
|
||||
// copy static files
|
||||
const {outDir} = props;
|
||||
const staticDir = path.resolve(siteDir, 'static');
|
||||
const staticFiles = await globby(['**'], {
|
||||
cwd: staticDir
|
||||
});
|
||||
await Promise.all(
|
||||
staticFiles.map(async source => {
|
||||
const fromPath = path.resolve(staticDir, source);
|
||||
const toPath = path.resolve(outDir, source);
|
||||
return fs.copy(fromPath, toPath);
|
||||
})
|
||||
);
|
||||
|
||||
const relativeDir = path.relative(process.cwd(), outDir);
|
||||
console.log(
|
||||
`\n${chalk.green('Success!')} Generated static files in ${chalk.cyan(
|
||||
relativeDir
|
||||
)}.\n`
|
||||
);
|
||||
};
|
16
v2/lib/commands/eject.js
Normal file
16
v2/lib/commands/eject.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
const fs = require('fs-extra');
|
||||
const chalk = require('chalk');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = async function eject(siteDir) {
|
||||
const defaultTheme = path.resolve(__dirname, '..', 'theme');
|
||||
const customTheme = path.resolve(siteDir, 'theme');
|
||||
await fs.copy(defaultTheme, customTheme);
|
||||
|
||||
const relativeDir = path.relative(process.cwd(), customTheme);
|
||||
console.log(
|
||||
`\n${chalk.green('Success!')} Copied default theme files to ${chalk.cyan(
|
||||
relativeDir
|
||||
)}.\n`
|
||||
);
|
||||
};
|
7
v2/lib/commands/init.js
Normal file
7
v2/lib/commands/init.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
module.exports = async function init(projectDir, cliOptions = {}) {
|
||||
console.log('Init command invoked ...');
|
||||
console.log(projectDir);
|
||||
console.log(cliOptions);
|
||||
|
||||
// TODO
|
||||
};
|
124
v2/lib/commands/start.js
Normal file
124
v2/lib/commands/start.js
Normal file
|
@ -0,0 +1,124 @@
|
|||
const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
const chalk = require('chalk');
|
||||
const webpack = require('webpack');
|
||||
const chokidar = require('chokidar');
|
||||
const convert = require('koa-connect');
|
||||
const range = require('koa-range');
|
||||
const mount = require('koa-mount');
|
||||
const serveStatic = require('koa-static');
|
||||
const history = require('connect-history-api-fallback');
|
||||
const portfinder = require('portfinder');
|
||||
const serve = require('webpack-serve');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const load = require('../load');
|
||||
const createClientConfig = require('../webpack/client');
|
||||
const {applyConfigureWebpack} = require('../webpack/utils');
|
||||
|
||||
function getHost(reqHost) {
|
||||
return reqHost || 'localhost';
|
||||
}
|
||||
|
||||
async function getPort(reqPort) {
|
||||
portfinder.basePort = parseInt(reqPort, 10) || 3000;
|
||||
const port = await portfinder.getPortPromise();
|
||||
return port;
|
||||
}
|
||||
|
||||
module.exports = async function start(siteDir, cliOptions = {}) {
|
||||
console.log('Start command invoked ...');
|
||||
console.log(cliOptions);
|
||||
|
||||
// Process all related files as a prop
|
||||
const props = await load(siteDir);
|
||||
|
||||
// Reload files processing
|
||||
if (!cliOptions.noWatch) {
|
||||
const reload = () => {
|
||||
load(siteDir).catch(err => {
|
||||
console.error(chalk.red(err.stack));
|
||||
});
|
||||
};
|
||||
const docsRelativeDir = props.siteConfig.customDocsPath;
|
||||
const fsWatcher = chokidar.watch(
|
||||
[`../${docsRelativeDir}/**/*.md`, 'blog/**/*.md', 'siteConfig.js'],
|
||||
{
|
||||
cwd: siteDir,
|
||||
ignoreInitial: true
|
||||
}
|
||||
);
|
||||
fsWatcher.on('add', reload);
|
||||
fsWatcher.on('change', reload);
|
||||
fsWatcher.on('unlink', reload);
|
||||
fsWatcher.on('addDir', reload);
|
||||
fsWatcher.on('unlinkDir', reload);
|
||||
}
|
||||
|
||||
const port = await getPort(cliOptions.port);
|
||||
const hotPort = await getPort(port + 1);
|
||||
const host = getHost(cliOptions.host);
|
||||
const {baseUrl} = props;
|
||||
|
||||
// create compiler from generated webpack config
|
||||
let config = createClientConfig(props);
|
||||
|
||||
const {siteConfig} = props;
|
||||
config.plugin('html-webpack-plugin').use(HtmlWebpackPlugin, [
|
||||
{
|
||||
inject: false,
|
||||
hash: true,
|
||||
template: path.resolve(__dirname, '../core/devTemplate.ejs'),
|
||||
filename: 'index.html',
|
||||
title: siteConfig.title
|
||||
}
|
||||
]);
|
||||
config = config.toConfig();
|
||||
|
||||
// apply user webpack config
|
||||
const {
|
||||
siteConfig: {configureWebpack}
|
||||
} = props;
|
||||
config = applyConfigureWebpack(configureWebpack, config, false);
|
||||
|
||||
const compiler = webpack(config);
|
||||
|
||||
// webpack-serve
|
||||
setTimeout(async () => {
|
||||
await serve(
|
||||
{},
|
||||
{
|
||||
compiler,
|
||||
open: true,
|
||||
devMiddleware: {
|
||||
logLevel: 'silent'
|
||||
},
|
||||
hotClient: {
|
||||
port: hotPort,
|
||||
logLevel: 'error'
|
||||
},
|
||||
logLevel: 'error',
|
||||
port,
|
||||
host,
|
||||
add: app => {
|
||||
// serve static files
|
||||
const staticDir = path.resolve(siteDir, 'static');
|
||||
if (fs.existsSync(staticDir)) {
|
||||
app.use(mount(baseUrl, serveStatic(staticDir)));
|
||||
}
|
||||
|
||||
// enable HTTP range requests
|
||||
app.use(range);
|
||||
|
||||
// rewrite request to `/` since dev is only a SPA
|
||||
app.use(
|
||||
convert(
|
||||
history({
|
||||
rewrites: [{from: /\.html$/, to: '/'}]
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}, 1000);
|
||||
};
|
15
v2/lib/core/App.js
Normal file
15
v2/lib/core/App.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import {renderRoutes} from 'react-router-config';
|
||||
|
||||
import routes from '@generated/routes'; // eslint-disable-line
|
||||
import docsMetadatas from '@generated/docsMetadatas'; // eslint-disable-line
|
||||
import docsSidebars from '@generated/docsSidebars'; // eslint-disable-line
|
||||
import pagesMetadatas from '@generated/pagesMetadatas'; // eslint-disable-line
|
||||
import siteConfig from '@generated/siteConfig'; //eslint-disable-line
|
||||
|
||||
export default () =>
|
||||
renderRoutes(routes, {
|
||||
docsMetadatas,
|
||||
docsSidebars,
|
||||
pagesMetadatas,
|
||||
siteConfig
|
||||
});
|
19
v2/lib/core/clientEntry.js
Normal file
19
v2/lib/core/clientEntry.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import {BrowserRouter} from 'react-router-dom';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import App from './App';
|
||||
import prerender from './prerender';
|
||||
import routes from '@generated/routes'; // eslint-disable-line
|
||||
|
||||
// Client side render (e.g: running in browser) to become single-page application (SPA)
|
||||
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||
prerender(routes, window.location.pathname).then(() => {
|
||||
ReactDOM.render(
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>,
|
||||
document.getElementById('app')
|
||||
);
|
||||
});
|
||||
}
|
12
v2/lib/core/devTemplate.ejs
Normal file
12
v2/lib/core/devTemplate.ejs
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="<%= htmlWebpackPlugin.files.chunks.main.entry %>"></script>
|
||||
</body>
|
||||
</html>
|
18
v2/lib/core/prerender.js
Normal file
18
v2/lib/core/prerender.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import {matchRoutes} from 'react-router-config';
|
||||
|
||||
/**
|
||||
* This helps us to make sure all the async component for that particular route
|
||||
* is loaded before rendering. This is to avoid loading screens on first page load
|
||||
*/
|
||||
export default function prerender(routeConfig, providedLocation) {
|
||||
const matches = matchRoutes(routeConfig, providedLocation);
|
||||
return Promise.all(
|
||||
matches.map(match => {
|
||||
const {component} = match.route;
|
||||
if (component && component.preload) {
|
||||
return component.preload();
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
);
|
||||
}
|
59
v2/lib/core/serverEntry.js
Normal file
59
v2/lib/core/serverEntry.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
import React from 'react';
|
||||
import {StaticRouter} from 'react-router-dom';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import Helmet from 'react-helmet';
|
||||
|
||||
import App from './App';
|
||||
import prerender from './prerender';
|
||||
import routes from '@generated/routes'; // eslint-disable-line
|
||||
import webpackClientStats from '@build/client.stats.json'; //eslint-disable-line
|
||||
|
||||
// Renderer for static-site-generator-webpack-plugin (async rendering via promises)
|
||||
export default function render(locals) {
|
||||
return prerender(routes, locals.path).then(() => {
|
||||
const context = {};
|
||||
const appHtml = ReactDOMServer.renderToString(
|
||||
<StaticRouter location={locals.path} context={context}>
|
||||
<App />
|
||||
</StaticRouter>
|
||||
);
|
||||
|
||||
const helmet = Helmet.renderStatic();
|
||||
const htmlAttributes = helmet.htmlAttributes.toString();
|
||||
const bodyAttributes = helmet.bodyAttributes.toString();
|
||||
const metaStrings = [
|
||||
helmet.title.toString(),
|
||||
helmet.meta.toString(),
|
||||
helmet.link.toString()
|
||||
];
|
||||
const metaHtml = metaStrings.filter(Boolean).join('\n ');
|
||||
|
||||
const assets = webpackClientStats.assetsByChunkName.main;
|
||||
const jsFiles = assets.filter(value => value.match(/\.js$/));
|
||||
const cssFiles = assets.filter(value => value.match(/\.css$/));
|
||||
const {baseUrl} = locals;
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html${htmlAttributes ? ` ${htmlAttributes}` : ''}>
|
||||
<head>
|
||||
${metaHtml}
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
${cssFiles.map(
|
||||
cssFile =>
|
||||
`<link rel="stylesheet" type="text/css" href="${baseUrl}${cssFile}" />`
|
||||
)}
|
||||
</head>
|
||||
<body${bodyAttributes ? ` ${bodyAttributes}` : ''}>
|
||||
<div id="app">${appHtml}</div>
|
||||
${jsFiles.map(
|
||||
jsFile =>
|
||||
`<script type="text/javascript" src="${baseUrl}${jsFile}"></script>`
|
||||
)}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
return html;
|
||||
});
|
||||
}
|
11
v2/lib/index.js
Normal file
11
v2/lib/index.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
const build = require('./commands/build');
|
||||
const init = require('./commands/init');
|
||||
const start = require('./commands/start');
|
||||
const eject = require('./commands/eject');
|
||||
|
||||
module.exports = {
|
||||
build,
|
||||
eject,
|
||||
init,
|
||||
start
|
||||
};
|
67
v2/lib/load/config.js
Normal file
67
v2/lib/load/config.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = function loadConfig(siteDir, deleteCache = true) {
|
||||
const configPath = path.resolve(siteDir, 'siteConfig.js');
|
||||
if (deleteCache) {
|
||||
delete require.cache[configPath];
|
||||
}
|
||||
let config = {};
|
||||
if (fs.existsSync(configPath)) {
|
||||
config = require(configPath); // eslint-disable-line
|
||||
}
|
||||
|
||||
const requiredFields = [
|
||||
'title',
|
||||
'tagline',
|
||||
'organizationName',
|
||||
'projectName',
|
||||
'baseUrl'
|
||||
];
|
||||
const optionalFields = [
|
||||
'customDocsPath',
|
||||
'defaultLanguage',
|
||||
'highlight',
|
||||
'markdownPlugins',
|
||||
'configureWebpack',
|
||||
'chainWebpack',
|
||||
'docsUrl',
|
||||
'customFields'
|
||||
];
|
||||
const missingFields = requiredFields.filter(field => !config[field]);
|
||||
if (missingFields && missingFields.length > 0) {
|
||||
throw new Error(
|
||||
`${missingFields.join(', ')} fields are missing in siteConfig.js`
|
||||
);
|
||||
}
|
||||
|
||||
/* Fill default value */
|
||||
const defaultConfig = {
|
||||
customDocsPath: 'docs',
|
||||
docsUrl: 'docs'
|
||||
};
|
||||
Object.keys(defaultConfig).forEach(field => {
|
||||
if (!config[field]) {
|
||||
config[field] = defaultConfig[field];
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
User's own array of custom fields,
|
||||
e.g: if they want to include some field so they can access it later from `props.siteConfig`
|
||||
*/
|
||||
const {customFields = []} = config;
|
||||
|
||||
/* We don't allow useless/ not meaningful field */
|
||||
const allowedFields = [...requiredFields, ...optionalFields, ...customFields];
|
||||
const uselessFields = Object.keys(config).filter(
|
||||
field => !allowedFields.includes(field)
|
||||
);
|
||||
if (uselessFields && uselessFields.length > 0) {
|
||||
throw new Error(
|
||||
`${uselessFields.join(', ')} fields are useless in siteConfig.js`
|
||||
);
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
129
v2/lib/load/docs/index.js
Normal file
129
v2/lib/load/docs/index.js
Normal file
|
@ -0,0 +1,129 @@
|
|||
const path = require('path');
|
||||
const globby = require('globby');
|
||||
const createOrder = require('./order');
|
||||
const loadSidebars = require('./sidebars');
|
||||
const processMetadata = require('./metadata');
|
||||
const {getSubFolder, idx} = require('../utils');
|
||||
|
||||
async function loadDocs({siteDir, docsDir, env, siteConfig}) {
|
||||
// @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 = {};
|
||||
|
||||
/* 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;
|
170
v2/lib/load/docs/metadata.js
Normal file
170
v2/lib/load/docs/metadata.js
Normal file
|
@ -0,0 +1,170 @@
|
|||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const {getSubFolder, idx, parse} = require('../utils');
|
||||
|
||||
function getLanguage(filepath, refDir, env) {
|
||||
const translationEnabled = idx(env, ['translation', 'enabled']);
|
||||
if (translationEnabled) {
|
||||
const detectedLangTag = getSubFolder(filepath, refDir);
|
||||
const enabledLanguages = idx(env, ['translation', 'enabledLanguages']);
|
||||
const langTags =
|
||||
(enabledLanguages && enabledLanguages.map(lang => lang.tag)) || [];
|
||||
if (langTags.includes(detectedLangTag)) {
|
||||
return detectedLangTag;
|
||||
}
|
||||
const defaultLanguage = idx(env, ['translation', 'defaultLanguage']);
|
||||
if (defaultLanguage && defaultLanguage.tag) {
|
||||
return defaultLanguage.tag;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getVersion(filepath, refDir, env) {
|
||||
const versioningEnabled = idx(env, ['versioning', 'enabled']);
|
||||
if (versioningEnabled) {
|
||||
const subFolder = getSubFolder(filepath, refDir);
|
||||
if (subFolder) {
|
||||
const detectedVersion = subFolder.replace(/^version-/, '');
|
||||
const versions = idx(env, ['versioning', 'versions']) || [];
|
||||
if (versions.includes(detectedVersion)) {
|
||||
return detectedVersion;
|
||||
}
|
||||
}
|
||||
return 'next';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
module.exports = async function processMetadata(
|
||||
source,
|
||||
refDir,
|
||||
env,
|
||||
order,
|
||||
siteConfig
|
||||
) {
|
||||
const filepath = path.resolve(refDir, source);
|
||||
const fileString = await fs.readFile(filepath, 'utf-8');
|
||||
const {metadata} = parse(fileString);
|
||||
|
||||
/* default id is the file name */
|
||||
if (!metadata.id) {
|
||||
metadata.id = path.basename(source, path.extname(source));
|
||||
}
|
||||
if (metadata.id.includes('/')) {
|
||||
throw new Error('Document id cannot include "/".');
|
||||
}
|
||||
|
||||
/* default title is the id */
|
||||
if (!metadata.title) {
|
||||
metadata.title = metadata.id;
|
||||
}
|
||||
|
||||
/* language */
|
||||
const language = getLanguage(filepath, refDir, env);
|
||||
metadata.language = language;
|
||||
const langPart = (language && `${language}/`) || '';
|
||||
|
||||
/* version */
|
||||
const defaultLangTag = idx(env, ['translation', 'defaultLanguage', 'tag']);
|
||||
let versionRefDir = refDir;
|
||||
if (language && language !== defaultLangTag) {
|
||||
versionRefDir = path.join(refDir, language);
|
||||
}
|
||||
const version = getVersion(filepath, versionRefDir, env);
|
||||
metadata.version = version;
|
||||
const latestVersion = idx(env, ['versioning', 'latestVersion']);
|
||||
const versionPart =
|
||||
(version && version !== latestVersion && `${version}/`) || '';
|
||||
|
||||
/*
|
||||
Convert temporarily metadata.id to the form of dirname/id without version/lang prefix
|
||||
ex: file `versioned_docs/version-1.0.0/en/foo/bar.md` with id `version-1.0.0-bar` => `foo/bar`
|
||||
*/
|
||||
if (language) {
|
||||
metadata.id = metadata.id.replace(new RegExp(`^${language}-`), '');
|
||||
}
|
||||
|
||||
if (version) {
|
||||
metadata.id = metadata.id.replace(new RegExp(`^version-${version}-`), '');
|
||||
}
|
||||
|
||||
const dirName = path.dirname(source);
|
||||
if (dirName !== '.') {
|
||||
let prefix = dirName;
|
||||
if (language) {
|
||||
prefix = prefix.replace(new RegExp(`^${language}`), '');
|
||||
}
|
||||
prefix = prefix.replace(/^\//, '');
|
||||
if (version) {
|
||||
prefix = prefix.replace(new RegExp(`^version-${version}`), '');
|
||||
}
|
||||
prefix = prefix.replace(/^\//, '');
|
||||
if (prefix) {
|
||||
metadata.id = `${prefix}/${metadata.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
The docs file source
|
||||
e.g: `@docs/hello.md` or `@versioned_docs/version-1.0.0/hello.md`
|
||||
*/
|
||||
if (language && language !== defaultLangTag) {
|
||||
metadata.source = `@translated_docs/${source}`;
|
||||
} else if (version && version !== 'next') {
|
||||
metadata.source = `@versioned_docs/${source}`;
|
||||
} else {
|
||||
metadata.source = `@docs/${source}`;
|
||||
}
|
||||
|
||||
/* Build the permalink */
|
||||
const {baseUrl, docsUrl} = siteConfig;
|
||||
|
||||
/*
|
||||
if user has own custom permalink defined in frontmatter
|
||||
e.g: :baseUrl:docsUrl/:langPart/:versionPart/endiliey/:id
|
||||
*/
|
||||
if (metadata.permalink) {
|
||||
metadata.permalink = path.resolve(
|
||||
metadata.permalink
|
||||
.replace(/:baseUrl/, baseUrl)
|
||||
.replace(/:docsUrl/, docsUrl)
|
||||
.replace(/:langPart/, langPart)
|
||||
.replace(/:versionPart/, versionPart)
|
||||
.replace(/:id/, metadata.id)
|
||||
);
|
||||
} else {
|
||||
metadata.permalink = `${baseUrl}${docsUrl}/${langPart}${versionPart}${
|
||||
metadata.id
|
||||
}`;
|
||||
}
|
||||
|
||||
/* if version */
|
||||
if (version && version !== 'next') {
|
||||
metadata.id = `version-${version}-${metadata.id}`;
|
||||
}
|
||||
|
||||
/* save localized id before adding language on it */
|
||||
metadata.localized_id = metadata.id;
|
||||
|
||||
/* if language */
|
||||
if (language) {
|
||||
metadata.id = `${language}-${metadata.id}`;
|
||||
}
|
||||
|
||||
/* Determine order */
|
||||
const id = metadata.localized_id;
|
||||
if (order[id]) {
|
||||
metadata.sidebar = order[id].sidebar;
|
||||
metadata.category = order[id].category;
|
||||
if (order[id].next) {
|
||||
metadata.next_id = order[id].next;
|
||||
metadata.next = (language ? `${language}-` : '') + order[id].next;
|
||||
}
|
||||
if (order[id].previous) {
|
||||
metadata.previous_id = order[id].previous;
|
||||
metadata.previous = (language ? `${language}-` : '') + order[id].previous;
|
||||
}
|
||||
}
|
||||
return metadata;
|
||||
};
|
36
v2/lib/load/docs/order.js
Normal file
36
v2/lib/load/docs/order.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
// build the docs meta such as next, previous, category and sidebar
|
||||
module.exports = function createOrder(allSidebars = {}) {
|
||||
const order = {};
|
||||
if (!allSidebars) {
|
||||
return order;
|
||||
}
|
||||
Object.keys(allSidebars).forEach(sidebar => {
|
||||
const categories = allSidebars[sidebar];
|
||||
|
||||
let ids = [];
|
||||
const categoryOrder = [];
|
||||
Object.keys(categories).forEach(category => {
|
||||
ids = ids.concat(categories[category]);
|
||||
// eslint-disable-next-line
|
||||
for (let i = 0; i < categories[category].length; i++) {
|
||||
categoryOrder.push(category);
|
||||
}
|
||||
});
|
||||
|
||||
// eslint-disable-next-line
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const id = ids[i];
|
||||
let previous;
|
||||
let next;
|
||||
if (i > 0) previous = ids[i - 1];
|
||||
if (i < ids.length - 1) next = ids[i + 1];
|
||||
order[id] = {
|
||||
previous,
|
||||
next,
|
||||
sidebar,
|
||||
category: categoryOrder[i]
|
||||
};
|
||||
}
|
||||
});
|
||||
return order;
|
||||
};
|
35
v2/lib/load/docs/sidebars.js
Normal file
35
v2/lib/load/docs/sidebars.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const {idx} = require('../utils');
|
||||
|
||||
module.exports = function loadSidebars({siteDir, env}) {
|
||||
let allSidebars = {};
|
||||
|
||||
// current sidebars
|
||||
const sidebarsJSONFile = path.join(siteDir, 'sidebars.json');
|
||||
if (fs.existsSync(sidebarsJSONFile)) {
|
||||
allSidebars = require(sidebarsJSONFile); // eslint-disable-line
|
||||
}
|
||||
|
||||
// versioned sidebars
|
||||
if (idx(env, ['versioning', 'enabled'])) {
|
||||
const versions = idx(env, ['versioning', 'versions']);
|
||||
if (Array.isArray(versions)) {
|
||||
versions.forEach(version => {
|
||||
const versionedSidebarsJSONFile = path.join(
|
||||
siteDir,
|
||||
'versioned_sidebars',
|
||||
`version-${version}-sidebars.json`
|
||||
);
|
||||
if (fs.existsSync(versionedSidebarsJSONFile)) {
|
||||
const sidebar = require(versionedSidebarsJSONFile); // eslint-disable-line
|
||||
Object.assign(allSidebars, sidebar);
|
||||
} else {
|
||||
const missingFile = path.relative(siteDir, versionedSidebarsJSONFile);
|
||||
throw new Error(`Failed to load ${missingFile}. It does not exist.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return allSidebars;
|
||||
};
|
60
v2/lib/load/env.js
Normal file
60
v2/lib/load/env.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const {idx} = require('./utils');
|
||||
|
||||
module.exports = function loadEnv({siteDir, siteConfig}) {
|
||||
// Translation
|
||||
const translation = {
|
||||
enabled: false,
|
||||
enabledLanguages: [],
|
||||
defaultLanguage: {}
|
||||
};
|
||||
|
||||
const languagesFile = path.join(siteDir, 'languages.js');
|
||||
if (fs.existsSync(languagesFile)) {
|
||||
const languages = require(languagesFile); // eslint-disable-line
|
||||
|
||||
/* Enabled languages */
|
||||
const enabledLanguages = languages.filter(lang => lang.enabled);
|
||||
if (!enabledLanguages || enabledLanguages.length === 0) {
|
||||
throw new Error(`Please at least enable one language in 'languages.js'`);
|
||||
}
|
||||
translation.enabledLanguages = enabledLanguages;
|
||||
|
||||
/* Default Language */
|
||||
const {defaultLanguage: defaultLanguageTag} = siteConfig;
|
||||
const defaultLanguage = enabledLanguages.find(
|
||||
lang => lang.tag === defaultLanguageTag
|
||||
);
|
||||
if (!defaultLanguage) {
|
||||
throw new Error(
|
||||
`Please set a default language in 'siteConfig.js' which is enabled in 'languages.js'`
|
||||
);
|
||||
}
|
||||
translation.defaultLanguage = defaultLanguage;
|
||||
|
||||
translation.enabled = true;
|
||||
}
|
||||
|
||||
// Versioning
|
||||
const versioning = {
|
||||
enabled: false,
|
||||
latestVersion: null,
|
||||
defaultVersion: null,
|
||||
versions: []
|
||||
};
|
||||
|
||||
const versionsJSONFile = path.join(siteDir, 'versions.json');
|
||||
if (fs.existsSync(versionsJSONFile)) {
|
||||
versioning.enabled = true;
|
||||
versioning.versions = JSON.parse(fs.readFileSync(versionsJSONFile, 'utf8'));
|
||||
versioning.latestVersion = idx(versioning, ['versions', 0]);
|
||||
const {defaultVersionShown} = siteConfig;
|
||||
versioning.defaultVersion = defaultVersionShown || versioning.latestVersion;
|
||||
}
|
||||
|
||||
return {
|
||||
translation,
|
||||
versioning
|
||||
};
|
||||
};
|
90
v2/lib/load/index.js
Normal file
90
v2/lib/load/index.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
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 genRoutesConfig = require('./routes');
|
||||
|
||||
module.exports = async function load(siteDir) {
|
||||
// @tested - siteConfig
|
||||
const siteConfig = loadConfig(siteDir);
|
||||
await generate(
|
||||
'siteConfig.js',
|
||||
`export default ${JSON.stringify(siteConfig, null, 2)};`
|
||||
);
|
||||
|
||||
// @tested - env
|
||||
const env = loadEnv({siteDir, siteConfig});
|
||||
|
||||
// docs
|
||||
const docsDir = path.resolve(siteDir, '..', siteConfig.customDocsPath);
|
||||
const {docsMetadatas, docsSidebars} = await loadDocs({
|
||||
siteDir,
|
||||
docsDir,
|
||||
env,
|
||||
siteConfig
|
||||
});
|
||||
await generate(
|
||||
'docsMetadatas.js',
|
||||
`export default ${JSON.stringify(docsMetadatas, null, 2)};`
|
||||
);
|
||||
await generate(
|
||||
'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
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// pages
|
||||
const pagesDir = path.resolve(siteDir, 'pages');
|
||||
const pagesMetadatas = await loadPages(pagesDir);
|
||||
await generate(
|
||||
'pagesMetadatas.js',
|
||||
`export default ${JSON.stringify(pagesMetadatas, null, 2)};`
|
||||
);
|
||||
|
||||
// resolve outDir
|
||||
const outDir = path.resolve(siteDir, 'build');
|
||||
|
||||
// resolve the theme
|
||||
const themePath = loadTheme(siteDir);
|
||||
|
||||
const {baseUrl} = siteConfig;
|
||||
const versionedDir = path.join(siteDir, 'versioned_docs');
|
||||
const translatedDir = path.join(siteDir, 'translated_docs');
|
||||
|
||||
const props = {
|
||||
siteConfig,
|
||||
siteDir,
|
||||
docsDir,
|
||||
docsMetadatas,
|
||||
docsSidebars,
|
||||
env,
|
||||
pagesDir,
|
||||
pagesMetadatas,
|
||||
outDir,
|
||||
themePath,
|
||||
baseUrl,
|
||||
sourceToMetadata,
|
||||
versionedDir,
|
||||
translatedDir
|
||||
};
|
||||
|
||||
// Generate React Router Config
|
||||
const routesConfig = await genRoutesConfig(props);
|
||||
await generate('routes.js', routesConfig);
|
||||
|
||||
return props;
|
||||
};
|
18
v2/lib/load/pages.js
Normal file
18
v2/lib/load/pages.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
const globby = require('globby');
|
||||
const {encodePath, fileToPath} = require('./utils');
|
||||
|
||||
async function loadPages(pagesDir) {
|
||||
const pagesFiles = await globby(['**/*.js'], {
|
||||
cwd: pagesDir
|
||||
});
|
||||
|
||||
const pagesMetadatas = await Promise.all(
|
||||
pagesFiles.map(async source => ({
|
||||
path: encodePath(fileToPath(source)),
|
||||
source
|
||||
}))
|
||||
);
|
||||
return pagesMetadatas;
|
||||
}
|
||||
|
||||
module.exports = loadPages;
|
58
v2/lib/load/routes.js
Normal file
58
v2/lib/load/routes.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
async function genRoutesConfig({docsMetadatas = {}, pagesMetadatas = []}) {
|
||||
function genDocsRoute(metadata) {
|
||||
const {permalink, source} = metadata;
|
||||
return `
|
||||
{
|
||||
path: ${JSON.stringify(permalink)},
|
||||
exact: true,
|
||||
component: Loadable({
|
||||
loader: () => import(${JSON.stringify(source)}),
|
||||
loading: Loading,
|
||||
render(loaded, props) {
|
||||
let Content = loaded.default;
|
||||
return (
|
||||
<Docs {...props} metadata={${JSON.stringify(metadata)}}>
|
||||
<Content />
|
||||
</Docs>
|
||||
);
|
||||
}
|
||||
})
|
||||
}`;
|
||||
}
|
||||
|
||||
function genPagesRoute({path: pagesPath, source}) {
|
||||
return `
|
||||
{
|
||||
path: ${JSON.stringify(pagesPath)},
|
||||
exact: true,
|
||||
component: Loadable({
|
||||
loader: () => import('@pages/${source}'),
|
||||
loading: Loading
|
||||
})
|
||||
}`;
|
||||
}
|
||||
|
||||
const notFoundRoute = `,
|
||||
{
|
||||
path: '*',
|
||||
component: NotFound
|
||||
}`;
|
||||
|
||||
const docsRoutes = Object.values(docsMetadatas)
|
||||
.map(genDocsRoute)
|
||||
.join(',');
|
||||
|
||||
return (
|
||||
`import React from 'react';\n` +
|
||||
`import Loadable from 'react-loadable';\n` +
|
||||
`import Loading from '@theme/Loading';\n` +
|
||||
`import Docs from '@theme/Docs';\n` +
|
||||
`import NotFound from '@theme/NotFound';\n` +
|
||||
`const routes = [${docsRoutes},${pagesMetadatas
|
||||
.map(genPagesRoute)
|
||||
.join(',')}${notFoundRoute}\n];\n` +
|
||||
`export default routes;\n`
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = genRoutesConfig;
|
20
v2/lib/load/theme.js
Normal file
20
v2/lib/load/theme.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = function loadConfig(siteDir) {
|
||||
const customThemePath = path.resolve(siteDir, 'theme');
|
||||
const themePath = fs.existsSync(customThemePath)
|
||||
? customThemePath
|
||||
: path.resolve(__dirname, '../theme');
|
||||
|
||||
const themeComponents = ['Docs', 'Loading', 'NotFound', 'Markdown'];
|
||||
themeComponents.forEach(component => {
|
||||
if (!require.resolve(path.join(themePath, component))) {
|
||||
throw new Error(
|
||||
`Failed to load ${themePath}/${component}. It does not exist.`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return themePath;
|
||||
};
|
81
v2/lib/load/utils.js
Normal file
81
v2/lib/load/utils.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
const path = require('path');
|
||||
const fm = require('front-matter');
|
||||
const escapeStringRegexp = require('escape-string-regexp');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const genPath = path.resolve(__dirname, '../core/generated');
|
||||
fs.ensureDirSync(genPath);
|
||||
|
||||
const genCache = new Map();
|
||||
async function generate(file, content) {
|
||||
const cached = genCache.get(file);
|
||||
if (cached !== content) {
|
||||
await fs.writeFile(path.join(genPath, file), content);
|
||||
genCache.set(file, content);
|
||||
}
|
||||
}
|
||||
|
||||
const indexRE = /(^|.*\/)index\.(md|js)$/i;
|
||||
const extRE = /\.(md|js)$/;
|
||||
|
||||
function fileToPath(file) {
|
||||
if (indexRE.test(file)) {
|
||||
return file.replace(indexRE, '/$1');
|
||||
}
|
||||
return `/${file.replace(extRE, '').replace(/\\/g, '/')}`;
|
||||
}
|
||||
|
||||
function encodePath(userpath) {
|
||||
return userpath
|
||||
.split('/')
|
||||
.map(item => encodeURIComponent(item))
|
||||
.join('/');
|
||||
}
|
||||
|
||||
function fileToComponentName(file) {
|
||||
const ext = extRE.exec(file)[1];
|
||||
let str = file.replace(extRE, '');
|
||||
str = str.replace(/([A-Z])/g, ' $1');
|
||||
str = str.replace(/^[\W_]+|[\W_]+$/g, '').toLowerCase();
|
||||
str = str.charAt(0).toUpperCase() + str.slice(1);
|
||||
str = str.replace(/[\W_]+(\w|$)/g, (_, ch) => ch.toUpperCase());
|
||||
return ext ? ext.toUpperCase() + str : str;
|
||||
}
|
||||
|
||||
function idx(target, keyPaths) {
|
||||
return (
|
||||
target &&
|
||||
(Array.isArray(keyPaths)
|
||||
? keyPaths.reduce((obj, key) => obj && obj[key], target)
|
||||
: target[keyPaths])
|
||||
);
|
||||
}
|
||||
|
||||
function getSubFolder(file, refDir) {
|
||||
const separator = escapeStringRegexp(path.sep);
|
||||
const baseDir = escapeStringRegexp(path.basename(refDir));
|
||||
const regexSubFolder = new RegExp(
|
||||
`${baseDir}${separator}(.*?)${separator}.*`
|
||||
);
|
||||
const match = regexSubFolder.exec(file);
|
||||
return match && match[1];
|
||||
}
|
||||
|
||||
function parse(fileString) {
|
||||
if (!fm.test(fileString)) {
|
||||
return {metadata: null, content: fileString};
|
||||
}
|
||||
const {attributes: metadata, body: content} = fm(fileString);
|
||||
|
||||
return {metadata, content};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
encodePath,
|
||||
generate,
|
||||
fileToPath,
|
||||
fileToComponentName,
|
||||
getSubFolder,
|
||||
idx,
|
||||
parse
|
||||
};
|
80
v2/lib/theme/Docs/index.js
Normal file
80
v2/lib/theme/Docs/index.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import Helmet from 'react-helmet';
|
||||
import styles from './styles.css';
|
||||
import Layout from '@theme/Layout'; // eslint-disable-line
|
||||
|
||||
export default class Docs extends React.Component {
|
||||
renderSidebar(metadata, docsSidebars, docsMetadatas) {
|
||||
const {sidebar, language, id: thisID} = metadata;
|
||||
if (!sidebar || !docsSidebars) {
|
||||
return null;
|
||||
}
|
||||
const thisSidebar = docsSidebars[sidebar];
|
||||
return (
|
||||
thisSidebar &&
|
||||
Object.keys(thisSidebar).map(categoryName => {
|
||||
return (
|
||||
<div className="navGroup" key={categoryName}>
|
||||
<h3 className="navGroupCategoryTitle">{categoryName}</h3>
|
||||
<ul>
|
||||
{thisSidebar[categoryName].map(rawLinkID => {
|
||||
const linkID = (language ? `${language}-` : '') + rawLinkID;
|
||||
const linkMetadata = docsMetadatas[linkID];
|
||||
if (!linkMetadata) {
|
||||
throw new Error(
|
||||
`Improper sidebars.json file, document with id '${linkID}' not found.`
|
||||
);
|
||||
}
|
||||
const linkClassName =
|
||||
linkID === thisID ? 'navListItemActive' : 'navListItem';
|
||||
return (
|
||||
<li className={linkClassName} key={linkID}>
|
||||
<Link className="navItem" to={linkMetadata.permalink}>
|
||||
{linkMetadata.sidebar_label || linkMetadata.title}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
route,
|
||||
siteConfig,
|
||||
docsMetadatas,
|
||||
docsSidebars,
|
||||
metadata
|
||||
} = this.props;
|
||||
return (
|
||||
<Layout {...this.props}>
|
||||
<Helmet>
|
||||
<title>{(metadata && metadata.title) || siteConfig.title}</title>
|
||||
</Helmet>
|
||||
<div>{this.renderSidebar(metadata, docsSidebars, docsMetadatas)}</div>
|
||||
<div>
|
||||
{metadata.previous &&
|
||||
docsMetadatas[metadata.previous] && (
|
||||
<Link to={docsMetadatas[metadata.previous].permalink}>
|
||||
<span>← {metadata.previous_title}</span>
|
||||
</Link>
|
||||
)}
|
||||
{' ⚫️ '}
|
||||
{metadata.next &&
|
||||
docsMetadatas[metadata.next] && (
|
||||
<Link to={docsMetadatas[metadata.next].permalink}>
|
||||
<span>{metadata.next_title} →</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.mainContainer}>{this.props.children}</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
10
v2/lib/theme/Docs/styles.css
Normal file
10
v2/lib/theme/Docs/styles.css
Normal file
|
@ -0,0 +1,10 @@
|
|||
.mainContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 30px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
justify-content: center;
|
||||
}
|
29
v2/lib/theme/Layout/index.js
Normal file
29
v2/lib/theme/Layout/index.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import styles from './styles.css';
|
||||
|
||||
/* eslint-disable react/prefer-stateless-function */
|
||||
export default class Layout extends React.Component {
|
||||
render() {
|
||||
const {children, pagesMetadatas, docsMetadatas = {}, location} = this.props;
|
||||
const docsLinks = Object.values(docsMetadatas).map(data => ({
|
||||
path: `${data.permalink}`
|
||||
}));
|
||||
const routeLinks = [...pagesMetadatas, ...docsLinks].map(
|
||||
data =>
|
||||
data.path !== location.pathname && (
|
||||
<li key={data.path}>
|
||||
<Link to={data.path}>{data.path}</Link>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
{children}
|
||||
<div className={styles.footer}>
|
||||
<ul className={styles.routeLinks}>{routeLinks}</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
39
v2/lib/theme/Layout/styles.css
Normal file
39
v2/lib/theme/Layout/styles.css
Normal file
|
@ -0,0 +1,39 @@
|
|||
.footer {
|
||||
color: #777;
|
||||
margin-top: 30px;
|
||||
padding: 10px 15px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.routeLinks {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.routeLinks li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.routeLinks li a {
|
||||
color: inherit;
|
||||
margin: 3px;
|
||||
padding: 3px 7px;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.routeLinks li a.selected,
|
||||
.routeLinks li a:hover {
|
||||
border-color: rgba(175, 47, 47, 0.1);
|
||||
}
|
||||
|
||||
.routeLinks li a.selected {
|
||||
border-color: rgba(175, 47, 47, 0.2);
|
||||
}
|
11
v2/lib/theme/Loading.js
Normal file
11
v2/lib/theme/Loading.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
|
||||
export default props => {
|
||||
if (props.error) {
|
||||
return <div align="center">Error 🔥🔥🔥</div>;
|
||||
}
|
||||
if (props.pastDelay) {
|
||||
return <div align="center">Loading...</div>;
|
||||
}
|
||||
return null;
|
||||
};
|
22
v2/lib/theme/Markdown/anchors.js
Normal file
22
v2/lib/theme/Markdown/anchors.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import toSlug from './toSlug';
|
||||
|
||||
function anchors(md) {
|
||||
const originalRender = md.renderer.rules.heading_open;
|
||||
|
||||
// eslint-disable-next-line
|
||||
md.renderer.rules.heading_open = function(tokens, idx, options, env) {
|
||||
const textToken = tokens[idx + 1];
|
||||
|
||||
if (textToken.content) {
|
||||
const anchor = toSlug(textToken.content, env);
|
||||
|
||||
return `<h${
|
||||
tokens[idx].hLevel
|
||||
}><a class="anchor" aria-hidden="true" id="${anchor}"></a><a href="#${anchor}" aria-hidden="true" class="hash-link"><svg class="hash-link-icon" aria-hidden="true" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>`;
|
||||
}
|
||||
|
||||
return originalRender(tokens, idx, options, env);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = anchors;
|
22
v2/lib/theme/Markdown/highlight.js
Normal file
22
v2/lib/theme/Markdown/highlight.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
const hljs = require('highlight.js');
|
||||
const chalk = require('chalk');
|
||||
const escapeHtml = require('escape-html');
|
||||
|
||||
export default (str, rawLang) => {
|
||||
if (rawLang === 'text' || !rawLang) {
|
||||
return escapeHtml(str);
|
||||
}
|
||||
const lang = rawLang.toLowerCase();
|
||||
try {
|
||||
if (hljs.getLanguage(lang)) {
|
||||
return hljs.highlight(lang, str).value;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
chalk.yellow(
|
||||
`Highlight.js syntax highlighting for language "${lang}" is not supported.`
|
||||
)
|
||||
);
|
||||
}
|
||||
return hljs.highlightAuto(str).value;
|
||||
};
|
94
v2/lib/theme/Markdown/index.js
Normal file
94
v2/lib/theme/Markdown/index.js
Normal file
|
@ -0,0 +1,94 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import React from 'react';
|
||||
import Markdown from 'remarkable';
|
||||
import Helmet from 'react-helmet';
|
||||
import highlight from './highlight';
|
||||
import anchors from './anchors';
|
||||
|
||||
class MarkdownBlock extends React.Component {
|
||||
content() {
|
||||
if (this.props.source) {
|
||||
return (
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: this.renderMarkdown(this.props.source)
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return React.Children.map(this.props.children, child => {
|
||||
if (typeof child === 'string') {
|
||||
return (
|
||||
<span
|
||||
dangerouslySetInnerHTML={{__html: this.renderMarkdown(child)}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return child;
|
||||
});
|
||||
}
|
||||
|
||||
renderMarkdown(source) {
|
||||
const alias = {
|
||||
js: 'jsx'
|
||||
};
|
||||
const {siteConfig} = this.props;
|
||||
const md = new Markdown({
|
||||
langPrefix: 'hljs css language-',
|
||||
highlight: highlight,
|
||||
html: true,
|
||||
linkify: true
|
||||
});
|
||||
|
||||
// Register anchors plugin
|
||||
md.use(anchors);
|
||||
|
||||
// Allow client sites to register their own plugins
|
||||
if (siteConfig.markdownPlugins) {
|
||||
siteConfig.markdownPlugins.forEach(plugin => {
|
||||
md.use(plugin);
|
||||
});
|
||||
}
|
||||
|
||||
const html = md.render(source);
|
||||
|
||||
// Ensure fenced code blocks use Highlight.js hljs class
|
||||
// https://github.com/jonschlinkert/remarkable/issues/224
|
||||
return html.replace(/<pre><code>/g, '<pre><code class="hljs">');
|
||||
}
|
||||
|
||||
render() {
|
||||
const {siteConfig} = this.props;
|
||||
const highlight = Object.assign(
|
||||
{},
|
||||
{
|
||||
version: '9.12.0',
|
||||
theme: 'default'
|
||||
},
|
||||
siteConfig.highlight
|
||||
);
|
||||
|
||||
// Use user-provided themeUrl if it exists, else construct one from version and theme.
|
||||
const highlightThemeURL = highlight.themeUrl
|
||||
? highlight.themeUrl
|
||||
: `https://cdnjs.cloudflare.com/ajax/libs/highlight.js/${
|
||||
highlight.version
|
||||
}/styles/${highlight.theme}.min.css`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<link rel="stylesheet" type="text/css" href={highlightThemeURL} />
|
||||
</Helmet>
|
||||
{this.content()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MarkdownBlock.defaultProps = {
|
||||
siteConfig: {}
|
||||
};
|
||||
|
||||
export default MarkdownBlock;
|
68
v2/lib/theme/Markdown/toSlug.js
Normal file
68
v2/lib/theme/Markdown/toSlug.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
/* eslint-disable */
|
||||
|
||||
const letters =
|
||||
'\u0041-\u005A\u0061-\u007A\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC';
|
||||
const numbers =
|
||||
'\u0030-\u0039\u00B2\u00B3\u00B9\u00BC-\u00BE\u0660-\u0669\u06F0-\u06F9\u07C0-\u07C9\u0966-\u096F\u09E6-\u09EF\u09F4-\u09F9\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0B72-\u0B77\u0BE6-\u0BF2\u0C66-\u0C6F\u0C78-\u0C7E\u0CE6-\u0CEF\u0D66-\u0D75\u0E50-\u0E59\u0ED0-\u0ED9\u0F20-\u0F33\u1040-\u1049\u1090-\u1099\u1369-\u137C\u16EE-\u16F0\u17E0-\u17E9\u17F0-\u17F9\u1810-\u1819\u1946-\u194F\u19D0-\u19DA\u1A80-\u1A89\u1A90-\u1A99\u1B50-\u1B59\u1BB0-\u1BB9\u1C40-\u1C49\u1C50-\u1C59\u2070\u2074-\u2079\u2080-\u2089\u2150-\u2182\u2185-\u2189\u2460-\u249B\u24EA-\u24FF\u2776-\u2793\u2CFD\u3007\u3021-\u3029\u3038-\u303A\u3192-\u3195\u3220-\u3229\u3248-\u324F\u3251-\u325F\u3280-\u3289\u32B1-\u32BF\uA620-\uA629\uA6E6-\uA6EF\uA830-\uA835\uA8D0-\uA8D9\uA900-\uA909\uA9D0-\uA9D9\uAA50-\uAA59\uABF0-\uABF9\uFF10-\uFF19';
|
||||
const exceptAlphanum = new RegExp(`[^${[letters, numbers].join('')}]`, 'g');
|
||||
|
||||
/**
|
||||
* Converts a string to a slug, that can be used in heading anchors
|
||||
*
|
||||
* @param {string} string
|
||||
* @param {Object} [context={}] - an optional context to track used slugs and
|
||||
* ensure that new slug will be unique
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
module.exports = (string, context = {}) => {
|
||||
// var accents = "àáäâèéëêìíïîòóöôùúüûñç";
|
||||
const accents =
|
||||
'\u00e0\u00e1\u00e4\u00e2\u00e8' +
|
||||
'\u00e9\u00eb\u00ea\u00ec\u00ed\u00ef' +
|
||||
'\u00ee\u00f2\u00f3\u00f6\u00f4\u00f9' +
|
||||
'\u00fa\u00fc\u00fb\u00f1\u00e7';
|
||||
|
||||
const without = 'aaaaeeeeiiiioooouuuunc';
|
||||
|
||||
let slug = string
|
||||
.toString()
|
||||
// Handle uppercase characters
|
||||
.toLowerCase()
|
||||
// Handle accentuated characters
|
||||
.replace(new RegExp(`[${accents}]`, 'g'), c =>
|
||||
without.charAt(accents.indexOf(c))
|
||||
)
|
||||
// Replace `.`, `(` and `?` with blank string like Github does
|
||||
.replace(/\.|\(|\?/g, '')
|
||||
// Dash special characters
|
||||
.replace(exceptAlphanum, '-')
|
||||
// Compress multiple dash
|
||||
.replace(/-+/g, '-')
|
||||
// Trim dashes
|
||||
.replace(/^-|-$/g, '');
|
||||
|
||||
// Add trailing `-` if string contains ` ...` in the end like Github does
|
||||
if (/\s[.]{1,}/.test(string)) {
|
||||
slug += '-';
|
||||
}
|
||||
|
||||
if (!context.slugStats) {
|
||||
context.slugStats = {};
|
||||
}
|
||||
|
||||
if (typeof context.slugStats[slug] === 'number') {
|
||||
// search for an index, that will not clash with an existing headings
|
||||
while (
|
||||
typeof context.slugStats[`${slug}-${++context.slugStats[slug]}`] ===
|
||||
'number'
|
||||
);
|
||||
slug += `-${context.slugStats[slug]}`;
|
||||
}
|
||||
|
||||
// we are tracking both original anchors and suffixed to avoid future name
|
||||
// clashing with headings with numbers e.g. `#Foo 1` may clash with the second `#Foo`
|
||||
context.slugStats[slug] = 0;
|
||||
|
||||
return slug;
|
||||
};
|
18
v2/lib/theme/NotFound.js
Normal file
18
v2/lib/theme/NotFound.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import Layout from '@theme/Layout';
|
||||
|
||||
export default class NotFound extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Layout>
|
||||
<div>404 Page Not Found</div>
|
||||
<div>
|
||||
<img
|
||||
alt="Not found"
|
||||
src="https://d2gg9evh47fn9z.cloudfront.net/800px_COLOURBOX3889253.jpg"
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
133
v2/lib/webpack/base.js
Normal file
133
v2/lib/webpack/base.js
Normal file
|
@ -0,0 +1,133 @@
|
|||
const Config = require('webpack-chain');
|
||||
const CSSExtractPlugin = require('mini-css-extract-plugin');
|
||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
||||
const path = require('path');
|
||||
|
||||
const mdLoader = require.resolve('./loader/markdown');
|
||||
|
||||
module.exports = function createBaseConfig(props, isServer) {
|
||||
const {
|
||||
siteConfig,
|
||||
outDir,
|
||||
themePath,
|
||||
docsDir,
|
||||
pagesDir,
|
||||
siteDir,
|
||||
sourceToMetadata,
|
||||
versionedDir,
|
||||
translatedDir,
|
||||
baseUrl
|
||||
} = props;
|
||||
|
||||
const config = new Config();
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
config
|
||||
.mode(isProd ? 'production' : 'development')
|
||||
.output.path(outDir)
|
||||
.filename(isProd ? '[name].[chunkhash].js' : '[name].js')
|
||||
.publicPath(isProd ? baseUrl : '/');
|
||||
|
||||
if (!isProd) {
|
||||
config.devtool('cheap-module-eval-source-map');
|
||||
}
|
||||
|
||||
config.resolve
|
||||
.set('symlinks', true)
|
||||
.alias.set('@theme', themePath)
|
||||
.set('@site', siteDir)
|
||||
.set('@versioned_docs', versionedDir)
|
||||
.set('@translated_docs', translatedDir)
|
||||
.set('@docs', docsDir)
|
||||
.set('@pages', pagesDir)
|
||||
.set('@build', outDir)
|
||||
.set('@generated', path.resolve(__dirname, '../core/generated'))
|
||||
.set('@core', path.resolve(__dirname, '../core'))
|
||||
.end();
|
||||
|
||||
function applyBabel(rule) {
|
||||
rule
|
||||
.use('babel')
|
||||
.loader('babel-loader')
|
||||
.options({
|
||||
babelrc: false,
|
||||
presets: ['env', 'react'],
|
||||
plugins: [isServer ? 'dynamic-import-node' : 'syntax-dynamic-import']
|
||||
});
|
||||
}
|
||||
|
||||
const jsRule = config.module
|
||||
.rule('js')
|
||||
.test(/\.js$/)
|
||||
.exclude.add(filepath => {
|
||||
// Always transpile lib directory
|
||||
if (filepath.startsWith(path.join(__dirname, '..'))) {
|
||||
return false;
|
||||
}
|
||||
// Don't transpile node_modules
|
||||
return /node_modules/.test(filepath);
|
||||
})
|
||||
.end();
|
||||
|
||||
applyBabel(jsRule);
|
||||
|
||||
const mdRule = config.module.rule('markdown').test(/\.md$/);
|
||||
|
||||
applyBabel(mdRule);
|
||||
|
||||
mdRule
|
||||
.use('markdown-loader')
|
||||
.loader(mdLoader)
|
||||
.options({
|
||||
siteConfig,
|
||||
versionedDir,
|
||||
translatedDir,
|
||||
docsDir,
|
||||
sourceToMetadata
|
||||
});
|
||||
|
||||
const cssRule = config.module.rule('css').test(/\.css$/);
|
||||
if (!isServer) {
|
||||
if (isProd) {
|
||||
cssRule.use('extract-css-loader').loader(CSSExtractPlugin.loader);
|
||||
} else {
|
||||
cssRule.use('style-loader').loader('style-loader');
|
||||
}
|
||||
}
|
||||
|
||||
cssRule
|
||||
.use('css-loader')
|
||||
.loader(isServer ? 'css-loader/locals' : 'css-loader')
|
||||
.options({
|
||||
modules: true,
|
||||
importLoaders: 1,
|
||||
localIdentName: `[local]_[hash:base64:8]`,
|
||||
sourceMap: !isProd,
|
||||
minimize: true
|
||||
});
|
||||
|
||||
// mini-css-extract plugin
|
||||
config.plugin('extract-css').use(CSSExtractPlugin, [
|
||||
{
|
||||
filename: isProd ? '[name].[chunkhash].css' : '[name].css',
|
||||
chunkFilename: isProd ? '[id].[chunkhash].css' : '[id].css'
|
||||
}
|
||||
]);
|
||||
|
||||
if (isProd) {
|
||||
config.optimization.minimizer([
|
||||
new UglifyJsPlugin({
|
||||
cache: true,
|
||||
uglifyOptions: {
|
||||
warnings: false,
|
||||
compress: false,
|
||||
ecma: 6,
|
||||
mangle: true
|
||||
},
|
||||
sourceMap: true
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
30
v2/lib/webpack/client.js
Normal file
30
v2/lib/webpack/client.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
const path = require('path');
|
||||
const webpackNiceLog = require('webpack-nicelog');
|
||||
const {StatsWriterPlugin} = require('webpack-stats-plugin');
|
||||
const cleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
const createBaseConfig = require('./base');
|
||||
const {applyChainWebpack} = require('./utils');
|
||||
|
||||
module.exports = function createClientConfig(props) {
|
||||
const config = createBaseConfig(props);
|
||||
config.entry('main').add(path.resolve(__dirname, '../core/clientEntry.js'));
|
||||
|
||||
// remove/clean build folders before building bundles
|
||||
const {outDir} = props;
|
||||
config
|
||||
.plugin('clean')
|
||||
.use(cleanWebpackPlugin, [outDir, {verbose: false, allowExternal: true}]);
|
||||
|
||||
// write webpack stats object so we can pickup correct client bundle path in server.
|
||||
config
|
||||
.plugin('stats')
|
||||
.use(StatsWriterPlugin, [{filename: 'client.stats.json'}]);
|
||||
|
||||
// show compilation progress bar and build time
|
||||
config.plugin('niceLog').use(webpackNiceLog, [{name: 'Client'}]);
|
||||
|
||||
// user extended webpack-chain config
|
||||
applyChainWebpack(props.siteConfig.chainWebpack, config, false);
|
||||
|
||||
return config;
|
||||
};
|
78
v2/lib/webpack/loader/markdown.js
Normal file
78
v2/lib/webpack/loader/markdown.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
const {getOptions} = require('loader-utils');
|
||||
const fm = require('front-matter');
|
||||
|
||||
module.exports = function(fileString) {
|
||||
const options = getOptions(this);
|
||||
const {
|
||||
siteConfig,
|
||||
versionedDir,
|
||||
docsDir,
|
||||
translatedDir,
|
||||
sourceToMetadata
|
||||
} = options;
|
||||
|
||||
/* Extract content of markdown (without frontmatter) */
|
||||
const {body} = fm(fileString);
|
||||
|
||||
/* Determine the source dir. e.g: @docs, @translated_docs/ko and @versioned_docs/version-1.0.0 */
|
||||
let sourceDir;
|
||||
let thisSource = this.resourcePath;
|
||||
if (thisSource.startsWith(translatedDir)) {
|
||||
thisSource = thisSource.replace(translatedDir, '@translated_docs');
|
||||
const {language, version} = sourceToMetadata[thisSource] || {};
|
||||
if (language && version && version !== 'next') {
|
||||
sourceDir = `@translated_docs/${language}/version-${version}`;
|
||||
} else if (language && (!version || version === 'next')) {
|
||||
sourceDir = `@translated_docs/${language}`;
|
||||
}
|
||||
} else if (thisSource.startsWith(versionedDir)) {
|
||||
thisSource = thisSource.replace(versionedDir, '@versioned_docs');
|
||||
const {version} = sourceToMetadata[thisSource] || {};
|
||||
if (version) {
|
||||
sourceDir = `@versioned_docs/version-${version}`;
|
||||
}
|
||||
} else if (thisSource.startsWith(docsDir)) {
|
||||
sourceDir = `@docs`;
|
||||
}
|
||||
|
||||
/* Replace internal markdown linking (except in fenced blocks) */
|
||||
let content = body;
|
||||
if (sourceDir) {
|
||||
let fencedBlock = false;
|
||||
const lines = body.split('\n').map(line => {
|
||||
if (line.trim().startsWith('```')) {
|
||||
fencedBlock = !fencedBlock;
|
||||
}
|
||||
if (fencedBlock) return line;
|
||||
|
||||
let modifiedLine = line;
|
||||
const mdLinks = [];
|
||||
const mdRegex = /(?:\]\()(?:\.\/)?([^'")\]\s>]+\.md)/g;
|
||||
let match = mdRegex.exec(content);
|
||||
while (match !== null) {
|
||||
mdLinks.push(match[1]);
|
||||
match = mdRegex.exec(content);
|
||||
}
|
||||
mdLinks.forEach(mdLink => {
|
||||
const targetSource = `${sourceDir}/${mdLink}`;
|
||||
const {permalink} = sourceToMetadata[targetSource] || {};
|
||||
if (permalink) {
|
||||
modifiedLine = modifiedLine.replace(mdLink, permalink);
|
||||
}
|
||||
});
|
||||
return modifiedLine;
|
||||
});
|
||||
content = lines.join('\n');
|
||||
}
|
||||
|
||||
/* Return a React component */
|
||||
return (
|
||||
`import React from 'react';\n` +
|
||||
`import Markdown from '@theme/Markdown'\n` +
|
||||
`export default () => (
|
||||
<Markdown siteConfig={${JSON.stringify(siteConfig)}}>
|
||||
{${JSON.stringify(content)}}
|
||||
</Markdown>
|
||||
);`
|
||||
);
|
||||
};
|
43
v2/lib/webpack/server.js
Normal file
43
v2/lib/webpack/server.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
const path = require('path');
|
||||
const staticSiteGenerator = require('static-site-generator-webpack-plugin');
|
||||
const webpackNiceLog = require('webpack-nicelog');
|
||||
const createBaseConfig = require('./base');
|
||||
const {applyChainWebpack} = require('./utils');
|
||||
|
||||
module.exports = function createServerConfig(props) {
|
||||
const config = createBaseConfig(props, true);
|
||||
|
||||
config.entry('main').add(path.resolve(__dirname, '../core/serverEntry.js'));
|
||||
config.target('node');
|
||||
config.output.filename('server.bundle.js').libraryTarget('commonjs2');
|
||||
|
||||
// Workaround for Webpack 4 Bug (https://github.com/webpack/webpack/issues/6522)
|
||||
config.output.globalObject('this');
|
||||
|
||||
const {siteConfig, docsMetadatas, pagesMetadatas} = props;
|
||||
|
||||
// static site generator webpack plugin
|
||||
const docsLinks = Object.values(docsMetadatas).map(data => ({
|
||||
path: `${data.permalink}`
|
||||
}));
|
||||
const paths = [...docsLinks, ...pagesMetadatas].map(data => data.path);
|
||||
config.plugin('siteGenerator').use(staticSiteGenerator, [
|
||||
{
|
||||
entry: 'main',
|
||||
locals: {
|
||||
baseUrl: siteConfig.baseUrl
|
||||
},
|
||||
paths
|
||||
}
|
||||
]);
|
||||
|
||||
// show compilation progress bar and build time
|
||||
config
|
||||
.plugin('niceLog')
|
||||
.use(webpackNiceLog, [{name: 'Server', color: 'yellow'}]);
|
||||
|
||||
// user extended webpack-chain config
|
||||
applyChainWebpack(props.siteConfig.chainWebpack, config, true);
|
||||
|
||||
return config;
|
||||
};
|
27
v2/lib/webpack/utils.js
Normal file
27
v2/lib/webpack/utils.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
const merge = require('webpack-merge');
|
||||
|
||||
// Modify the generated webpack config with normal webpack config
|
||||
function applyConfigureWebpack(userConfig, config, isServer) {
|
||||
if (typeof userConfig === 'object') {
|
||||
return merge(config, userConfig);
|
||||
}
|
||||
if (typeof userConfig === 'function') {
|
||||
const res = userConfig(config, isServer);
|
||||
if (res && typeof res === 'object') {
|
||||
return merge(config, res);
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
// Modify the generated webpack config with webpack-chain API
|
||||
function applyChainWebpack(userChainWebpack, config, isServer) {
|
||||
if (userChainWebpack) {
|
||||
userChainWebpack(config, isServer);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
applyConfigureWebpack,
|
||||
applyChainWebpack
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue