v2: prepare to move

This commit is contained in:
endiliey 2018-09-17 11:16:07 +08:00
parent dc7ef96849
commit 45736200b0
172 changed files with 0 additions and 0 deletions

77
v2/lib/commands/build.js Normal file
View 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
View 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
View 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
View 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
View 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
});

View 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')
);
});
}

View 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
View 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;
})
);
}

View 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
View 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
View 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
View 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;

View 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
View 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;
};

View 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
View 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
View 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
View 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
View 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
View 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
View 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
};

View 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>
);
}
}

View 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;
}

View 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>
);
}
}

View 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
View 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;
};

View 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;

View 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;
};

View 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;

View 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
View 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
View 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
View 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;
};

View 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
View 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
View 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
};