docusaurus/packages/docusaurus-1.x/lib/server/server.js
Tushar Sharma bff9a53095 fix: enable live reloading title of doc (#1507)
* fix: enable live reloading title of doc

* fix: removing unnecessary reload statements

* fix: only title change should trigger a live reload

* fix: adding more properties that triggers a live reload

* fix: refactoring
2019-05-27 14:48:01 +07:00

409 lines
13 KiB
JavaScript

/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* eslint-disable no-cond-assign */
function execute(port, host) {
const extractTranslations = require('../write-translations');
const metadataUtils = require('./metadataUtils');
const blog = require('./blog');
const docs = require('./docs');
const env = require('./env.js');
const express = require('express');
const React = require('react');
const request = require('request');
const fs = require('fs-extra');
const path = require('path');
const {isSeparateCss} = require('./utils');
const mkdirp = require('mkdirp');
const glob = require('glob');
const chalk = require('chalk');
const translate = require('./translate');
const {renderToStaticMarkupWithDoctype} = require('./renderUtils');
const feed = require('./feed');
const sitemap = require('./sitemap');
const routing = require('./routing');
const loadConfig = require('./config');
const CWD = process.cwd();
const join = path.join;
const sep = path.sep;
function removeModulePathFromCache(moduleName) {
/* eslint-disable no-underscore-dangle */
Object.keys(module.constructor._pathCache).forEach(cacheKey => {
if (cacheKey.indexOf(moduleName) > 0) {
delete module.constructor._pathCache[cacheKey];
}
});
}
// Remove a module and child modules from require cache, so server
// does not have to be restarted.
function removeModuleAndChildrenFromCache(moduleName) {
let mod = require.resolve(moduleName);
if (mod && (mod = require.cache[mod])) {
mod.children.forEach(child => {
delete require.cache[child.id];
removeModulePathFromCache(mod.id);
});
delete require.cache[mod.id];
removeModulePathFromCache(mod.id);
}
}
const readMetadata = require('./readMetadata.js');
let Metadata;
let MetadataBlog;
let siteConfig;
function reloadSiteConfig() {
const siteConfigPath = join(CWD, 'siteConfig.js');
removeModuleAndChildrenFromCache(siteConfigPath);
const oldBaseUrl = siteConfig && siteConfig.baseUrl;
siteConfig = loadConfig(siteConfigPath);
if (oldBaseUrl && oldBaseUrl !== siteConfig.baseUrl) {
console.log('Base url has changed. Please restart server ...');
process.exit();
}
}
function reloadMetadata() {
removeModuleAndChildrenFromCache('./readMetadata.js');
readMetadata.generateMetadataDocs();
removeModuleAndChildrenFromCache('../core/metadata.js');
Metadata = require('../core/metadata.js');
}
function reloadMetadataBlog() {
if (fs.existsSync(join(__dirname, '..', 'core', 'MetadataBlog.js'))) {
removeModuleAndChildrenFromCache(join('..', 'core', 'MetadataBlog.js'));
fs.removeSync(join(__dirname, '..', 'core', 'MetadataBlog.js'));
}
reloadSiteConfig();
readMetadata.generateMetadataBlog(siteConfig);
MetadataBlog = require(join('..', 'core', 'MetadataBlog.js'));
}
function reloadTranslations() {
removeModuleAndChildrenFromCache('./translation.js');
}
function requestFile(url, res, notFoundCallback) {
request.get(url, (error, response, body) => {
if (!error) {
if (response) {
if (response.statusCode === 404 && notFoundCallback) {
notFoundCallback();
} else {
res.status(response.statusCode).send(body);
}
} else {
console.error('No response');
}
} else {
console.error('Request failed:', error);
}
});
}
reloadMetadata();
reloadMetadataBlog();
extractTranslations();
reloadSiteConfig();
const app = express();
app.get(routing.docs(siteConfig), (req, res, next) => {
const url = decodeURI(req.path.toString().replace(siteConfig.baseUrl, ''));
const metakey = Object.keys(Metadata).find(
id => Metadata[id].permalink === url,
);
let metadata = Metadata[metakey];
const file = docs.getFile(metadata);
if (!file) {
next();
return;
}
const {rawContent, metadata: rawMetadata} = metadataUtils.extractMetadata(
file,
);
// if any of the followings is changed, reload the metadata
const reloadTriggers = ['sidebar_label', 'hide_title', 'title'];
if (reloadTriggers.some(key => metadata[key] !== rawMetadata[key])) {
reloadMetadata();
extractTranslations();
reloadTranslations();
metadata = Metadata[metakey];
}
reloadSiteConfig();
removeModuleAndChildrenFromCache('../core/DocsLayout.js');
const mdToHtml = metadataUtils.mdToHtml(Metadata, siteConfig);
res.send(docs.getMarkup(rawContent, mdToHtml, metadata, siteConfig));
});
app.get(routing.sitemap(siteConfig), (req, res) => {
sitemap((err, xml) => {
if (err) {
res.status(500).send('Sitemap error');
} else {
res.set('Content-Type', 'application/xml');
res.send(xml);
}
});
});
app.get(routing.feed(siteConfig), (req, res, next) => {
res.set('Content-Type', 'application/rss+xml');
const file = req.path
.toString()
.split('blog/')[1]
.toLowerCase();
if (file === 'atom.xml') {
res.send(feed('atom'));
} else if (file === 'feed.xml') {
res.send(feed('rss'));
}
next();
});
app.get(routing.blog(siteConfig), (req, res, next) => {
// Regenerate the blog metadata in case it has changed. Consider improving
// this to regenerate on file save rather than on page request.
reloadMetadataBlog();
removeModuleAndChildrenFromCache(join('..', 'core', 'BlogPageLayout.js'));
const blogPages = blog.getPagesMarkup(MetadataBlog.length, siteConfig);
const urlPath = req.path.toString().split('blog/')[1];
if (urlPath === 'index.html') {
res.send(blogPages['/index.html']);
} else if (urlPath.endsWith('/index.html') && blogPages[urlPath]) {
res.send(blogPages[urlPath]);
} else if (urlPath.match(/page([0-9]+)/)) {
res.send(blogPages[`${urlPath.replace(/\/$/, '')}/index.html`]);
} else {
const file = join(CWD, 'blog', blog.urlToSource(urlPath));
removeModuleAndChildrenFromCache(join('..', 'core', 'BlogPostLayout.js'));
const blogPost = blog.getPostMarkup(file, siteConfig);
if (!blogPost) {
next();
return;
}
res.send(blogPost);
}
});
app.get(routing.page(siteConfig), (req, res, next) => {
reloadSiteConfig();
// Look for user-provided HTML file first.
let htmlFile = req.path.toString().replace(siteConfig.baseUrl, '');
htmlFile = join(CWD, 'pages', htmlFile);
if (
fs.existsSync(htmlFile) ||
fs.existsSync(
(htmlFile = htmlFile.replace(
path.basename(htmlFile),
join('en', path.basename(htmlFile)),
)),
)
) {
if (siteConfig.wrapPagesHTML) {
removeModuleAndChildrenFromCache(join('..', 'core', 'Site.js'));
const Site = require(join('..', 'core', 'Site.js'));
const str = renderToStaticMarkupWithDoctype(
<Site
language="en"
config={siteConfig}
metadata={{id: path.basename(htmlFile, '.html')}}>
<div
dangerouslySetInnerHTML={{
__html: fs.readFileSync(htmlFile, {encoding: 'utf8'}),
}}
/>
</Site>,
);
res.send(str);
} else {
res.send(fs.readFileSync(htmlFile, {encoding: 'utf8'}));
}
next();
return;
}
// look for user provided react file either in specified path or in path for english files
let file = req.path.toString().replace(/\.html$/, '.js');
file = file.replace(siteConfig.baseUrl, '');
let userFile = join(CWD, 'pages', file);
let language = env.translation.enabled ? 'en' : '';
const regexLang = /(.*)\/.*\.html$/;
const match = regexLang.exec(req.path);
const parts = match[1].split('/');
const enabledLangTags = env.translation
.enabledLanguages()
.map(lang => lang.tag);
for (let i = 0; i < parts.length; i++) {
if (enabledLangTags.indexOf(parts[i]) !== -1) {
language = parts[i];
}
}
let englishFile = join(CWD, 'pages', file);
if (language && language !== 'en') {
englishFile = englishFile.replace(sep + language + sep, `${sep}en${sep}`);
}
// check for: a file for the page, an english file for page with unspecified language, or an
// english file for the page
if (
fs.existsSync(userFile) ||
fs.existsSync(
(userFile = userFile.replace(
path.basename(userFile),
`en${sep}${path.basename(userFile)}`,
)),
) ||
fs.existsSync((userFile = englishFile))
) {
// copy into docusaurus so require paths work
const userFileParts = userFile.split(`pages${sep}`);
let tempFile = join(__dirname, '..', 'pages', userFileParts[1]);
tempFile = tempFile.replace(
path.basename(file),
`temp${path.basename(file)}`,
);
mkdirp.sync(path.dirname(tempFile));
fs.copySync(userFile, tempFile);
// render into a string
removeModuleAndChildrenFromCache(tempFile);
const ReactComp = require(tempFile);
removeModuleAndChildrenFromCache(join('..', 'core', 'Site.js'));
const Site = require(join('..', 'core', 'Site.js'));
translate.setLanguage(language);
const str = renderToStaticMarkupWithDoctype(
<Site
language={language}
config={siteConfig}
title={ReactComp.title}
description={ReactComp.description}
metadata={{id: path.basename(userFile, '.js')}}>
<ReactComp config={siteConfig} language={language} />
</Site>,
);
fs.removeSync(tempFile);
res.send(str);
} else {
next();
}
});
app.get(`${siteConfig.baseUrl}css/main.css`, (req, res) => {
const mainCssPath = join(
__dirname,
'..',
'static',
req.path.toString().replace(siteConfig.baseUrl, '/'),
);
let cssContent = fs.readFileSync(mainCssPath, {encoding: 'utf8'});
const files = glob.sync(join(CWD, 'static', '**', '*.css'));
files.forEach(file => {
if (isSeparateCss(file, siteConfig.separateCss)) {
return;
}
cssContent = `${cssContent}\n${fs.readFileSync(file, {
encoding: 'utf8',
})}`;
});
if (
!siteConfig.colors ||
!siteConfig.colors.primaryColor ||
!siteConfig.colors.secondaryColor
) {
console.error(
`${chalk.yellow(
'Missing color configuration.',
)} Make sure siteConfig.colors includes primaryColor and secondaryColor fields.`,
);
}
Object.keys(siteConfig.colors).forEach(key => {
const color = siteConfig.colors[key];
cssContent = cssContent.replace(new RegExp(`\\$${key}`, 'g'), color);
});
if (siteConfig.fonts) {
Object.keys(siteConfig.fonts).forEach(key => {
const fontString = siteConfig.fonts[key]
.map(font => `"${font}"`)
.join(', ');
cssContent = cssContent.replace(
new RegExp(`\\$${key}`, 'g'),
fontString,
);
});
}
res.header('Content-Type', 'text/css');
res.send(cssContent);
});
// serve static assets from these locations
app.use(
`${siteConfig.baseUrl}${
siteConfig.docsUrl ? `${siteConfig.docsUrl}/` : ''
}assets`,
express.static(join(CWD, '..', readMetadata.getDocsPath(), 'assets')),
);
app.use(
`${siteConfig.baseUrl}blog/assets`,
express.static(join(CWD, 'blog', 'assets')),
);
app.use(siteConfig.baseUrl, express.static(join(CWD, 'static')));
app.use(siteConfig.baseUrl, express.static(join(__dirname, '..', 'static')));
// "redirect" requests to pages ending with "/" or no extension so that,
// for example, request to "blog" returns "blog/index.html" or "blog.html"
app.get(routing.noExtension(), (req, res, next) => {
const slash = req.path.toString().endsWith('/') ? '' : '/';
const requestUrl = `http://${host}:${port}${req.path}`;
requestFile(`${requestUrl + slash}index.html`, res, () => {
requestFile(
slash === '/'
? `${requestUrl}.html`
: requestUrl.replace(/\/$/, '.html'),
res,
next,
);
});
});
// handle special cleanUrl case like '/blog/1.2.3' & '/blog.robots.hai'
// where we should try to serve '/blog/1.2.3.html' & '/blog.robots.hai.html'
app.get(routing.dotfiles(), (req, res, next) => {
if (!siteConfig.cleanUrl) {
next();
return;
}
requestFile(`http://${host}:${port}${req.path}.html`, res, next);
});
app.listen(port);
}
module.exports = execute;