docusaurus/lib/server/generate.js
Erin Teo 5771549e75 Rename latestVersion to defaultVersion (#639)
* Rename lastVersion to defaultVersion

* Added defaultVersionShown to site-config doc

* Remove defaultVersionShown from siteConfig

* Update api-site-config.md
2018-05-05 17:24:39 -07:00

539 lines
18 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.
*/
async function execute() {
const extractTranslations = require('../write-translations.js');
const CWD = process.cwd();
const cssnano = require('cssnano');
const fs = require('fs-extra');
const readMetadata = require('./readMetadata.js');
const path = require('path');
const color = require('color');
const getTOC = require('../core/getTOC.js');
const React = require('react');
const mkdirp = require('mkdirp');
const glob = require('glob');
const chalk = require('chalk');
const Site = require('../core/Site.js');
const env = require('./env.js');
const siteConfig = require(CWD + '/siteConfig.js');
const translate = require('./translate.js');
const feed = require('./feed.js');
const sitemap = require('./sitemap.js');
const join = path.join;
const sep = path.sep;
const escapeStringRegexp = require('escape-string-regexp');
const {renderToStaticMarkupWithDoctype} = require('./renderUtils');
// create the folder path for a file if it does not exist, then write the file
function writeFileAndCreateFolder(file, content) {
mkdirp.sync(path.dirname(file));
fs.writeFileSync(file, content);
}
const TABLE_OF_CONTENTS_TOKEN = '<AUTOGENERATED_TABLE_OF_CONTENTS>';
// takes the content of a doc article and returns the content with a table of
// contents inserted
const insertTableOfContents = rawContent => {
const filterRe = /^`[^`]*`/;
const headers = getTOC(rawContent, 'h3', null);
const tableOfContents = headers
.filter(header => filterRe.test(header.rawContent))
.map(header => ` - [${header.rawContent}](#${header.hashLink})`)
.join('\n');
return rawContent.replace(TABLE_OF_CONTENTS_TOKEN, tableOfContents);
};
// returns true if a file should be excluded from concatentation to
// default Docusaurus styles
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('generate.js triggered...');
// array of tags of enabled languages
const enabledLanguages = env.translation
.enabledLanguages()
.map(lang => lang.tag);
readMetadata.generateMetadataDocs();
const Metadata = require('../core/metadata.js');
// TODO: what if the project is a github org page? We should not use
// siteConfig.projectName in this case. Otherwise a GitHub org doc URL would
// look weird: https://myorg.github.io/myorg/docs
// TODO: siteConfig.projectName is a misnomer. The actual project name is
// `title`. `projectName` is only used to generate a folder, which isn't
// needed when the project's a GitHub org page
const buildDir = join(CWD, 'build', siteConfig.projectName);
// mdToHtml is a map from a markdown file name to its html link, used to
// change relative markdown links that work on GitHub into actual site links
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 DocsLayout = require('../core/DocsLayout.js');
const Redirect = require('../core/Redirect.js');
fs.removeSync(join(CWD, 'build'));
// create html files for all docs by going through all doc ids
Object.keys(Metadata).forEach(id => {
const metadata = Metadata[id];
// determine what file to use according to its id
let file;
if (metadata.original_id) {
if (env.translation.enabled && metadata.language !== 'en') {
file = join(CWD, 'translated_docs', metadata.language, metadata.source);
} else {
file = join(CWD, 'versioned_docs', metadata.source);
}
} else {
if (metadata.language === 'en') {
file = join(CWD, '..', readMetadata.getDocsPath(), metadata.source);
} else {
file = join(CWD, 'translated_docs', metadata.language, metadata.source);
}
}
if (!fs.existsSync(file)) {
return;
}
let rawContent = readMetadata.extractMetadata(fs.readFileSync(file, 'utf8'))
.rawContent;
const language = metadata.language;
// generate table of contents if appropriate
if (rawContent && rawContent.indexOf(TABLE_OF_CONTENTS_TOKEN) != -1) {
rawContent = insertTableOfContents(rawContent);
}
let defaultVersion = env.versioning.defaultVersion;
// 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 !== defaultVersion
? '/' + metadata.version + '/'
: '/'
);
// replace relative links without "./"
rawContent = rawContent.replace(
new RegExp('\\]\\(' + key, 'g'),
'](' + link
);
// replace relative links with "./"
rawContent = rawContent.replace(
new RegExp('\\]\\(\\./' + key, 'g'),
'](' + link
);
});
// replace any relative links to static assets to absolute links
rawContent = rawContent.replace(
/\]\(assets\//g,
'](' + siteConfig.baseUrl + 'docs/assets/'
);
const docComp = (
<DocsLayout metadata={metadata} language={language} config={siteConfig}>
{rawContent}
</DocsLayout>
);
const str = renderToStaticMarkupWithDoctype(docComp);
const targetFile = join(buildDir, metadata.permalink);
writeFileAndCreateFolder(targetFile, str);
// generate english page redirects when languages are enabled
if (
env.translation.enabled &&
metadata.permalink.indexOf('docs/en') !== -1
) {
const redirectComp = (
<Redirect
metadata={metadata}
language={language}
config={siteConfig}
redirect={siteConfig.baseUrl + metadata.permalink}
/>
);
const redirectStr = renderToStaticMarkupWithDoctype(redirectComp);
// create a redirects page for doc files
const redirectFile = join(
buildDir,
metadata.permalink.replace('docs/en', 'docs')
);
writeFileAndCreateFolder(redirectFile, redirectStr);
}
});
// copy docs assets if they exist
if (fs.existsSync(join(CWD, '..', readMetadata.getDocsPath(), 'assets'))) {
fs.copySync(
join(CWD, '..', readMetadata.getDocsPath(), 'assets'),
join(buildDir, 'docs', 'assets')
);
}
// create html files for all blog posts (each article)
if (fs.existsSync(join(__dirname, '..', 'core', 'MetadataBlog.js'))) {
fs.removeSync(join(__dirname, '..', 'core', 'MetadataBlog.js'));
}
readMetadata.generateMetadataBlog();
const MetadataBlog = require('../core/MetadataBlog.js');
const BlogPostLayout = require('../core/BlogPostLayout.js');
let files = glob.sync(join(CWD, 'blog', '**', '*.*'));
files
.sort()
.reverse()
.forEach(file => {
// Why normalize? In case we are on Windows.
// Remember the nuance of glob: https://www.npmjs.com/package/glob#windows
let normalizedFile = path.normalize(file);
const extension = path.extname(normalizedFile);
if (extension !== '.md' && extension !== '.markdown') {
return;
}
// convert filename to use slashes
const filePath = path
.basename(normalizedFile)
.replace('-', '/')
.replace('-', '/')
.replace('-', '/')
.replace(/\.md$/, '.html');
const result = readMetadata.extractMetadata(
fs.readFileSync(normalizedFile, {encoding: 'utf8'})
);
const rawContent = result.rawContent;
const metadata = Object.assign(
{path: filePath, content: rawContent},
result.metadata
);
metadata.id = metadata.title;
let language = 'en';
const blogPostComp = (
<BlogPostLayout
metadata={metadata}
language={language}
config={siteConfig}>
{rawContent}
</BlogPostLayout>
);
const str = renderToStaticMarkupWithDoctype(blogPostComp);
let targetFile = join(buildDir, 'blog', filePath);
writeFileAndCreateFolder(targetFile, str);
});
// create html files for all blog pages (collections of article previews)
const BlogPageLayout = require('../core/BlogPageLayout.js');
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 = (
<BlogPageLayout
metadata={metadata}
language={language}
config={siteConfig}
/>
);
const str = renderToStaticMarkupWithDoctype(blogPageComp);
let targetFile = join(
buildDir,
'blog',
page > 0 ? 'page' + (page + 1) : '',
'index.html'
);
writeFileAndCreateFolder(targetFile, str);
}
// create rss files for all blog pages, if there are any blog files
if (MetadataBlog.length > 0) {
let targetFile = join(buildDir, 'blog', 'feed.xml');
writeFileAndCreateFolder(targetFile, feed());
targetFile = join(buildDir, 'blog', 'atom.xml');
writeFileAndCreateFolder(targetFile, feed('atom'));
}
// create sitemap
if (MetadataBlog.length > 0 || Object.keys(Metadata).length > 0) {
let targetFile = join(buildDir, 'sitemap.xml');
sitemap(xml => {
writeFileAndCreateFolder(targetFile, xml);
});
}
// copy blog assets if they exist
if (fs.existsSync(join(CWD, 'blog', 'assets'))) {
fs.copySync(join(CWD, 'blog', 'assets'), join(buildDir, 'blog', 'assets'));
}
// copy all static files from docusaurus
files = glob.sync(join(__dirname, '..', 'static', '**'));
files.forEach(file => {
// Why normalize? In case we are on Windows.
// Remember the nuance of glob: https://www.npmjs.com/package/glob#windows
let targetFile = path.normalize(file);
targetFile = join(
buildDir,
targetFile.split(sep + 'static' + sep)[1] || ''
);
// parse css files to replace colors according to siteConfig
if (file.match(/\.css$/)) {
let cssContent = fs.readFileSync(file, '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);
});
const codeColor = color(siteConfig.colors.primaryColor)
.alpha(0.07)
.string();
cssContent = cssContent.replace(
new RegExp('\\$codeColor', 'g'),
codeColor
);
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
);
});
}
mkdirp.sync(path.dirname(targetFile));
fs.writeFileSync(targetFile, cssContent);
} else if (!fs.lstatSync(file).isDirectory()) {
mkdirp.sync(path.dirname(targetFile));
fs.copySync(file, targetFile);
}
});
// Copy all static files from user.
files = glob.sync(join(CWD, 'static', '**'), {dot: true});
files.forEach(file => {
// Why normalize? In case we are on Windows.
// Remember the nuance of glob: https://www.npmjs.com/package/glob#windows
let normalizedFile = path.normalize(file);
// parse css files to replace colors and fonts according to siteConfig
if (normalizedFile.match(/\.css$/) && !isSeparateCss(normalizedFile)) {
const mainCss = join(buildDir, 'css', 'main.css');
let cssContent = fs.readFileSync(normalizedFile, 'utf8');
cssContent = fs.readFileSync(mainCss, 'utf8') + '\n' + cssContent;
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
);
});
}
fs.writeFileSync(mainCss, cssContent);
} else if (!fs.lstatSync(normalizedFile).isDirectory()) {
let parts = normalizedFile.split(sep + 'static' + sep);
let targetFile = join(buildDir, parts[1]);
mkdirp.sync(path.dirname(targetFile));
fs.copySync(normalizedFile, targetFile);
}
});
// Use cssnano to minify the final combined CSS.
const mainCss = join(buildDir, 'css', 'main.css');
const cssContent = fs.readFileSync(mainCss, 'utf8');
const {css} = await cssnano.process(
cssContent,
/* postcssOpts */ {},
/* cssnanoOpts */ {
preset: 'default',
}
);
fs.writeFileSync(mainCss, css);
// compile/copy pages from user
let pagesArr = [];
files = glob.sync(join(CWD, 'pages', '**'));
files.forEach(file => {
// Why normalize? In case we are on Windows.
// Remember the nuance of glob: https://www.npmjs.com/package/glob#windows
let normalizedFile = path.normalize(file);
// render .js files to strings
if (normalizedFile.match(/\.js$/)) {
const pageID = path.basename(normalizedFile, '.js');
// make temp file for sake of require paths
const parts = normalizedFile.split('pages');
let tempFile = join(__dirname, '..', 'pages', parts[1]);
tempFile = tempFile.replace(
path.basename(normalizedFile),
'temp' + path.basename(normalizedFile)
);
mkdirp.sync(path.dirname(tempFile));
fs.copySync(normalizedFile, tempFile);
const ReactComp = require(tempFile);
let targetFile = join(buildDir, parts[1]);
targetFile = targetFile.replace(/\.js$/, '.html');
const regexLang = new RegExp(
escapeStringRegexp(sep + 'pages' + sep) +
'(.*)' +
escapeStringRegexp(sep)
);
const match = regexLang.exec(normalizedFile);
const langParts = match[1].split(sep);
if (langParts.indexOf('en') !== -1) {
// copy and compile a page for each enabled language from the English file
for (let i = 0; i < enabledLanguages.length; i++) {
let language = enabledLanguages[i];
// skip conversion from english file if a file exists for this language
if (
language !== 'en' &&
fs.existsSync(
normalizedFile.replace(sep + 'en' + sep, sep + language + sep)
)
) {
continue;
}
translate.setLanguage(language);
const str = renderToStaticMarkupWithDoctype(
<Site
language={language}
config={siteConfig}
metadata={{id: pageID}}>
<ReactComp language={language} />
</Site>
);
writeFileAndCreateFolder(
// TODO: use path functions
targetFile.replace(sep + 'en' + sep, sep + language + sep),
str
);
}
// write to base level
let language = env.translation.enabled ? 'en' : '';
translate.setLanguage(language);
const str = renderToStaticMarkupWithDoctype(
<Site language={language} config={siteConfig} metadata={{id: pageID}}>
<ReactComp language={language} />
</Site>
);
writeFileAndCreateFolder(
targetFile.replace(sep + 'en' + sep, sep),
str
);
} else {
// allow for rendering of other files not in pages/en folder
let language = env.translation.enabled ? 'en' : '';
translate.setLanguage(language);
const str = renderToStaticMarkupWithDoctype(
<Site language={language} config={siteConfig} metadata={{id: pageID}}>
<ReactComp language={language} />
</Site>
);
writeFileAndCreateFolder(targetFile.replace(sep + en + sep, sep), str);
}
fs.removeSync(tempFile);
} else if (siteConfig.wrapPagesHTML && normalizedFile.match(/\.html$/)) {
const pageID = path.basename(normalizedFile, '.html');
const parts = normalizedFile.split('pages');
const targetFile = join(buildDir, parts[1]);
const str = renderToStaticMarkupWithDoctype(
<Site language="en" config={siteConfig} metadata={{id: pageID}}>
<div
dangerouslySetInnerHTML={{
__html: fs.readFileSync(normalizedFile, {encoding: 'utf8'}),
}}
/>
</Site>
);
writeFileAndCreateFolder(targetFile, str);
} else if (!fs.lstatSync(normalizedFile).isDirectory()) {
// copy other non .js files
let parts = normalizedFile.split('pages');
let targetFile = join(buildDir, parts[1]);
mkdirp.sync(path.dirname(targetFile));
fs.copySync(normalizedFile, targetFile);
}
});
// Generate CNAME file if a custom domain is specified in siteConfig
if (siteConfig.cname) {
let targetFile = join(buildDir, 'CNAME');
fs.writeFileSync(targetFile, siteConfig.cname);
}
}
module.exports = execute;