Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/docusaurus-types/src/context.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type GlobalData = {[pluginName: string]: {[pluginId: string]: unknown}};

export type LoadContext = {
siteDir: string;
siteVersion: string | undefined;
generatedFilesDir: string;
siteConfig: DocusaurusConfig;
siteConfigPath: string;
Expand Down
3 changes: 2 additions & 1 deletion packages/docusaurus-types/src/plugin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/

import type {TranslationFile} from './i18n';
import type {CodeTranslations, TranslationFile} from './i18n';
import type {RuleSetRule, Configuration as WebpackConfiguration} from 'webpack';
import type {CustomizeRuleString} from 'webpack-merge/dist/types';
import type {CommanderStatic} from 'commander';
Expand Down Expand Up @@ -185,6 +185,7 @@ export type LoadedPlugin = InitializedPlugin & {
readonly content: unknown;
readonly globalData: unknown;
readonly routes: RouteConfig[];
readonly defaultCodeTranslations: CodeTranslations;
};

export type PluginModule<Content = unknown> = {
Expand Down
10 changes: 7 additions & 3 deletions packages/docusaurus-utils/src/emitUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {findAsyncSequential} from './jsUtils';

const fileHash = new Map<string, string>();

const hashContent = (content: string): string => {
return createHash('md5').update(content).digest('hex');
};

/**
* Outputs a file to the generated files directory. Only writes files if content
* differs from cache (for hot reload performance).
Expand All @@ -38,7 +42,7 @@ export async function generate(
// first "A" remains in cache. But if the file never existed in cache, no
// need to register it.
if (fileHash.get(filepath)) {
fileHash.set(filepath, createHash('md5').update(content).digest('hex'));
fileHash.set(filepath, hashContent(content));
}
return;
}
Expand All @@ -50,11 +54,11 @@ export async function generate(
// overwriting and we can reuse old file.
if (!lastHash && (await fs.pathExists(filepath))) {
const lastContent = await fs.readFile(filepath, 'utf8');
lastHash = createHash('md5').update(lastContent).digest('hex');
lastHash = hashContent(lastContent);
fileHash.set(filepath, lastHash);
}

const currentHash = createHash('md5').update(content).digest('hex');
const currentHash = hashContent(content);

if (lastHash !== currentHash) {
await fs.outputFile(filepath, content);
Expand Down
15 changes: 12 additions & 3 deletions packages/docusaurus-utils/src/gitUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@
*/

import path from 'path';
import shell from 'shelljs';
import fs from 'fs-extra';
import _ from 'lodash';
import shell from 'shelljs'; // TODO replace with async-first version

const realHasGitFn = () => !!shell.which('git');

// The hasGit call is synchronous IO so we memoize it
// The user won't install Git in the middle of a build anyway...
const hasGit =
process.env.NODE_ENV === 'test' ? realHasGitFn : _.memoize(realHasGitFn);

/** Custom error thrown when git is not found in `PATH`. */
export class GitNotFoundError extends Error {}
Expand Down Expand Up @@ -86,13 +95,13 @@ export async function getFileCommitDate(
timestamp: number;
author?: string;
}> {
if (!shell.which('git')) {
if (!hasGit()) {
throw new GitNotFoundError(
`Failed to retrieve git history for "${file}" because git is not installed.`,
);
}

if (!shell.test('-f', file)) {
if (!(await fs.pathExists(file))) {
throw new Error(
`Failed to retrieve git history for "${file}" because the file does not exist.`,
);
Expand Down
208 changes: 94 additions & 114 deletions packages/docusaurus/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,23 +64,15 @@ export async function build(
process.on(sig, () => process.exit());
});

async function tryToBuildLocale({
locale,
isLastLocale,
}: {
locale: string;
isLastLocale: boolean;
}) {
async function tryToBuildLocale({locale}: {locale: string}) {
try {
PerfLogger.start(`Building site for locale ${locale}`);
await buildLocale({
siteDir,
locale,
cliOptions,
forceTerminate,
isLastLocale,
});
PerfLogger.end(`Building site for locale ${locale}`);
await PerfLogger.async(`${logger.name(locale)}`, () =>
buildLocale({
siteDir,
locale,
cliOptions,
}),
);
} catch (err) {
throw new Error(
logger.interpolate`Unable to build website for locale name=${locale}.`,
Expand All @@ -91,20 +83,28 @@ export async function build(
}
}

PerfLogger.start(`Get locales to build`);
const locales = await getLocalesToBuild({siteDir, cliOptions});
PerfLogger.end(`Get locales to build`);
const locales = await PerfLogger.async('Get locales to build', () =>
getLocalesToBuild({siteDir, cliOptions}),
);

if (locales.length > 1) {
logger.info`Website will be built for all these locales: ${locales}`;
}

PerfLogger.start(`Building ${locales.length} locales`);
await mapAsyncSequential(locales, (locale) => {
const isLastLocale = locales.indexOf(locale) === locales.length - 1;
return tryToBuildLocale({locale, isLastLocale});
});
PerfLogger.end(`Building ${locales.length} locales`);
await PerfLogger.async(`Build`, () =>
mapAsyncSequential(locales, async (locale) => {
const isLastLocale = locales.indexOf(locale) === locales.length - 1;
await tryToBuildLocale({locale});
if (isLastLocale) {
logger.info`Use code=${'npm run serve'} command to test your build locally.`;
}

// TODO do we really need this historical forceTerminate exit???
if (forceTerminate && isLastLocale && !cliOptions.bundleAnalyzer) {
process.exit(0);
}
}),
);
}

async function getLocalesToBuild({
Expand Down Expand Up @@ -144,14 +144,10 @@ async function buildLocale({
siteDir,
locale,
cliOptions,
forceTerminate,
isLastLocale,
}: {
siteDir: string;
locale: string;
cliOptions: Partial<BuildCLIOptions>;
forceTerminate: boolean;
isLastLocale: boolean;
}): Promise<string> {
// Temporary workaround to unlock the ability to translate the site config
// We'll remove it if a better official API can be designed
Expand All @@ -160,81 +156,66 @@ async function buildLocale({

logger.info`name=${`[${locale}]`} Creating an optimized production build...`;

PerfLogger.start('Loading site');
const site = await loadSite({
siteDir,
outDir: cliOptions.outDir,
config: cliOptions.config,
locale,
localizePath: cliOptions.locale ? false : undefined,
});
PerfLogger.end('Loading site');
const site = await PerfLogger.async('Load site', () =>
loadSite({
siteDir,
outDir: cliOptions.outDir,
config: cliOptions.config,
locale,
localizePath: cliOptions.locale ? false : undefined,
}),
);

const {props} = site;
const {outDir, plugins} = props;

// We can build the 2 configs in parallel
PerfLogger.start('Creating webpack configs');
const [{clientConfig, clientManifestPath}, {serverConfig, serverBundlePath}] =
await Promise.all([
getBuildClientConfig({
props,
cliOptions,
}),
getBuildServerConfig({
props,
}),
]);
PerfLogger.end('Creating webpack configs');

// Make sure generated client-manifest is cleaned first, so we don't reuse
// the one from previous builds.
// TODO do we really need this? .docusaurus folder is cleaned between builds
PerfLogger.start('Deleting previous client manifest');
await ensureUnlink(clientManifestPath);
PerfLogger.end('Deleting previous client manifest');
await PerfLogger.async('Creating webpack configs', () =>
Promise.all([
getBuildClientConfig({
props,
cliOptions,
}),
getBuildServerConfig({
props,
}),
]),
);

// Run webpack to build JS bundle (client) and static html files (server).
PerfLogger.start('Bundling');
await compile([clientConfig, serverConfig]);
PerfLogger.end('Bundling');
await PerfLogger.async('Bundling with Webpack', () =>
compile([clientConfig, serverConfig]),
);

PerfLogger.start('Executing static site generation');
const {collectedData} = await executeSSG({
props,
serverBundlePath,
clientManifestPath,
});
PerfLogger.end('Executing static site generation');
const {collectedData} = await PerfLogger.async('SSG', () =>
executeSSG({
props,
serverBundlePath,
clientManifestPath,
}),
);

// Remove server.bundle.js because it is not needed.
PerfLogger.start('Deleting server bundle');
await ensureUnlink(serverBundlePath);
PerfLogger.end('Deleting server bundle');
await PerfLogger.async('Deleting server bundle', () =>
ensureUnlink(serverBundlePath),
);

// Plugin Lifecycle - postBuild.
PerfLogger.start('Executing postBuild()');
await executePluginsPostBuild({plugins, props, collectedData});
PerfLogger.end('Executing postBuild()');
await PerfLogger.async('postBuild()', () =>
executePluginsPostBuild({plugins, props, collectedData}),
);

// TODO execute this in parallel to postBuild?
PerfLogger.start('Executing broken links checker');
await executeBrokenLinksCheck({props, collectedData});
PerfLogger.end('Executing broken links checker');
await PerfLogger.async('Broken links checker', () =>
executeBrokenLinksCheck({props, collectedData}),
);

logger.success`Generated static files in path=${path.relative(
process.cwd(),
outDir,
)}.`;

if (isLastLocale) {
logger.info`Use code=${'npm run serve'} command to test your build locally.`;
}

if (forceTerminate && isLastLocale && !cliOptions.bundleAnalyzer) {
process.exit(0);
}

return outDir;
}

Expand All @@ -247,40 +228,39 @@ async function executeSSG({
serverBundlePath: string;
clientManifestPath: string;
}) {
PerfLogger.start('Reading client manifest');
const manifest: Manifest = await fs.readJSON(clientManifestPath, 'utf-8');
PerfLogger.end('Reading client manifest');
const manifest: Manifest = await PerfLogger.async(
'Read client manifest',
() => fs.readJSON(clientManifestPath, 'utf-8'),
);

PerfLogger.start('Compiling SSR template');
const ssrTemplate = await compileSSRTemplate(
props.siteConfig.ssrTemplate ?? defaultSSRTemplate,
const ssrTemplate = await PerfLogger.async('Compile SSR template', () =>
compileSSRTemplate(props.siteConfig.ssrTemplate ?? defaultSSRTemplate),
);
PerfLogger.end('Compiling SSR template');

PerfLogger.start('Loading App renderer');
const renderer = await loadAppRenderer({
serverBundlePath,
});
PerfLogger.end('Loading App renderer');

PerfLogger.start('Generate static files');
const ssgResult = await generateStaticFiles({
pathnames: props.routesPaths,
renderer,
params: {
trailingSlash: props.siteConfig.trailingSlash,
outDir: props.outDir,
baseUrl: props.baseUrl,
manifest,
headTags: props.headTags,
preBodyTags: props.preBodyTags,
postBodyTags: props.postBodyTags,
ssrTemplate,
noIndex: props.siteConfig.noIndex,
DOCUSAURUS_VERSION,
},
});
PerfLogger.end('Generate static files');
const renderer = await PerfLogger.async('Load App renderer', () =>
loadAppRenderer({
serverBundlePath,
}),
);

const ssgResult = await PerfLogger.async('Generate static files', () =>
generateStaticFiles({
pathnames: props.routesPaths,
renderer,
params: {
trailingSlash: props.siteConfig.trailingSlash,
outDir: props.outDir,
baseUrl: props.baseUrl,
manifest,
headTags: props.headTags,
preBodyTags: props.preBodyTags,
postBodyTags: props.postBodyTags,
ssrTemplate,
noIndex: props.siteConfig.noIndex,
DOCUSAURUS_VERSION,
},
}),
);

return ssgResult;
}
Expand Down
Loading