/** * Copyright (c) 2017-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ function execute(port) { const translation = require("./translation.js"); const express = require("express"); const React = require("react"); const request = require("request"); const renderToStaticMarkup = require("react-dom/server").renderToStaticMarkup; const fs = require("fs-extra"); const os = require("os"); const path = require("path"); const toSlug = require("../core/toSlug.js"); const mkdirp = require("mkdirp"); const glob = require("glob"); const translate = require("./translate.js"); const versionFallback = require("./versionFallback"); const CWD = process.cwd(); const ENABLE_TRANSLATION = fs.existsSync(CWD + "/languages.js"); const ENABLE_VERSIONING = fs.existsSync(CWD + "/versions.json"); let siteConfig = require(CWD + "/siteConfig.js"); /** * Removes a module from the cache */ function purgeCache(moduleName) { // Traverse the cache looking for the files // loaded by the specified module name searchCache(moduleName, function(mod) { delete require.cache[mod.id]; }); // Remove cached paths to the module. Object.keys(module.constructor._pathCache).forEach(function(cacheKey) { if (cacheKey.indexOf(moduleName) > 0) { delete module.constructor._pathCache[cacheKey]; } }); } /** * Traverses the cache to search for all the cached * files of the specified module name */ function searchCache(moduleName, callback) { // Resolve the module identified by the specified name let mod = require.resolve(moduleName); // Check if the module has been resolved and found within // the cache if (mod && (mod = require.cache[mod]) !== undefined) { // Recursively go over the results (function traverse(mod) { // Go over each of the module's children and // traverse them mod.children.forEach(function(child) { traverse(child); }); // Call the specified callback providing the // found cached module callback(mod); })(mod); } } /****************************************************************************/ let readMetadata; let Metadata; function reloadMetadata() { purgeCache("./readMetadata.js"); readMetadata = require("./readMetadata.js"); readMetadata.generateDocsMetadata(); purgeCache("../core/metadata.js"); Metadata = require("../core/metadata.js"); } /****************************************************************************/ const TABLE_OF_CONTENTS_TOKEN = ""; const insertTableOfContents = rawContent => { const regexp = /\n###\s+(`.*`.*)\n/g; let match; const headers = []; while ((match = regexp.exec(rawContent))) { headers.push(match[1]); } const tableOfContents = headers .map(header => ` - [${header}](#${toSlug(header)})`) .join("\n"); return rawContent.replace(TABLE_OF_CONTENTS_TOKEN, tableOfContents); }; /****************************************************************************/ function isSeparateCss(file) { if (!siteConfig.separateCss) { return false; } for (let i = 0; i < siteConfig.separateCss.length; i++) { if (file.includes(siteConfig.separateCss[i])) { return true; } } return false; } /****************************************************************************/ console.log("server.js triggered..."); reloadMetadata(); /* handle all requests for document pages */ const app = express().get(/docs\/.*html$/, (req, res, next) => { purgeCache(CWD + "/siteConfig.js"); siteConfig = require(CWD + "/siteConfig.js"); let url = req.path.toString().replace(siteConfig.baseUrl, ""); reloadMetadata(); // links is a map from a permalink to an id let links = {}; Object.keys(Metadata).forEach(id => { const metadata = Metadata[id]; links[metadata.permalink] = id; }); const mdToHtml = {}; Object.keys(Metadata).forEach(id => { const metadata = Metadata[id]; if (metadata.language !== "en" || metadata.original_id) { return; } let htmlLink = siteConfig.baseUrl + metadata.permalink.replace("/next/", "/"); if (htmlLink.includes("/docs/en/")) { htmlLink = htmlLink.replace("/docs/en/", "/docs/en/VERSION/"); } else { htmlLink = htmlLink.replace("/docs/", "/docs/VERSION/"); } mdToHtml[metadata.source] = htmlLink; }); const metadata = Metadata[links[url]]; if (!metadata) { next(); return; } const language = metadata.language; let file; if (metadata.original_id) { if (ENABLE_TRANSLATION && metadata.language !== "en") { file = CWD + "/translated_docs/" + metadata.language + "/" + metadata.source; } else { file = CWD + "/versioned_docs/" + metadata.source; } } else { if (metadata.language === "en") { file = CWD + "/../docs/" + metadata.source; } else { file = CWD + "/translated_docs/" + metadata.language + "/" + metadata.source; } } if (!fs.existsSync(file)) { next(); return; } let rawContent = readMetadata.extractMetadata(fs.readFileSync(file, "utf8")) .rawContent; /* generate table of contents if appropriate */ if (rawContent && rawContent.indexOf(TABLE_OF_CONTENTS_TOKEN) !== -1) { rawContent = insertTableOfContents(rawContent); } let latestVersion; if (ENABLE_VERSIONING) { latestVersion = JSON.parse( fs.readFileSync(CWD + "/versions.json", "utf8") )[0]; } /* replace any links to markdown files to their website html links */ Object.keys(mdToHtml).forEach(function(key, index) { let link = mdToHtml[key]; link = link.replace("/en/", "/" + language + "/"); link = link.replace( "/VERSION/", metadata.version && metadata.version !== latestVersion ? "/" + metadata.version + "/" : "/" ); rawContent = rawContent.replace( new RegExp("\\]\\(" + key, "g"), "](" + link ); }); rawContent = rawContent.replace( /\]\(assets\//g, "](" + siteConfig.baseUrl + "docs/assets/" ); purgeCache("../core/DocsLayout.js"); const DocsLayout = require("../core/DocsLayout.js"); const docComp = ( {rawContent} ); res.send(renderToStaticMarkup(docComp)); }); /* handle all requests for blog pages and posts */ app.get(/blog\/.*html$/, (req, res) => { purgeCache(CWD + "/siteConfig.js"); siteConfig = require(CWD + "/siteConfig.js"); if (fs.existsSync(__dirname + "/../core/MetadataBlog.js")) { purgeCache("../core/MetadataBlog.js"); fs.removeSync(__dirname + "/../core/MetadataBlog.js"); } readMetadata.generateBlogMetadata(); const MetadataBlog = require("../core/MetadataBlog.js"); /* generate all of the blog pages */ purgeCache("../core/BlogPageLayout.js"); const BlogPageLayout = require("../core/BlogPageLayout.js"); const blogPages = {}; /* make blog pages with 10 posts per page */ const perPage = 10; for ( let page = 0; page < Math.ceil(MetadataBlog.length / perPage); page++ ) { let language = "en"; const metadata = { page: page, perPage: perPage }; const blogPageComp = ( ); const str = renderToStaticMarkup(blogPageComp); let path = (page > 0 ? "page" + (page + 1) : "") + "/index.html"; blogPages[path] = str; } let parts = req.path.toString().split("blog/"); // send corresponding blog page if appropriate if (parts[1] === "index.html") { res.send(blogPages["/index.html"]); } else if (parts[1].endsWith("/index.html")) { res.send(blogPages[parts[1]]); } else if (parts[1].match(/page([0-9]+)/)) { if (parts[1].endsWith("/")) { res.send(blogPages[parts[1] + "index.html"]); } else { res.send(blogPages[parts[1] + "/index.html"]); } } else { // else send corresponding blog post let file = parts[1]; file = file.replace(/\.html$/, ".md"); file = file.replace(new RegExp("/", "g"), "-"); file = CWD + "/blog/" + file; const result = readMetadata.extractMetadata( fs.readFileSync(file, { encoding: "utf8" }) ); let rawContent = result.rawContent; rawContent = rawContent.replace( /\]\(assets\//g, "](" + siteConfig.baseUrl + "blog/assets/" ); const metadata = Object.assign( { path: req.path.toString().split("blog/")[1], content: rawContent }, result.metadata ); metadata.id = metadata.title; let language = "en"; purgeCache("../core/BlogPostLayout.js"); const BlogPostLayout = require("../core/BlogPostLayout.js"); const blogPostComp = ( {rawContent} ); res.send(renderToStaticMarkup(blogPostComp)); } }); /* handle all other main pages */ app.get("*.html", (req, res, next) => { purgeCache(CWD + "/siteConfig.js"); siteConfig = require(CWD + "/siteConfig.js"); /* look for user provided html file first */ let htmlFile = req.path.toString().replace(siteConfig.baseUrl, ""); htmlFile = CWD + "/pages/" + htmlFile; if ( fs.existsSync(htmlFile) || fs.existsSync( (htmlFile = htmlFile.replace( path.basename(htmlFile), "en/" + path.basename(htmlFile) )) ) ) { res.send(fs.readFileSync(htmlFile, { encoding: "utf8" })); 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 = CWD + "/pages/" + file; let language = "en"; const regexLang = /(.*)\/.*\.html$/; const match = regexLang.exec(req.path); const parts = match[1].split("/"); const enabledLangTags = []; for (let i = 0; i < translation["languages"].length; i++) { enabledLangTags.push(translation["languages"][i].tag); } for (let i = 0; i < parts.length; i++) { if (enabledLangTags.indexOf(parts[i]) !== -1) { language = parts[i]; } } let englishFile = CWD + "/pages/" + file; if (language !== "en") { englishFile = englishFile.replace("/" + language + "/", "/en/"); } /* check for: a file for the page, an english file for page with unspecified language, english file for the page */ if ( fs.existsSync(userFile) || fs.existsSync( (userFile = userFile.replace( path.basename(userFile), "en/" + path.basename(userFile) )) ) || fs.existsSync((userFile = englishFile)) ) { /* copy into docusaurus so require paths work */ let parts = userFile.split("pages/"); let tempFile = __dirname + "/../pages/" + parts[1]; tempFile = tempFile.replace( path.basename(file), "temp" + path.basename(file) ); mkdirp.sync(tempFile.replace(new RegExp("/[^/]*$"), "")); fs.copySync(userFile, tempFile); /* render into a string */ purgeCache(tempFile); const ReactComp = require(tempFile); purgeCache("../core/Site.js"); const Site = require("../core/Site.js"); translate.setLanguage(language); const str = renderToStaticMarkup( ); fs.removeSync(tempFile); res.send(str); } else { next(); return; } }); /* generate the main.css file by concatenating user provided css to the end */ app.get(/main\.css$/, (req, res) => { const mainCssPath = __dirname + "/../static/" + req.path.toString().replace(siteConfig.baseUrl, "/"); let cssContent = fs.readFileSync(mainCssPath, { encoding: "utf8" }); let files = glob.sync(CWD + "/static/**/*.css"); files.forEach(file => { if (isSeparateCss(file)) { return; } cssContent = cssContent + "\n" + fs.readFileSync(file, { encoding: "utf8" }); }); cssContent = cssContent .toString() .replace( new RegExp("{primaryColor}", "g"), siteConfig.colors.primaryColor ); cssContent = cssContent.replace( new RegExp("{secondaryColor}", "g"), siteConfig.colors.secondaryColor ); cssContent = cssContent.replace( new RegExp("{prismColor}", "g"), siteConfig.colors.prismColor ); res.send(cssContent); }); app.use( siteConfig.baseUrl + "docs/assets/", express.static(CWD + "/../docs/assets") ); app.use( siteConfig.baseUrl + "blog/assets/", express.static(CWD + "/blog/assets") ); app.use(siteConfig.baseUrl, express.static(CWD + "/static")); app.use(siteConfig.baseUrl, express.static(__dirname + "/../static")); app.get(/\/[^\.]*\/?$/, (req, res) => { if (req.path.toString().endsWith("/")) { request.get( "http://localhost:" + port + req.path + "index.html", (err, response, body) => { if (!err) { res.send(body); } } ); } else { request.get( "http://localhost:" + port + req.path + "/index.html", (err, response, body) => { if (!err) { res.send(body); } } ); } }); app.listen(port); console.log("listening on port: " + port); console.log("Open http://localhost:" + port + "/"); } module.exports = execute;