mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-06 04:42:40 +02:00
feat(core): hash router option - browse site offline (experimental) (#9859)
This commit is contained in:
parent
b73ad1ece5
commit
17f3e02a42
38 changed files with 1018 additions and 266 deletions
60
.github/workflows/build-hash-router.yml
vendored
Normal file
60
.github/workflows/build-hash-router.yml
vendored
Normal file
|
@ -0,0 +1,60 @@
|
|||
name: Build Hash Router
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- docusaurus-v**
|
||||
paths:
|
||||
- packages/**
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Hash Router
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: yarn
|
||||
- name: Installation
|
||||
run: yarn
|
||||
- name: Build Hash Router
|
||||
run: yarn build:website:fast
|
||||
env:
|
||||
DOCUSAURUS_ROUTER: 'hash'
|
||||
BASE_URL: '/docusaurus/' # GH pages deploys under https://facebook.github.io/docusaurus/
|
||||
- name: Upload Website artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: website-hash-router-archive
|
||||
path: website/build
|
||||
- name: Upload Website Pages artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: website/build
|
||||
|
||||
# See https://docusaurus.io/docs/deployment#triggering-deployment-with-github-actions
|
||||
deploy:
|
||||
name: Deploy to GitHub Pages
|
||||
if: ${{ github.event_name != 'pull_request' && github.ref_name == 'main')}}
|
||||
needs: build
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import {addLeadingSlash, removePrefix} from '@docusaurus/utils-common';
|
||||
import logger from '@docusaurus/logger';
|
||||
import collectRedirects from './collectRedirects';
|
||||
import writeRedirectFiles, {
|
||||
toRedirectFiles,
|
||||
|
@ -15,14 +16,24 @@ import type {LoadContext, Plugin} from '@docusaurus/types';
|
|||
import type {PluginContext, RedirectItem} from './types';
|
||||
import type {PluginOptions, Options} from './options';
|
||||
|
||||
const PluginName = 'docusaurus-plugin-client-redirects';
|
||||
|
||||
export default function pluginClientRedirectsPages(
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Plugin<void> {
|
||||
const {trailingSlash} = context.siteConfig;
|
||||
const router = context.siteConfig.future.experimental_router;
|
||||
|
||||
if (router === 'hash') {
|
||||
logger.warn(
|
||||
`${PluginName} does not support the Hash Router and will be disabled.`,
|
||||
);
|
||||
return {name: PluginName};
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'docusaurus-plugin-client-redirects',
|
||||
name: PluginName,
|
||||
async postBuild(props) {
|
||||
const pluginContext: PluginContext = {
|
||||
relativeRoutesPaths: props.routesPaths.map(
|
||||
|
|
|
@ -106,6 +106,7 @@ const getPlugin = async (
|
|||
baseUrl: '/',
|
||||
url: 'https://docusaurus.io',
|
||||
markdown,
|
||||
future: {},
|
||||
} as DocusaurusConfig;
|
||||
return pluginContentBlog(
|
||||
{
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
applyTrailingSlash,
|
||||
} from '@docusaurus/utils-common';
|
||||
import {load as cheerioLoad} from 'cheerio';
|
||||
import type {DocusaurusConfig} from '@docusaurus/types';
|
||||
import type {DocusaurusConfig, HtmlTags, LoadContext} from '@docusaurus/types';
|
||||
import type {
|
||||
FeedType,
|
||||
PluginOptions,
|
||||
|
@ -254,3 +254,59 @@ export async function createBlogFeedFiles({
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function createFeedHtmlHeadTags({
|
||||
context,
|
||||
options,
|
||||
}: {
|
||||
context: LoadContext;
|
||||
options: PluginOptions;
|
||||
}): HtmlTags {
|
||||
const feedTypes = options.feedOptions.type;
|
||||
if (!feedTypes) {
|
||||
return [];
|
||||
}
|
||||
const feedTitle = options.feedOptions.title ?? context.siteConfig.title;
|
||||
const feedsConfig = {
|
||||
rss: {
|
||||
type: 'application/rss+xml',
|
||||
path: 'rss.xml',
|
||||
title: `${feedTitle} RSS Feed`,
|
||||
},
|
||||
atom: {
|
||||
type: 'application/atom+xml',
|
||||
path: 'atom.xml',
|
||||
title: `${feedTitle} Atom Feed`,
|
||||
},
|
||||
json: {
|
||||
type: 'application/json',
|
||||
path: 'feed.json',
|
||||
title: `${feedTitle} JSON Feed`,
|
||||
},
|
||||
};
|
||||
const headTags: HtmlTags = [];
|
||||
|
||||
feedTypes.forEach((feedType) => {
|
||||
const {
|
||||
type,
|
||||
path: feedConfigPath,
|
||||
title: feedConfigTitle,
|
||||
} = feedsConfig[feedType];
|
||||
|
||||
headTags.push({
|
||||
tagName: 'link',
|
||||
attributes: {
|
||||
rel: 'alternate',
|
||||
type,
|
||||
href: normalizeUrl([
|
||||
context.siteConfig.baseUrl,
|
||||
options.routeBasePath,
|
||||
feedConfigPath,
|
||||
]),
|
||||
title: feedConfigTitle,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return headTags;
|
||||
}
|
||||
|
|
|
@ -29,11 +29,11 @@ import {
|
|||
} from './blogUtils';
|
||||
import footnoteIDFixer from './remark/footnoteIDFixer';
|
||||
import {translateContent, getTranslationFiles} from './translations';
|
||||
import {createBlogFeedFiles} from './feed';
|
||||
import {createBlogFeedFiles, createFeedHtmlHeadTags} from './feed';
|
||||
|
||||
import {createAllRoutes} from './routes';
|
||||
import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';
|
||||
import type {LoadContext, Plugin, HtmlTags} from '@docusaurus/types';
|
||||
import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||
import type {
|
||||
PluginOptions,
|
||||
BlogPostFrontMatter,
|
||||
|
@ -44,6 +44,8 @@ import type {
|
|||
BlogPaginated,
|
||||
} from '@docusaurus/plugin-content-blog';
|
||||
|
||||
const PluginName = 'docusaurus-plugin-content-blog';
|
||||
|
||||
export default async function pluginContentBlog(
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
|
@ -55,22 +57,29 @@ export default async function pluginContentBlog(
|
|||
localizationDir,
|
||||
i18n: {currentLocale},
|
||||
} = context;
|
||||
|
||||
const router = siteConfig.future.experimental_router;
|
||||
const isBlogFeedDisabledBecauseOfHashRouter =
|
||||
router === 'hash' && !!options.feedOptions.type;
|
||||
if (isBlogFeedDisabledBecauseOfHashRouter) {
|
||||
logger.warn(
|
||||
`${PluginName} feed feature does not support the Hash Router. Feeds won't be generated.`,
|
||||
);
|
||||
}
|
||||
|
||||
const {onBrokenMarkdownLinks, baseUrl} = siteConfig;
|
||||
|
||||
const contentPaths: BlogContentPaths = {
|
||||
contentPath: path.resolve(siteDir, options.path),
|
||||
contentPathLocalized: getPluginI18nPath({
|
||||
localizationDir,
|
||||
pluginName: 'docusaurus-plugin-content-blog',
|
||||
pluginName: PluginName,
|
||||
pluginId: options.id,
|
||||
}),
|
||||
};
|
||||
const pluginId = options.id ?? DEFAULT_PLUGIN_ID;
|
||||
|
||||
const pluginDataDirRoot = path.join(
|
||||
generatedFilesDir,
|
||||
'docusaurus-plugin-content-blog',
|
||||
);
|
||||
const pluginDataDirRoot = path.join(generatedFilesDir, PluginName);
|
||||
const dataDir = path.join(pluginDataDirRoot, pluginId);
|
||||
// TODO Docusaurus v4 breaking change
|
||||
// module aliasing should be automatic
|
||||
|
@ -84,7 +93,7 @@ export default async function pluginContentBlog(
|
|||
});
|
||||
|
||||
return {
|
||||
name: 'docusaurus-plugin-content-blog',
|
||||
name: PluginName,
|
||||
|
||||
getPathsToWatch() {
|
||||
const {include} = options;
|
||||
|
@ -295,15 +304,16 @@ export default async function pluginContentBlog(
|
|||
},
|
||||
|
||||
async postBuild({outDir, content}) {
|
||||
if (!options.feedOptions.type) {
|
||||
return;
|
||||
}
|
||||
const {blogPosts} = content;
|
||||
if (!blogPosts.length) {
|
||||
if (
|
||||
!content.blogPosts.length ||
|
||||
!options.feedOptions.type ||
|
||||
isBlogFeedDisabledBecauseOfHashRouter
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await createBlogFeedFiles({
|
||||
blogPosts,
|
||||
blogPosts: content.blogPosts,
|
||||
options,
|
||||
outDir,
|
||||
siteConfig,
|
||||
|
@ -312,56 +322,15 @@ export default async function pluginContentBlog(
|
|||
},
|
||||
|
||||
injectHtmlTags({content}) {
|
||||
if (!content.blogPosts.length || !options.feedOptions.type) {
|
||||
if (
|
||||
!content.blogPosts.length ||
|
||||
!options.feedOptions.type ||
|
||||
isBlogFeedDisabledBecauseOfHashRouter
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const feedTypes = options.feedOptions.type;
|
||||
const feedTitle = options.feedOptions.title ?? context.siteConfig.title;
|
||||
const feedsConfig = {
|
||||
rss: {
|
||||
type: 'application/rss+xml',
|
||||
path: 'rss.xml',
|
||||
title: `${feedTitle} RSS Feed`,
|
||||
},
|
||||
atom: {
|
||||
type: 'application/atom+xml',
|
||||
path: 'atom.xml',
|
||||
title: `${feedTitle} Atom Feed`,
|
||||
},
|
||||
json: {
|
||||
type: 'application/json',
|
||||
path: 'feed.json',
|
||||
title: `${feedTitle} JSON Feed`,
|
||||
},
|
||||
};
|
||||
const headTags: HtmlTags = [];
|
||||
|
||||
feedTypes.forEach((feedType) => {
|
||||
const {
|
||||
type,
|
||||
path: feedConfigPath,
|
||||
title: feedConfigTitle,
|
||||
} = feedsConfig[feedType];
|
||||
|
||||
headTags.push({
|
||||
tagName: 'link',
|
||||
attributes: {
|
||||
rel: 'alternate',
|
||||
type,
|
||||
href: normalizeUrl([
|
||||
baseUrl,
|
||||
options.routeBasePath,
|
||||
feedConfigPath,
|
||||
]),
|
||||
title: feedConfigTitle,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
headTags,
|
||||
};
|
||||
return {headTags: createFeedHtmlHeadTags({context, options})};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
"@babel/core": "^7.23.3",
|
||||
"@babel/preset-env": "^7.23.3",
|
||||
"@docusaurus/core": "3.3.2",
|
||||
"@docusaurus/logger": "3.3.2",
|
||||
"@docusaurus/theme-common": "3.3.2",
|
||||
"@docusaurus/theme-translations": "3.3.2",
|
||||
"@docusaurus/types": "3.3.2",
|
||||
|
|
|
@ -11,11 +11,14 @@ import WebpackBar from 'webpackbar';
|
|||
import Terser from 'terser-webpack-plugin';
|
||||
import {injectManifest} from 'workbox-build';
|
||||
import {normalizeUrl} from '@docusaurus/utils';
|
||||
import logger from '@docusaurus/logger';
|
||||
import {compile} from '@docusaurus/core/lib/webpack/utils';
|
||||
import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations';
|
||||
import type {HtmlTags, LoadContext, Plugin} from '@docusaurus/types';
|
||||
import type {PluginOptions} from '@docusaurus/plugin-pwa';
|
||||
|
||||
const PluginName = 'docusaurus-plugin-pwa';
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
function getSWBabelLoader() {
|
||||
|
@ -47,6 +50,7 @@ export default function pluginPWA(
|
|||
outDir,
|
||||
baseUrl,
|
||||
i18n: {currentLocale},
|
||||
siteConfig,
|
||||
} = context;
|
||||
const {
|
||||
debug,
|
||||
|
@ -57,8 +61,15 @@ export default function pluginPWA(
|
|||
swRegister,
|
||||
} = options;
|
||||
|
||||
if (siteConfig.future.experimental_router === 'hash') {
|
||||
logger.warn(
|
||||
`${PluginName} does not support the Hash Router and will be disabled.`,
|
||||
);
|
||||
return {name: PluginName};
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'docusaurus-plugin-pwa',
|
||||
name: PluginName,
|
||||
|
||||
getThemePath() {
|
||||
return '../lib/theme';
|
||||
|
|
|
@ -12,12 +12,21 @@ import createSitemap from './createSitemap';
|
|||
import type {PluginOptions, Options} from './options';
|
||||
import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||
|
||||
const PluginName = 'docusaurus-plugin-sitemap';
|
||||
|
||||
export default function pluginSitemap(
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Plugin<void> {
|
||||
if (context.siteConfig.future.experimental_router === 'hash') {
|
||||
logger.warn(
|
||||
`${PluginName} does not support the Hash Router and will be disabled.`,
|
||||
);
|
||||
return {name: PluginName};
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'docusaurus-plugin-sitemap',
|
||||
name: PluginName,
|
||||
|
||||
async postBuild({siteConfig, routes, outDir, head}) {
|
||||
if (siteConfig.noIndex) {
|
||||
|
|
|
@ -5,38 +5,21 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import _ from 'lodash';
|
||||
import logger from '@docusaurus/logger';
|
||||
import {defaultConfig, compile} from 'eta';
|
||||
import {normalizeUrl} from '@docusaurus/utils';
|
||||
import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations';
|
||||
import openSearchTemplate from './templates/opensearch';
|
||||
import {
|
||||
createOpenSearchFile,
|
||||
createOpenSearchHeadTags,
|
||||
shouldCreateOpenSearchFile,
|
||||
} from './opensearch';
|
||||
|
||||
import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||
import type {ThemeConfig} from '@docusaurus/theme-search-algolia';
|
||||
|
||||
const getCompiledOpenSearchTemplate = _.memoize(() =>
|
||||
compile(openSearchTemplate.trim()),
|
||||
);
|
||||
|
||||
function renderOpenSearchTemplate(data: {
|
||||
title: string;
|
||||
siteUrl: string;
|
||||
searchUrl: string;
|
||||
faviconUrl: string | null;
|
||||
}) {
|
||||
const compiled = getCompiledOpenSearchTemplate();
|
||||
return compiled(data, defaultConfig);
|
||||
}
|
||||
|
||||
const OPEN_SEARCH_FILENAME = 'opensearch.xml';
|
||||
|
||||
export default function themeSearchAlgolia(context: LoadContext): Plugin<void> {
|
||||
const {
|
||||
baseUrl,
|
||||
siteConfig: {title, url, favicon, themeConfig},
|
||||
siteConfig: {themeConfig},
|
||||
i18n: {currentLocale},
|
||||
} = context;
|
||||
const {
|
||||
|
@ -70,45 +53,17 @@ export default function themeSearchAlgolia(context: LoadContext): Plugin<void> {
|
|||
}
|
||||
},
|
||||
|
||||
async postBuild({outDir}) {
|
||||
if (searchPagePath) {
|
||||
const siteUrl = normalizeUrl([url, baseUrl]);
|
||||
|
||||
try {
|
||||
await fs.writeFile(
|
||||
path.join(outDir, OPEN_SEARCH_FILENAME),
|
||||
renderOpenSearchTemplate({
|
||||
title,
|
||||
siteUrl,
|
||||
searchUrl: normalizeUrl([siteUrl, searchPagePath]),
|
||||
faviconUrl: favicon ? normalizeUrl([siteUrl, favicon]) : null,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error('Generating OpenSearch file failed.');
|
||||
throw err;
|
||||
}
|
||||
async postBuild() {
|
||||
if (shouldCreateOpenSearchFile({context})) {
|
||||
await createOpenSearchFile({context});
|
||||
}
|
||||
},
|
||||
|
||||
injectHtmlTags() {
|
||||
if (!searchPagePath) {
|
||||
return {};
|
||||
if (shouldCreateOpenSearchFile({context})) {
|
||||
return {headTags: createOpenSearchHeadTags({context})};
|
||||
}
|
||||
|
||||
return {
|
||||
headTags: [
|
||||
{
|
||||
tagName: 'link',
|
||||
attributes: {
|
||||
rel: 'search',
|
||||
type: 'application/opensearchdescription+xml',
|
||||
title,
|
||||
href: normalizeUrl([baseUrl, OPEN_SEARCH_FILENAME]),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
return {};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
115
packages/docusaurus-theme-search-algolia/src/opensearch.ts
Normal file
115
packages/docusaurus-theme-search-algolia/src/opensearch.ts
Normal file
|
@ -0,0 +1,115 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import _ from 'lodash';
|
||||
import {defaultConfig, compile} from 'eta';
|
||||
import {normalizeUrl} from '@docusaurus/utils';
|
||||
import openSearchTemplate from './templates/opensearch';
|
||||
|
||||
import type {HtmlTags, LoadContext} from '@docusaurus/types';
|
||||
import type {ThemeConfig} from '@docusaurus/theme-search-algolia';
|
||||
|
||||
const getCompiledOpenSearchTemplate = _.memoize(() =>
|
||||
compile(openSearchTemplate.trim()),
|
||||
);
|
||||
|
||||
function renderOpenSearchTemplate(data: {
|
||||
title: string;
|
||||
siteUrl: string;
|
||||
searchUrl: string;
|
||||
faviconUrl: string | null;
|
||||
}) {
|
||||
const compiled = getCompiledOpenSearchTemplate();
|
||||
return compiled(data, defaultConfig);
|
||||
}
|
||||
|
||||
const OPEN_SEARCH_FILENAME = 'opensearch.xml';
|
||||
|
||||
export function shouldCreateOpenSearchFile({
|
||||
context,
|
||||
}: {
|
||||
context: LoadContext;
|
||||
}): boolean {
|
||||
const {
|
||||
siteConfig: {
|
||||
themeConfig,
|
||||
future: {experimental_router: router},
|
||||
},
|
||||
} = context;
|
||||
const {
|
||||
algolia: {searchPagePath},
|
||||
} = themeConfig as ThemeConfig;
|
||||
|
||||
return !!searchPagePath && router !== 'hash';
|
||||
}
|
||||
|
||||
function createOpenSearchFileContent({
|
||||
context,
|
||||
searchPagePath,
|
||||
}: {
|
||||
context: LoadContext;
|
||||
searchPagePath: string;
|
||||
}): string {
|
||||
const {
|
||||
baseUrl,
|
||||
siteConfig: {title, url, favicon},
|
||||
} = context;
|
||||
|
||||
const siteUrl = normalizeUrl([url, baseUrl]);
|
||||
|
||||
return renderOpenSearchTemplate({
|
||||
title,
|
||||
siteUrl,
|
||||
searchUrl: normalizeUrl([siteUrl, searchPagePath]),
|
||||
faviconUrl: favicon ? normalizeUrl([siteUrl, favicon]) : null,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createOpenSearchFile({
|
||||
context,
|
||||
}: {
|
||||
context: LoadContext;
|
||||
}): Promise<void> {
|
||||
const {
|
||||
outDir,
|
||||
siteConfig: {themeConfig},
|
||||
} = context;
|
||||
const {
|
||||
algolia: {searchPagePath},
|
||||
} = themeConfig as ThemeConfig;
|
||||
if (!searchPagePath) {
|
||||
throw new Error('no searchPagePath provided in themeConfig.algolia');
|
||||
}
|
||||
const fileContent = createOpenSearchFileContent({context, searchPagePath});
|
||||
try {
|
||||
await fs.writeFile(path.join(outDir, OPEN_SEARCH_FILENAME), fileContent);
|
||||
} catch (err) {
|
||||
throw new Error('Generating OpenSearch file failed.', {cause: err});
|
||||
}
|
||||
}
|
||||
|
||||
export function createOpenSearchHeadTags({
|
||||
context,
|
||||
}: {
|
||||
context: LoadContext;
|
||||
}): HtmlTags {
|
||||
const {
|
||||
baseUrl,
|
||||
siteConfig: {title},
|
||||
} = context;
|
||||
return {
|
||||
tagName: 'link',
|
||||
attributes: {
|
||||
rel: 'search',
|
||||
type: 'application/opensearchdescription+xml',
|
||||
title,
|
||||
href: normalizeUrl([baseUrl, OPEN_SEARCH_FILENAME]),
|
||||
},
|
||||
};
|
||||
}
|
20
packages/docusaurus-types/src/config.d.ts
vendored
20
packages/docusaurus-types/src/config.d.ts
vendored
|
@ -17,6 +17,8 @@ export type RemarkRehypeOptions = ProcessorOptions['remarkRehypeOptions'];
|
|||
|
||||
export type ReportingSeverity = 'ignore' | 'log' | 'warn' | 'throw';
|
||||
|
||||
export type RouterType = 'browser' | 'hash';
|
||||
|
||||
export type ThemeConfig = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
@ -123,6 +125,24 @@ export type StorageConfig = {
|
|||
|
||||
export type FutureConfig = {
|
||||
experimental_storage: StorageConfig;
|
||||
|
||||
/**
|
||||
* Docusaurus can work with 2 router types.
|
||||
*
|
||||
* - The "browser" router is the main/default router of Docusaurus.
|
||||
* It will use the browser history and regular urls to navigate from
|
||||
* one page to another. A static file will be emitted for each page.
|
||||
*
|
||||
* - The "hash" router can be useful in very specific situations (such as
|
||||
* distributing your app for offline-first usage), but should be avoided
|
||||
* in most cases. All pages paths will be prefixed with a /#/.
|
||||
* It will opt out of static site generation, only emit a single index.html
|
||||
* entry point, and use the browser hash for routing. The Docusaurus site
|
||||
* content will be rendered client-side, like a regular single page
|
||||
* application.
|
||||
* @see https://github.com/facebook/docusaurus/issues/3825
|
||||
*/
|
||||
experimental_router: RouterType;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
1
packages/docusaurus-types/src/index.d.ts
vendored
1
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -7,6 +7,7 @@
|
|||
|
||||
export {
|
||||
ReportingSeverity,
|
||||
RouterType,
|
||||
ThemeConfig,
|
||||
MarkdownConfig,
|
||||
DefaultParseFrontMatter,
|
||||
|
|
|
@ -90,6 +90,30 @@ describe('normalizeUrl', () => {
|
|||
input: ['http://foobar.com', '', 'test', '/'],
|
||||
output: 'http://foobar.com/test/',
|
||||
},
|
||||
{
|
||||
input: ['http://foobar.com/', '', 'test', '/'],
|
||||
output: 'http://foobar.com/test/',
|
||||
},
|
||||
{
|
||||
input: ['http://foobar.com', '#', 'test'],
|
||||
output: 'http://foobar.com/#/test',
|
||||
},
|
||||
{
|
||||
input: ['http://foobar.com/', '#', 'test'],
|
||||
output: 'http://foobar.com/#/test',
|
||||
},
|
||||
{
|
||||
input: ['http://foobar.com', '/#/', 'test'],
|
||||
output: 'http://foobar.com/#/test',
|
||||
},
|
||||
{
|
||||
input: ['http://foobar.com', '#/', 'test'],
|
||||
output: 'http://foobar.com/#/test',
|
||||
},
|
||||
{
|
||||
input: ['http://foobar.com', '/#', 'test'],
|
||||
output: 'http://foobar.com/#/test',
|
||||
},
|
||||
{
|
||||
input: ['/', '', 'hello', '', '/', '/', '', '/', '/world'],
|
||||
output: '/hello/world',
|
||||
|
|
|
@ -90,7 +90,7 @@ export function normalizeUrl(rawUrls: string[]): string {
|
|||
// first plain protocol part.
|
||||
|
||||
// Remove trailing slash before parameters or hash.
|
||||
str = str.replace(/\/(?<search>\?|&|#[^!])/g, '$1');
|
||||
str = str.replace(/\/(?<search>\?|&|#[^!/])/g, '$1');
|
||||
|
||||
// Replace ? in parameters with &.
|
||||
const parts = str.split('?');
|
||||
|
|
|
@ -5,16 +5,24 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {startTransition} from 'react';
|
||||
import React, {startTransition, type ReactNode} from 'react';
|
||||
import ReactDOM, {type ErrorInfo} from 'react-dom/client';
|
||||
import {BrowserRouter} from 'react-router-dom';
|
||||
import {HelmetProvider} from 'react-helmet-async';
|
||||
|
||||
import {BrowserRouter, HashRouter} from 'react-router-dom';
|
||||
import siteConfig from '@generated/docusaurus.config';
|
||||
import ExecutionEnvironment from './exports/ExecutionEnvironment';
|
||||
import App from './App';
|
||||
import preload from './preload';
|
||||
import docusaurus from './docusaurus';
|
||||
|
||||
function Router({children}: {children: ReactNode}): ReactNode {
|
||||
return siteConfig.future.experimental_router === 'hash' ? (
|
||||
<HashRouter>{children}</HashRouter>
|
||||
) : (
|
||||
<BrowserRouter>{children}</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface NodeModule {
|
||||
hot?: {accept: () => void};
|
||||
|
@ -31,9 +39,9 @@ if (ExecutionEnvironment.canUseDOM) {
|
|||
|
||||
const app = (
|
||||
<HelmetProvider>
|
||||
<BrowserRouter>
|
||||
<Router>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</Router>
|
||||
</HelmetProvider>
|
||||
);
|
||||
|
||||
|
|
|
@ -40,9 +40,9 @@ function Link(
|
|||
}: Props,
|
||||
forwardedRef: React.ForwardedRef<HTMLAnchorElement>,
|
||||
): JSX.Element {
|
||||
const {
|
||||
siteConfig: {trailingSlash, baseUrl},
|
||||
} = useDocusaurusContext();
|
||||
const {siteConfig} = useDocusaurusContext();
|
||||
const {trailingSlash, baseUrl} = siteConfig;
|
||||
const router = siteConfig.future.experimental_router;
|
||||
const {withBaseUrl} = useBaseUrlUtils();
|
||||
const brokenLinks = useBrokenLinks();
|
||||
const innerRef = useRef<HTMLAnchorElement | null>(null);
|
||||
|
@ -81,6 +81,15 @@ function Link(
|
|||
? maybeAddBaseUrl(targetLinkWithoutPathnameProtocol)
|
||||
: undefined;
|
||||
|
||||
// TODO find a way to solve this problem properly
|
||||
// Fix edge case when useBaseUrl is used on a link
|
||||
// "./" is useful for images and other resources
|
||||
// But we don't need it for <Link>
|
||||
// unfortunately we can't really make the difference :/
|
||||
if (router === 'hash' && targetLink?.startsWith('./')) {
|
||||
targetLink = targetLink?.slice(1);
|
||||
}
|
||||
|
||||
if (targetLink && isInternal) {
|
||||
targetLink = applyTrailingSlash(targetLink, {trailingSlash, baseUrl});
|
||||
}
|
||||
|
@ -148,8 +157,7 @@ function Link(
|
|||
const hasInternalTarget = !props.target || props.target === '_self';
|
||||
|
||||
// Should we use a regular <a> tag instead of React-Router Link component?
|
||||
const isRegularHtmlLink =
|
||||
!targetLink || !isInternal || !hasInternalTarget || isAnchorLink;
|
||||
const isRegularHtmlLink = !targetLink || !isInternal || !hasInternalTarget;
|
||||
|
||||
if (!noBrokenLinkCheck && (isAnchorLink || !isRegularHtmlLink)) {
|
||||
brokenLinks.collectLink(targetLink!);
|
||||
|
|
|
@ -7,13 +7,222 @@
|
|||
|
||||
import React from 'react';
|
||||
import {renderHook} from '@testing-library/react-hooks';
|
||||
import useBaseUrl, {useBaseUrlUtils} from '../useBaseUrl';
|
||||
import {fromPartial} from '@total-typescript/shoehorn';
|
||||
import useBaseUrl, {addBaseUrl, useBaseUrlUtils} from '../useBaseUrl';
|
||||
import {Context} from '../../docusaurusContext';
|
||||
import type {DocusaurusContext} from '@docusaurus/types';
|
||||
import type {DocusaurusContext, FutureConfig} from '@docusaurus/types';
|
||||
import type {BaseUrlOptions} from '@docusaurus/useBaseUrl';
|
||||
|
||||
type AddBaseUrlParams = Parameters<typeof addBaseUrl>[0];
|
||||
|
||||
const future: FutureConfig = fromPartial({
|
||||
experimental_router: 'browser',
|
||||
});
|
||||
|
||||
const forcePrepend = {forcePrependBaseUrl: true};
|
||||
|
||||
// TODO migrate more tests here, it's easier to test a pure function
|
||||
describe('addBaseUrl', () => {
|
||||
function baseTest(params: Partial<AddBaseUrlParams>) {
|
||||
return addBaseUrl({
|
||||
siteUrl: 'https://docusaurus.io',
|
||||
baseUrl: '/baseUrl/',
|
||||
url: 'hello',
|
||||
router: 'browser',
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
describe('with browser router', () => {
|
||||
function test(params: {
|
||||
url: AddBaseUrlParams['url'];
|
||||
baseUrl: AddBaseUrlParams['baseUrl'];
|
||||
options?: AddBaseUrlParams['options'];
|
||||
}) {
|
||||
return baseTest({
|
||||
...params,
|
||||
router: 'browser',
|
||||
});
|
||||
}
|
||||
|
||||
it('/baseUrl/ + hello', () => {
|
||||
expect(
|
||||
test({
|
||||
baseUrl: '/baseUrl/',
|
||||
url: 'hello',
|
||||
}),
|
||||
).toBe('/baseUrl/hello');
|
||||
});
|
||||
|
||||
it('/baseUrl/ + hello - absolute option', () => {
|
||||
expect(
|
||||
test({
|
||||
baseUrl: '/baseUrl/',
|
||||
url: 'hello',
|
||||
options: {absolute: true},
|
||||
}),
|
||||
).toBe('https://docusaurus.io/baseUrl/hello');
|
||||
});
|
||||
|
||||
it('/baseUrl/ + /hello', () => {
|
||||
expect(
|
||||
test({
|
||||
baseUrl: '/baseUrl/',
|
||||
url: '/hello',
|
||||
}),
|
||||
).toBe('/baseUrl/hello');
|
||||
});
|
||||
|
||||
it('/baseUrl/ + /hello - absolute option', () => {
|
||||
expect(
|
||||
test({
|
||||
baseUrl: '/baseUrl/',
|
||||
url: '/hello',
|
||||
options: {absolute: true},
|
||||
}),
|
||||
).toBe('https://docusaurus.io/baseUrl/hello');
|
||||
});
|
||||
|
||||
it('/ + hello', () => {
|
||||
expect(
|
||||
test({
|
||||
baseUrl: '/',
|
||||
url: 'hello',
|
||||
}),
|
||||
).toBe('/hello');
|
||||
});
|
||||
|
||||
it('/ + hello - absolute', () => {
|
||||
expect(
|
||||
test({
|
||||
baseUrl: '/',
|
||||
url: 'hello',
|
||||
options: {absolute: true},
|
||||
}),
|
||||
).toBe('https://docusaurus.io/hello');
|
||||
});
|
||||
|
||||
it('/ + /hello', () => {
|
||||
expect(
|
||||
test({
|
||||
baseUrl: '/',
|
||||
url: '/hello',
|
||||
}),
|
||||
).toBe('/hello');
|
||||
});
|
||||
|
||||
it('/ + /hello - absolute', () => {
|
||||
expect(
|
||||
test({
|
||||
baseUrl: '/',
|
||||
url: '/hello',
|
||||
options: {absolute: true},
|
||||
}),
|
||||
).toBe('https://docusaurus.io/hello');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with hash router', () => {
|
||||
function test(params: {
|
||||
url: AddBaseUrlParams['url'];
|
||||
baseUrl: AddBaseUrlParams['baseUrl'];
|
||||
options?: AddBaseUrlParams['options'];
|
||||
}) {
|
||||
return baseTest({
|
||||
...params,
|
||||
router: 'hash',
|
||||
});
|
||||
}
|
||||
|
||||
it('/baseUrl/ + hello', () => {
|
||||
expect(
|
||||
test({
|
||||
baseUrl: '/baseUrl/',
|
||||
url: 'hello',
|
||||
}),
|
||||
).toBe('./hello');
|
||||
});
|
||||
|
||||
it('/baseUrl/ + hello - absolute option', () => {
|
||||
expect(
|
||||
test({
|
||||
baseUrl: '/baseUrl/',
|
||||
url: 'hello',
|
||||
options: {absolute: true},
|
||||
}),
|
||||
).toBe('./hello');
|
||||
});
|
||||
|
||||
it('/baseUrl/ + /hello', () => {
|
||||
expect(
|
||||
test({
|
||||
baseUrl: '/baseUrl/',
|
||||
url: '/hello',
|
||||
}),
|
||||
).toBe('./hello');
|
||||
});
|
||||
|
||||
it('/baseUrl/ + /hello - absolute option', () => {
|
||||
expect(
|
||||
test({
|
||||
baseUrl: '/baseUrl/',
|
||||
url: '/hello',
|
||||
options: {absolute: true},
|
||||
}),
|
||||
).toBe('./hello');
|
||||
});
|
||||
|
||||
it('/ + hello', () => {
|
||||
expect(
|
||||
test({
|
||||
baseUrl: '/',
|
||||
url: 'hello',
|
||||
}),
|
||||
).toBe('./hello');
|
||||
});
|
||||
|
||||
it('/ + hello - absolute', () => {
|
||||
expect(
|
||||
test({
|
||||
baseUrl: '/',
|
||||
url: 'hello',
|
||||
options: {absolute: true},
|
||||
}),
|
||||
).toBe('./hello');
|
||||
});
|
||||
|
||||
it('/ + /hello', () => {
|
||||
expect(
|
||||
test({
|
||||
baseUrl: '/',
|
||||
url: 'hello',
|
||||
options: {absolute: true},
|
||||
}),
|
||||
).toBe('./hello');
|
||||
});
|
||||
|
||||
it('/ + /hello - absolute', () => {
|
||||
expect(
|
||||
test({
|
||||
baseUrl: '/',
|
||||
url: 'hello',
|
||||
options: {absolute: true},
|
||||
}),
|
||||
).toBe('./hello');
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
src
|
||||
:
|
||||
"img/docusaurus.svg"
|
||||
srcDark
|
||||
:
|
||||
"img/docusaurus_keytar.svg"
|
||||
*/
|
||||
});
|
||||
|
||||
describe('useBaseUrl', () => {
|
||||
const createUseBaseUrlMock =
|
||||
(context: DocusaurusContext) => (url: string, options?: BaseUrlOptions) =>
|
||||
|
@ -27,6 +236,7 @@ describe('useBaseUrl', () => {
|
|||
siteConfig: {
|
||||
baseUrl: '/',
|
||||
url: 'https://docusaurus.io',
|
||||
future,
|
||||
},
|
||||
} as DocusaurusContext);
|
||||
|
||||
|
@ -55,6 +265,7 @@ describe('useBaseUrl', () => {
|
|||
siteConfig: {
|
||||
baseUrl: '/docusaurus/',
|
||||
url: 'https://docusaurus.io',
|
||||
future,
|
||||
},
|
||||
} as DocusaurusContext);
|
||||
|
||||
|
@ -96,6 +307,7 @@ describe('useBaseUrlUtils().withBaseUrl()', () => {
|
|||
siteConfig: {
|
||||
baseUrl: '/',
|
||||
url: 'https://docusaurus.io',
|
||||
future,
|
||||
},
|
||||
} as DocusaurusContext);
|
||||
|
||||
|
@ -124,6 +336,7 @@ describe('useBaseUrlUtils().withBaseUrl()', () => {
|
|||
siteConfig: {
|
||||
baseUrl: '/docusaurus/',
|
||||
url: 'https://docusaurus.io',
|
||||
future,
|
||||
},
|
||||
} as DocusaurusContext);
|
||||
|
||||
|
|
|
@ -9,19 +9,34 @@ import {useCallback} from 'react';
|
|||
import useDocusaurusContext from './useDocusaurusContext';
|
||||
import {hasProtocol} from './isInternalUrl';
|
||||
import type {BaseUrlOptions, BaseUrlUtils} from '@docusaurus/useBaseUrl';
|
||||
import type {RouterType} from '@docusaurus/types';
|
||||
|
||||
function addBaseUrl(
|
||||
siteUrl: string,
|
||||
baseUrl: string,
|
||||
url: string,
|
||||
{forcePrependBaseUrl = false, absolute = false}: BaseUrlOptions = {},
|
||||
): string {
|
||||
export function addBaseUrl({
|
||||
siteUrl,
|
||||
baseUrl,
|
||||
url,
|
||||
options: {forcePrependBaseUrl = false, absolute = false} = {},
|
||||
router,
|
||||
}: {
|
||||
siteUrl: string;
|
||||
baseUrl: string;
|
||||
url: string;
|
||||
router: RouterType;
|
||||
options?: BaseUrlOptions;
|
||||
}): string {
|
||||
// It never makes sense to add base url to a local anchor url, or one with a
|
||||
// protocol
|
||||
if (!url || url.startsWith('#') || hasProtocol(url)) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// TODO hash router + /baseUrl/ is unlikely to work well in all situations
|
||||
// This will support most cases, but not all
|
||||
// See https://github.com/facebook/docusaurus/pull/9859
|
||||
if (router === 'hash') {
|
||||
return url.startsWith('/') ? `.${url}` : `./${url}`;
|
||||
}
|
||||
|
||||
if (forcePrependBaseUrl) {
|
||||
return baseUrl + url.replace(/^\//, '');
|
||||
}
|
||||
|
@ -41,14 +56,14 @@ function addBaseUrl(
|
|||
}
|
||||
|
||||
export function useBaseUrlUtils(): BaseUrlUtils {
|
||||
const {
|
||||
siteConfig: {baseUrl, url: siteUrl},
|
||||
} = useDocusaurusContext();
|
||||
const {siteConfig} = useDocusaurusContext();
|
||||
const {baseUrl, url: siteUrl} = siteConfig;
|
||||
const router = siteConfig.future.experimental_router;
|
||||
|
||||
const withBaseUrl = useCallback(
|
||||
(url: string, options?: BaseUrlOptions) =>
|
||||
addBaseUrl(siteUrl, baseUrl, url, options),
|
||||
[siteUrl, baseUrl],
|
||||
addBaseUrl({siteUrl, baseUrl, url, options, router}),
|
||||
[siteUrl, baseUrl, router],
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
@ -20,12 +20,20 @@ import {compile} from '../webpack/utils';
|
|||
import {PerfLogger} from '../utils';
|
||||
|
||||
import {loadI18n} from '../server/i18n';
|
||||
import {generateStaticFiles, loadAppRenderer} from '../ssg';
|
||||
import {compileSSRTemplate} from '../templates/templates';
|
||||
import {
|
||||
generateHashRouterEntrypoint,
|
||||
generateStaticFiles,
|
||||
loadAppRenderer,
|
||||
} from '../ssg';
|
||||
import {
|
||||
compileSSRTemplate,
|
||||
renderHashRouterTemplate,
|
||||
} from '../templates/templates';
|
||||
import defaultSSRTemplate from '../templates/ssr.html.template';
|
||||
import type {SSGParams} from '../ssg';
|
||||
|
||||
import type {Manifest} from 'react-loadable-ssr-addon-v5-slorber';
|
||||
import type {LoadedPlugin, Props} from '@docusaurus/types';
|
||||
import type {LoadedPlugin, Props, RouterType} from '@docusaurus/types';
|
||||
import type {SiteCollectedData} from '../common';
|
||||
|
||||
export type BuildCLIOptions = Pick<
|
||||
|
@ -164,7 +172,9 @@ async function buildLocale({
|
|||
);
|
||||
|
||||
const {props} = site;
|
||||
const {outDir, plugins} = props;
|
||||
const {outDir, plugins, siteConfig} = props;
|
||||
|
||||
const router = siteConfig.future.experimental_router;
|
||||
|
||||
// We can build the 2 configs in parallel
|
||||
const [{clientConfig, clientManifestPath}, {serverConfig, serverBundlePath}] =
|
||||
|
@ -181,15 +191,20 @@ async function buildLocale({
|
|||
);
|
||||
|
||||
// Run webpack to build JS bundle (client) and static html files (server).
|
||||
await PerfLogger.async('Bundling with Webpack', () =>
|
||||
compile([clientConfig, serverConfig]),
|
||||
);
|
||||
await PerfLogger.async('Bundling with Webpack', () => {
|
||||
if (router === 'hash') {
|
||||
return compile([clientConfig]);
|
||||
} else {
|
||||
return compile([clientConfig, serverConfig]);
|
||||
}
|
||||
});
|
||||
|
||||
const {collectedData} = await PerfLogger.async('SSG', () =>
|
||||
executeSSG({
|
||||
props,
|
||||
serverBundlePath,
|
||||
clientManifestPath,
|
||||
router,
|
||||
}),
|
||||
);
|
||||
|
||||
|
@ -220,11 +235,13 @@ async function executeSSG({
|
|||
props,
|
||||
serverBundlePath,
|
||||
clientManifestPath,
|
||||
router,
|
||||
}: {
|
||||
props: Props;
|
||||
serverBundlePath: string;
|
||||
clientManifestPath: string;
|
||||
}) {
|
||||
router: RouterType;
|
||||
}): Promise<{collectedData: SiteCollectedData}> {
|
||||
const manifest: Manifest = await PerfLogger.async(
|
||||
'Read client manifest',
|
||||
() => fs.readJSON(clientManifestPath, 'utf-8'),
|
||||
|
@ -234,6 +251,27 @@ async function executeSSG({
|
|||
compileSSRTemplate(props.siteConfig.ssrTemplate ?? defaultSSRTemplate),
|
||||
);
|
||||
|
||||
const params: SSGParams = {
|
||||
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,
|
||||
};
|
||||
|
||||
if (router === 'hash') {
|
||||
PerfLogger.start('Generate Hash Router entry point');
|
||||
const content = renderHashRouterTemplate({params});
|
||||
await generateHashRouterEntrypoint({content, params});
|
||||
PerfLogger.end('Generate Hash Router entry point');
|
||||
return {collectedData: {}};
|
||||
}
|
||||
|
||||
const renderer = await PerfLogger.async('Load App renderer', () =>
|
||||
loadAppRenderer({
|
||||
serverBundlePath,
|
||||
|
@ -244,18 +282,7 @@ async function executeSSG({
|
|||
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,
|
||||
},
|
||||
params,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
@ -20,12 +20,18 @@ import {
|
|||
} from '../../server/site';
|
||||
import {formatPluginName} from '../../server/plugins/pluginsUtils';
|
||||
import type {StartCLIOptions} from './start';
|
||||
import type {LoadedPlugin} from '@docusaurus/types';
|
||||
import type {LoadedPlugin, RouterType} from '@docusaurus/types';
|
||||
|
||||
export type OpenUrlContext = {
|
||||
host: string;
|
||||
port: number;
|
||||
getOpenUrl: ({baseUrl}: {baseUrl: string}) => string;
|
||||
getOpenUrl: ({
|
||||
baseUrl,
|
||||
router,
|
||||
}: {
|
||||
baseUrl: string;
|
||||
router: RouterType;
|
||||
}) => string;
|
||||
};
|
||||
|
||||
export async function createOpenUrlContext({
|
||||
|
@ -40,9 +46,13 @@ export async function createOpenUrlContext({
|
|||
return process.exit();
|
||||
}
|
||||
|
||||
const getOpenUrl: OpenUrlContext['getOpenUrl'] = ({baseUrl}) => {
|
||||
const getOpenUrl: OpenUrlContext['getOpenUrl'] = ({baseUrl, router}) => {
|
||||
const urls = prepareUrls(protocol, host, port);
|
||||
return normalizeUrl([urls.localUrlForBrowser, baseUrl]);
|
||||
return normalizeUrl([
|
||||
urls.localUrlForBrowser,
|
||||
router === 'hash' ? '/#/' : '',
|
||||
baseUrl,
|
||||
]);
|
||||
};
|
||||
|
||||
return {host, port, getOpenUrl};
|
||||
|
@ -83,6 +93,7 @@ export async function createReloadableSite(startParams: StartParams) {
|
|||
const getOpenUrl = () =>
|
||||
openUrlContext.getOpenUrl({
|
||||
baseUrl: site.props.baseUrl,
|
||||
router: site.props.siteConfig.future.experimental_router,
|
||||
});
|
||||
|
||||
const printOpenUrlMessage = () => {
|
||||
|
|
|
@ -81,7 +81,8 @@ async function createDevServerConfig({
|
|||
'access-control-allow-origin': '*',
|
||||
},
|
||||
devMiddleware: {
|
||||
publicPath: baseUrl,
|
||||
publicPath:
|
||||
siteConfig.future.experimental_router === 'hash' ? 'auto' : baseUrl,
|
||||
// Reduce log verbosity, see https://github.com/facebook/docusaurus/pull/5420#issuecomment-906613105
|
||||
stats: 'summary',
|
||||
},
|
||||
|
|
|
@ -8,6 +8,7 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = `
|
|||
"clientModules": [],
|
||||
"customFields": {},
|
||||
"future": {
|
||||
"experimental_router": "browser",
|
||||
"experimental_storage": {
|
||||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
|
@ -68,6 +69,7 @@ exports[`loadSiteConfig website with ts + js config 1`] = `
|
|||
"clientModules": [],
|
||||
"customFields": {},
|
||||
"future": {
|
||||
"experimental_router": "browser",
|
||||
"experimental_storage": {
|
||||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
|
@ -128,6 +130,7 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = `
|
|||
"clientModules": [],
|
||||
"customFields": {},
|
||||
"future": {
|
||||
"experimental_router": "browser",
|
||||
"experimental_storage": {
|
||||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
|
@ -188,6 +191,7 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = `
|
|||
"clientModules": [],
|
||||
"customFields": {},
|
||||
"future": {
|
||||
"experimental_router": "browser",
|
||||
"experimental_storage": {
|
||||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
|
@ -248,6 +252,7 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = `
|
|||
"clientModules": [],
|
||||
"customFields": {},
|
||||
"future": {
|
||||
"experimental_router": "browser",
|
||||
"experimental_storage": {
|
||||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
|
@ -308,6 +313,7 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = `
|
|||
"clientModules": [],
|
||||
"customFields": {},
|
||||
"future": {
|
||||
"experimental_router": "browser",
|
||||
"experimental_storage": {
|
||||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
|
@ -368,6 +374,7 @@ exports[`loadSiteConfig website with valid async config 1`] = `
|
|||
"clientModules": [],
|
||||
"customFields": {},
|
||||
"future": {
|
||||
"experimental_router": "browser",
|
||||
"experimental_storage": {
|
||||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
|
@ -430,6 +437,7 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = `
|
|||
"clientModules": [],
|
||||
"customFields": {},
|
||||
"future": {
|
||||
"experimental_router": "browser",
|
||||
"experimental_storage": {
|
||||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
|
@ -492,6 +500,7 @@ exports[`loadSiteConfig website with valid config creator function 1`] = `
|
|||
"clientModules": [],
|
||||
"customFields": {},
|
||||
"future": {
|
||||
"experimental_router": "browser",
|
||||
"experimental_storage": {
|
||||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
|
@ -557,6 +566,7 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = `
|
|||
"customFields": {},
|
||||
"favicon": "img/docusaurus.ico",
|
||||
"future": {
|
||||
"experimental_router": "browser",
|
||||
"experimental_storage": {
|
||||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
|
|
|
@ -78,6 +78,7 @@ exports[`load loads props for site with custom i18n path 1`] = `
|
|||
"clientModules": [],
|
||||
"customFields": {},
|
||||
"future": {
|
||||
"experimental_router": "browser",
|
||||
"experimental_storage": {
|
||||
"namespace": false,
|
||||
"type": "localStorage",
|
||||
|
|
|
@ -42,6 +42,7 @@ describe('normalizeConfig', () => {
|
|||
type: 'sessionStorage',
|
||||
namespace: true,
|
||||
},
|
||||
experimental_router: 'hash',
|
||||
},
|
||||
tagline: 'my awesome site',
|
||||
organizationName: 'facebook',
|
||||
|
@ -648,6 +649,7 @@ describe('future', () => {
|
|||
type: 'sessionStorage',
|
||||
namespace: 'myNamespace',
|
||||
},
|
||||
experimental_router: 'hash',
|
||||
};
|
||||
expect(
|
||||
normalizeConfig({
|
||||
|
@ -675,6 +677,97 @@ describe('future', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
describe('router', () => {
|
||||
it('accepts router - undefined', () => {
|
||||
expect(
|
||||
normalizeConfig({
|
||||
future: {
|
||||
experimental_router: undefined,
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
future: expect.objectContaining({experimental_router: 'browser'}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts router - hash', () => {
|
||||
expect(
|
||||
normalizeConfig({
|
||||
future: {
|
||||
experimental_router: 'hash',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
future: expect.objectContaining({experimental_router: 'hash'}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts router - browser', () => {
|
||||
expect(
|
||||
normalizeConfig({
|
||||
future: {
|
||||
experimental_router: 'browser',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
future: expect.objectContaining({experimental_router: 'browser'}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects router - invalid enum value', () => {
|
||||
// @ts-expect-error: invalid
|
||||
const router: DocusaurusConfig['future']['experimental_router'] =
|
||||
'badRouter';
|
||||
expect(() =>
|
||||
normalizeConfig({
|
||||
future: {
|
||||
experimental_router: router,
|
||||
},
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
""future.experimental_router" must be one of [browser, hash]
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('rejects router - null', () => {
|
||||
const router: DocusaurusConfig['future']['experimental_router'] = null;
|
||||
expect(() =>
|
||||
normalizeConfig({
|
||||
future: {
|
||||
experimental_router: router,
|
||||
},
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
""future.experimental_router" must be one of [browser, hash]
|
||||
"future.experimental_router" must be a string
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('rejects router - number', () => {
|
||||
// @ts-expect-error: invalid
|
||||
const router: DocusaurusConfig['future']['experimental_router'] = 42;
|
||||
expect(() =>
|
||||
normalizeConfig({
|
||||
future: {
|
||||
experimental_router: router,
|
||||
},
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
""future.experimental_router" must be one of [browser, hash]
|
||||
"future.experimental_router" must be a string
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('storage', () => {
|
||||
it('accepts storage - undefined', () => {
|
||||
expect(
|
||||
|
@ -707,9 +800,9 @@ describe('future', () => {
|
|||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
future: {
|
||||
future: expect.objectContaining({
|
||||
experimental_storage: storage,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
@ -757,12 +850,12 @@ describe('future', () => {
|
|||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
future: {
|
||||
future: expect.objectContaining({
|
||||
experimental_storage: {
|
||||
...DEFAULT_STORAGE_CONFIG,
|
||||
...storage,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
@ -779,12 +872,12 @@ describe('future', () => {
|
|||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
future: {
|
||||
future: expect.objectContaining({
|
||||
experimental_storage: {
|
||||
...DEFAULT_STORAGE_CONFIG,
|
||||
type: 'localStorage',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
@ -850,12 +943,12 @@ describe('future', () => {
|
|||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
future: {
|
||||
future: expect.objectContaining({
|
||||
experimental_storage: {
|
||||
...DEFAULT_STORAGE_CONFIG,
|
||||
...storage,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
@ -872,12 +965,12 @@ describe('future', () => {
|
|||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
future: {
|
||||
future: expect.objectContaining({
|
||||
experimental_storage: {
|
||||
...DEFAULT_STORAGE_CONFIG,
|
||||
...storage,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
import {loadHtmlTags} from '../htmlTags';
|
||||
import type {LoadedPlugin} from '@docusaurus/types';
|
||||
|
||||
function testHtmlTags(plugins: LoadedPlugin[]) {
|
||||
return loadHtmlTags({plugins, router: 'browser'});
|
||||
}
|
||||
|
||||
const pluginEmpty = {
|
||||
name: 'plugin-empty',
|
||||
} as LoadedPlugin;
|
||||
|
@ -85,7 +89,7 @@ const pluginMaybeInjectHeadTags = {
|
|||
|
||||
describe('loadHtmlTags', () => {
|
||||
it('works for an empty plugin', () => {
|
||||
const htmlTags = loadHtmlTags([pluginEmpty]);
|
||||
const htmlTags = testHtmlTags([pluginEmpty]);
|
||||
expect(htmlTags).toMatchInlineSnapshot(`
|
||||
{
|
||||
"headTags": "",
|
||||
|
@ -96,7 +100,7 @@ describe('loadHtmlTags', () => {
|
|||
});
|
||||
|
||||
it('only injects headTags', () => {
|
||||
const htmlTags = loadHtmlTags([pluginHeadTags]);
|
||||
const htmlTags = testHtmlTags([pluginHeadTags]);
|
||||
expect(htmlTags).toMatchInlineSnapshot(`
|
||||
{
|
||||
"headTags": "<link rel="preconnect" href="www.google-analytics.com">
|
||||
|
@ -109,7 +113,7 @@ describe('loadHtmlTags', () => {
|
|||
});
|
||||
|
||||
it('only injects preBodyTags', () => {
|
||||
const htmlTags = loadHtmlTags([pluginPreBodyTags]);
|
||||
const htmlTags = testHtmlTags([pluginPreBodyTags]);
|
||||
expect(htmlTags).toMatchInlineSnapshot(`
|
||||
{
|
||||
"headTags": "",
|
||||
|
@ -120,7 +124,7 @@ describe('loadHtmlTags', () => {
|
|||
});
|
||||
|
||||
it('only injects postBodyTags', () => {
|
||||
const htmlTags = loadHtmlTags([pluginPostBodyTags]);
|
||||
const htmlTags = testHtmlTags([pluginPostBodyTags]);
|
||||
expect(htmlTags).toMatchInlineSnapshot(`
|
||||
{
|
||||
"headTags": "",
|
||||
|
@ -132,7 +136,7 @@ describe('loadHtmlTags', () => {
|
|||
});
|
||||
|
||||
it('allows multiple plugins that inject different part of html tags', () => {
|
||||
const htmlTags = loadHtmlTags([
|
||||
const htmlTags = testHtmlTags([
|
||||
pluginHeadTags,
|
||||
pluginPostBodyTags,
|
||||
pluginPreBodyTags,
|
||||
|
@ -150,7 +154,7 @@ describe('loadHtmlTags', () => {
|
|||
});
|
||||
|
||||
it('allows multiple plugins that might/might not inject html tags', () => {
|
||||
const htmlTags = loadHtmlTags([
|
||||
const htmlTags = testHtmlTags([
|
||||
pluginEmpty,
|
||||
pluginHeadTags,
|
||||
pluginPostBodyTags,
|
||||
|
@ -169,7 +173,7 @@ describe('loadHtmlTags', () => {
|
|||
});
|
||||
it('throws for invalid tag', () => {
|
||||
expect(() =>
|
||||
loadHtmlTags([
|
||||
testHtmlTags([
|
||||
// @ts-expect-error: test
|
||||
{
|
||||
injectHtmlTags() {
|
||||
|
@ -191,7 +195,7 @@ describe('loadHtmlTags', () => {
|
|||
|
||||
it('throws for invalid tagName', () => {
|
||||
expect(() =>
|
||||
loadHtmlTags([
|
||||
testHtmlTags([
|
||||
{
|
||||
// @ts-expect-error: test
|
||||
injectHtmlTags() {
|
||||
|
@ -210,7 +214,7 @@ describe('loadHtmlTags', () => {
|
|||
|
||||
it('throws for invalid tag object', () => {
|
||||
expect(() =>
|
||||
loadHtmlTags([
|
||||
testHtmlTags([
|
||||
{
|
||||
// @ts-expect-error: test
|
||||
injectHtmlTags() {
|
||||
|
|
|
@ -39,6 +39,7 @@ export const DEFAULT_STORAGE_CONFIG: StorageConfig = {
|
|||
|
||||
export const DEFAULT_FUTURE_CONFIG: FutureConfig = {
|
||||
experimental_storage: DEFAULT_STORAGE_CONFIG,
|
||||
experimental_router: 'browser',
|
||||
};
|
||||
|
||||
export const DEFAULT_MARKDOWN_CONFIG: MarkdownConfig = {
|
||||
|
@ -206,6 +207,9 @@ const STORAGE_CONFIG_SCHEMA = Joi.object({
|
|||
|
||||
const FUTURE_CONFIG_SCHEMA = Joi.object<FutureConfig>({
|
||||
experimental_storage: STORAGE_CONFIG_SCHEMA,
|
||||
experimental_router: Joi.string()
|
||||
.equal('browser', 'hash')
|
||||
.default(DEFAULT_FUTURE_CONFIG.experimental_router),
|
||||
})
|
||||
.optional()
|
||||
.default(DEFAULT_FUTURE_CONFIG);
|
||||
|
|
|
@ -14,6 +14,7 @@ import type {
|
|||
HtmlTagObject,
|
||||
HtmlTags,
|
||||
LoadedPlugin,
|
||||
RouterType,
|
||||
} from '@docusaurus/types';
|
||||
|
||||
function assertIsHtmlTagObject(val: unknown): asserts val is HtmlTagObject {
|
||||
|
@ -36,16 +37,35 @@ function assertIsHtmlTagObject(val: unknown): asserts val is HtmlTagObject {
|
|||
}
|
||||
}
|
||||
|
||||
function htmlTagObjectToString(tag: unknown): string {
|
||||
function hashRouterAbsoluteToRelativeTagAttribute(
|
||||
name: string,
|
||||
value: string,
|
||||
): string {
|
||||
if ((name === 'src' || name === 'href') && value.startsWith('/')) {
|
||||
return `.${value}`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function htmlTagObjectToString({
|
||||
tag,
|
||||
router,
|
||||
}: {
|
||||
tag: unknown;
|
||||
router: RouterType;
|
||||
}): string {
|
||||
assertIsHtmlTagObject(tag);
|
||||
const isVoidTag = (voidHtmlTags as string[]).includes(tag.tagName);
|
||||
const tagAttributes = tag.attributes ?? {};
|
||||
const attributes = Object.keys(tagAttributes)
|
||||
.map((attr) => {
|
||||
const value = tagAttributes[attr]!;
|
||||
let value = tagAttributes[attr]!;
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? attr : undefined;
|
||||
}
|
||||
if (router === 'hash') {
|
||||
value = hashRouterAbsoluteToRelativeTagAttribute(attr, value);
|
||||
}
|
||||
return `${attr}="${escapeHTML(value)}"`;
|
||||
})
|
||||
.filter((str): str is string => Boolean(str));
|
||||
|
@ -55,10 +75,18 @@ function htmlTagObjectToString(tag: unknown): string {
|
|||
return openingTag + innerHTML + closingTag;
|
||||
}
|
||||
|
||||
function createHtmlTagsString(tags: HtmlTags | undefined): string {
|
||||
function createHtmlTagsString({
|
||||
tags,
|
||||
router,
|
||||
}: {
|
||||
tags: HtmlTags | undefined;
|
||||
router: RouterType;
|
||||
}): string {
|
||||
return (Array.isArray(tags) ? tags : [tags])
|
||||
.filter(Boolean)
|
||||
.map((val) => (typeof val === 'string' ? val : htmlTagObjectToString(val)))
|
||||
.map((val) =>
|
||||
typeof val === 'string' ? val : htmlTagObjectToString({tag: val, router}),
|
||||
)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
|
@ -66,9 +94,13 @@ function createHtmlTagsString(tags: HtmlTags | undefined): string {
|
|||
* Runs the `injectHtmlTags` lifecycle, and aggregates all plugins' tags into
|
||||
* directly render-able HTML markup.
|
||||
*/
|
||||
export function loadHtmlTags(
|
||||
plugins: LoadedPlugin[],
|
||||
): Pick<Props, 'headTags' | 'preBodyTags' | 'postBodyTags'> {
|
||||
export function loadHtmlTags({
|
||||
plugins,
|
||||
router,
|
||||
}: {
|
||||
plugins: LoadedPlugin[];
|
||||
router: RouterType;
|
||||
}): Pick<Props, 'headTags' | 'preBodyTags' | 'postBodyTags'> {
|
||||
const pluginHtmlTags = plugins.map(
|
||||
(plugin) => plugin.injectHtmlTags?.({content: plugin.content}) ?? {},
|
||||
);
|
||||
|
@ -78,7 +110,7 @@ export function loadHtmlTags(
|
|||
tagTypes,
|
||||
tagTypes.map((type) =>
|
||||
pluginHtmlTags
|
||||
.map((tags) => createHtmlTagsString(tags[type]))
|
||||
.map((tags) => createHtmlTagsString({tags: tags[type], router}))
|
||||
.join('\n')
|
||||
.trim(),
|
||||
),
|
||||
|
|
|
@ -147,7 +147,10 @@ function createSiteProps(
|
|||
codeTranslations: siteCodeTranslations,
|
||||
} = context;
|
||||
|
||||
const {headTags, preBodyTags, postBodyTags} = loadHtmlTags(plugins);
|
||||
const {headTags, preBodyTags, postBodyTags} = loadHtmlTags({
|
||||
plugins,
|
||||
router: siteConfig.future.experimental_router,
|
||||
});
|
||||
|
||||
const siteMetadata = createSiteMetadata({plugins, siteVersion});
|
||||
|
||||
|
|
|
@ -227,6 +227,20 @@ It might also require to wrap your client code in ${logger.code(
|
|||
return parts.join('\n');
|
||||
}
|
||||
|
||||
export async function generateHashRouterEntrypoint({
|
||||
content,
|
||||
params,
|
||||
}: {
|
||||
content: string;
|
||||
params: SSGParams;
|
||||
}): Promise<void> {
|
||||
await writeStaticFile({
|
||||
pathname: '/',
|
||||
content,
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
async function writeStaticFile({
|
||||
content,
|
||||
pathname,
|
||||
|
|
|
@ -113,3 +113,41 @@ export function renderSSRTemplate({
|
|||
|
||||
return ssrTemplate(data);
|
||||
}
|
||||
|
||||
export function renderHashRouterTemplate({
|
||||
params,
|
||||
}: {
|
||||
params: SSGParams;
|
||||
}): string {
|
||||
const {
|
||||
// baseUrl,
|
||||
headTags,
|
||||
preBodyTags,
|
||||
postBodyTags,
|
||||
manifest,
|
||||
DOCUSAURUS_VERSION,
|
||||
ssrTemplate,
|
||||
} = params;
|
||||
|
||||
const {scripts, stylesheets} = getScriptsAndStylesheets({
|
||||
manifest,
|
||||
modules: [],
|
||||
});
|
||||
|
||||
const data: SSRTemplateData = {
|
||||
appHtml: '',
|
||||
baseUrl: './',
|
||||
htmlAttributes: '',
|
||||
bodyAttributes: '',
|
||||
headTags,
|
||||
preBodyTags,
|
||||
postBodyTags,
|
||||
metaAttributes: [],
|
||||
scripts,
|
||||
stylesheets,
|
||||
noIndex: false,
|
||||
version: DOCUSAURUS_VERSION,
|
||||
};
|
||||
|
||||
return ssrTemplate(data);
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ describe('base webpack config', () => {
|
|||
const props = {
|
||||
outDir: '',
|
||||
siteDir: path.resolve(__dirname, '__fixtures__', 'base_test_site'),
|
||||
siteConfig: {staticDirectories: ['static']},
|
||||
siteConfig: {staticDirectories: ['static'], future: {}},
|
||||
baseUrl: '',
|
||||
generatedFilesDir: '',
|
||||
routesPaths: [''],
|
||||
|
|
|
@ -112,7 +112,8 @@ export async function createBaseConfig({
|
|||
chunkFilename: isProd
|
||||
? 'assets/js/[name].[contenthash:8].js'
|
||||
: '[name].js',
|
||||
publicPath: baseUrl,
|
||||
publicPath:
|
||||
siteConfig.future.experimental_router === 'hash' ? 'auto' : baseUrl,
|
||||
hashFunction: 'xxhash64',
|
||||
},
|
||||
// Don't throw warning when asset created is over 250kb
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import path from 'path';
|
||||
import logger from '@docusaurus/logger';
|
||||
import merge from 'webpack-merge';
|
||||
import WebpackBar from 'webpackbar';
|
||||
import webpack from 'webpack';
|
||||
|
@ -15,29 +14,12 @@ import ReactLoadableSSRAddon from 'react-loadable-ssr-addon-v5-slorber';
|
|||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||
import {createBaseConfig} from './base';
|
||||
import ChunkAssetPlugin from './plugins/ChunkAssetPlugin';
|
||||
import {formatStatsErrorMessage} from './utils';
|
||||
import CleanWebpackPlugin from './plugins/CleanWebpackPlugin';
|
||||
import ForceTerminatePlugin from './plugins/ForceTerminatePlugin';
|
||||
import {createStaticDirectoriesCopyPlugin} from './plugins/StaticDirectoriesCopyPlugin';
|
||||
import type {Props} from '@docusaurus/types';
|
||||
import type {Configuration} from 'webpack';
|
||||
|
||||
// When building, include the plugin to force terminate building if errors
|
||||
// happened in the client bundle.
|
||||
class ForceTerminatePlugin implements webpack.WebpackPluginInstance {
|
||||
apply(compiler: webpack.Compiler) {
|
||||
compiler.hooks.done.tap('client:done', (stats) => {
|
||||
if (stats.hasErrors()) {
|
||||
const errorsWarnings = stats.toJson('errors-warnings');
|
||||
logger.error(
|
||||
`Client bundle compiled with errors therefore further build is impossible.\n${formatStatsErrorMessage(
|
||||
errorsWarnings,
|
||||
)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function createBaseClientConfig({
|
||||
props,
|
||||
hydrate,
|
||||
|
@ -68,6 +50,7 @@ async function createBaseClientConfig({
|
|||
new WebpackBar({
|
||||
name: 'Client',
|
||||
}),
|
||||
await createStaticDirectoriesCopyPlugin({props}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
@ -129,7 +112,12 @@ export async function createBuildClientConfig({
|
|||
bundleAnalyzer: boolean;
|
||||
}): Promise<{config: Configuration; clientManifestPath: string}> {
|
||||
// Apply user webpack config.
|
||||
const {generatedFilesDir} = props;
|
||||
const {generatedFilesDir, siteConfig} = props;
|
||||
const router = siteConfig.future.experimental_router;
|
||||
|
||||
// With the hash router, we don't hydrate the React app, even in build mode!
|
||||
// This is because it will always be a client-rendered React app
|
||||
const hydrate = router !== 'hash';
|
||||
|
||||
const clientManifestPath = path.join(
|
||||
generatedFilesDir,
|
||||
|
@ -137,7 +125,7 @@ export async function createBuildClientConfig({
|
|||
);
|
||||
|
||||
const config: Configuration = merge(
|
||||
await createBaseClientConfig({props, minify, hydrate: true}),
|
||||
await createBaseClientConfig({props, minify, hydrate}),
|
||||
{
|
||||
plugins: [
|
||||
new ForceTerminatePlugin(),
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import logger from '@docusaurus/logger';
|
||||
import {formatStatsErrorMessage} from '../utils';
|
||||
import type webpack from 'webpack';
|
||||
|
||||
// When building, include the plugin to force terminate building if errors
|
||||
// happened in the client bundle.
|
||||
export default class ForceTerminatePlugin
|
||||
implements webpack.WebpackPluginInstance
|
||||
{
|
||||
apply(compiler: webpack.Compiler) {
|
||||
compiler.hooks.done.tap('client:done', (stats) => {
|
||||
if (stats.hasErrors()) {
|
||||
const errorsWarnings = stats.toJson('errors-warnings');
|
||||
logger.error(
|
||||
`Client bundle compiled with errors therefore further build is impossible.\n${formatStatsErrorMessage(
|
||||
errorsWarnings,
|
||||
)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import CopyWebpackPlugin from 'copy-webpack-plugin';
|
||||
import type {Props} from '@docusaurus/types';
|
||||
|
||||
export async function createStaticDirectoriesCopyPlugin({
|
||||
props,
|
||||
}: {
|
||||
props: Props;
|
||||
}): Promise<CopyWebpackPlugin | undefined> {
|
||||
const {
|
||||
outDir,
|
||||
siteDir,
|
||||
siteConfig: {staticDirectories: staticDirectoriesOption},
|
||||
} = props;
|
||||
|
||||
// The staticDirectories option can contain empty directories, or non-existent
|
||||
// directories (e.g. user deleted `static`). Instead of issuing an error, we
|
||||
// just silently filter them out, because user could have never configured it
|
||||
// in the first place (the default option should always "work").
|
||||
const staticDirectories: string[] = (
|
||||
await Promise.all(
|
||||
staticDirectoriesOption.map(async (dir) => {
|
||||
const staticDir = path.resolve(siteDir, dir);
|
||||
if (
|
||||
(await fs.pathExists(staticDir)) &&
|
||||
(await fs.readdir(staticDir)).length > 0
|
||||
) {
|
||||
return staticDir;
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
)
|
||||
).filter(Boolean);
|
||||
|
||||
if (staticDirectories.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new CopyWebpackPlugin({
|
||||
patterns: staticDirectories.map((dir) => ({
|
||||
from: dir,
|
||||
to: outDir,
|
||||
toType: 'dir',
|
||||
})),
|
||||
});
|
||||
}
|
|
@ -6,11 +6,9 @@
|
|||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import merge from 'webpack-merge';
|
||||
import {NODE_MAJOR_VERSION, NODE_MINOR_VERSION} from '@docusaurus/utils';
|
||||
import WebpackBar from 'webpackbar';
|
||||
import CopyWebpackPlugin from 'copy-webpack-plugin';
|
||||
import {createBaseConfig} from './base';
|
||||
import type {Props} from '@docusaurus/types';
|
||||
import type {Configuration} from 'webpack';
|
||||
|
@ -48,48 +46,8 @@ export default async function createServerConfig(params: {
|
|||
name: 'Server',
|
||||
color: 'yellow',
|
||||
}),
|
||||
await createStaticDirectoriesCopyPlugin(params),
|
||||
].filter(Boolean),
|
||||
});
|
||||
|
||||
return {config, serverBundlePath};
|
||||
}
|
||||
|
||||
async function createStaticDirectoriesCopyPlugin({props}: {props: Props}) {
|
||||
const {
|
||||
outDir,
|
||||
siteDir,
|
||||
siteConfig: {staticDirectories: staticDirectoriesOption},
|
||||
} = props;
|
||||
|
||||
// The staticDirectories option can contain empty directories, or non-existent
|
||||
// directories (e.g. user deleted `static`). Instead of issuing an error, we
|
||||
// just silently filter them out, because user could have never configured it
|
||||
// in the first place (the default option should always "work").
|
||||
const staticDirectories: string[] = (
|
||||
await Promise.all(
|
||||
staticDirectoriesOption.map(async (dir) => {
|
||||
const staticDir = path.resolve(siteDir, dir);
|
||||
if (
|
||||
(await fs.pathExists(staticDir)) &&
|
||||
(await fs.readdir(staticDir)).length > 0
|
||||
) {
|
||||
return staticDir;
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
)
|
||||
).filter(Boolean);
|
||||
|
||||
if (staticDirectories.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new CopyWebpackPlugin({
|
||||
patterns: staticDirectories.map((dir) => ({
|
||||
from: dir,
|
||||
to: outDir,
|
||||
toType: 'dir',
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -201,6 +201,7 @@ export default {
|
|||
type: 'localStorage',
|
||||
namespace: true,
|
||||
},
|
||||
experimental_router: 'hash',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
@ -208,6 +209,7 @@ export default {
|
|||
- `experimental_storage`: Site-wide browser storage options that theme authors should strive to respect.
|
||||
- `type`: The browser storage theme authors should use. Possible values are `localStorage` and `sessionStorage`. Defaults to `localStorage`.
|
||||
- `namespace`: Whether to namespace the browser storage keys to avoid storage key conflicts when Docusaurus sites are hosted under the same domain, or on localhost. Possible values are `string | boolean`. The namespace is appended at the end of the storage keys `key-namespace`. Use `true` to automatically generate a random namespace from your site `url + baseUrl`. Defaults to `false` (no namespace, historical behavior).
|
||||
- `experimental_router`: The router type to use. Possible values are `browser` and `hash`. Defaults to `browser`. The `hash` router is only useful for rare cases where you want to opt-out of static site generation, have a fully client-side app with a single `index.html` entrypoint file. This can be useful to distribute a Docusaurus site as a `.zip` archive that you can [browse locally without running a web server](https://github.com/facebook/docusaurus/issues/3825).
|
||||
|
||||
### `noIndex` {#noIndex}
|
||||
|
||||
|
|
|
@ -23,8 +23,8 @@ import ConfigLocalized from './docusaurus.config.localized.json';
|
|||
|
||||
import PrismLight from './src/utils/prismLight';
|
||||
import PrismDark from './src/utils/prismDark';
|
||||
import type {Config, DocusaurusConfig} from '@docusaurus/types';
|
||||
|
||||
import type {Config} from '@docusaurus/types';
|
||||
import type * as Preset from '@docusaurus/preset-classic';
|
||||
import type {Options as DocsOptions} from '@docusaurus/plugin-content-docs';
|
||||
import type {Options as BlogOptions} from '@docusaurus/plugin-content-blog';
|
||||
|
@ -95,6 +95,9 @@ function getNextVersionName() {
|
|||
// Test with: DOCUSAURUS_CRASH_TEST=true yarn build:website:fast
|
||||
const crashTest = process.env.DOCUSAURUS_CRASH_TEST === 'true';
|
||||
|
||||
const router = process.env
|
||||
.DOCUSAURUS_ROUTER as DocusaurusConfig['future']['experimental_router'];
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
const isDeployPreview =
|
||||
|
@ -151,6 +154,7 @@ export default async function createConfigAsync() {
|
|||
experimental_storage: {
|
||||
namespace: true,
|
||||
},
|
||||
experimental_router: router,
|
||||
},
|
||||
// Dogfood both settings:
|
||||
// - force trailing slashes for deploy previews
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue