mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-12 15:52:39 +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 {addLeadingSlash, removePrefix} from '@docusaurus/utils-common';
|
||||||
|
import logger from '@docusaurus/logger';
|
||||||
import collectRedirects from './collectRedirects';
|
import collectRedirects from './collectRedirects';
|
||||||
import writeRedirectFiles, {
|
import writeRedirectFiles, {
|
||||||
toRedirectFiles,
|
toRedirectFiles,
|
||||||
|
@ -15,14 +16,24 @@ import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||||
import type {PluginContext, RedirectItem} from './types';
|
import type {PluginContext, RedirectItem} from './types';
|
||||||
import type {PluginOptions, Options} from './options';
|
import type {PluginOptions, Options} from './options';
|
||||||
|
|
||||||
|
const PluginName = 'docusaurus-plugin-client-redirects';
|
||||||
|
|
||||||
export default function pluginClientRedirectsPages(
|
export default function pluginClientRedirectsPages(
|
||||||
context: LoadContext,
|
context: LoadContext,
|
||||||
options: PluginOptions,
|
options: PluginOptions,
|
||||||
): Plugin<void> {
|
): Plugin<void> {
|
||||||
const {trailingSlash} = context.siteConfig;
|
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 {
|
return {
|
||||||
name: 'docusaurus-plugin-client-redirects',
|
name: PluginName,
|
||||||
async postBuild(props) {
|
async postBuild(props) {
|
||||||
const pluginContext: PluginContext = {
|
const pluginContext: PluginContext = {
|
||||||
relativeRoutesPaths: props.routesPaths.map(
|
relativeRoutesPaths: props.routesPaths.map(
|
||||||
|
|
|
@ -106,6 +106,7 @@ const getPlugin = async (
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
url: 'https://docusaurus.io',
|
url: 'https://docusaurus.io',
|
||||||
markdown,
|
markdown,
|
||||||
|
future: {},
|
||||||
} as DocusaurusConfig;
|
} as DocusaurusConfig;
|
||||||
return pluginContentBlog(
|
return pluginContentBlog(
|
||||||
{
|
{
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
applyTrailingSlash,
|
applyTrailingSlash,
|
||||||
} from '@docusaurus/utils-common';
|
} from '@docusaurus/utils-common';
|
||||||
import {load as cheerioLoad} from 'cheerio';
|
import {load as cheerioLoad} from 'cheerio';
|
||||||
import type {DocusaurusConfig} from '@docusaurus/types';
|
import type {DocusaurusConfig, HtmlTags, LoadContext} from '@docusaurus/types';
|
||||||
import type {
|
import type {
|
||||||
FeedType,
|
FeedType,
|
||||||
PluginOptions,
|
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';
|
} from './blogUtils';
|
||||||
import footnoteIDFixer from './remark/footnoteIDFixer';
|
import footnoteIDFixer from './remark/footnoteIDFixer';
|
||||||
import {translateContent, getTranslationFiles} from './translations';
|
import {translateContent, getTranslationFiles} from './translations';
|
||||||
import {createBlogFeedFiles} from './feed';
|
import {createBlogFeedFiles, createFeedHtmlHeadTags} from './feed';
|
||||||
|
|
||||||
import {createAllRoutes} from './routes';
|
import {createAllRoutes} from './routes';
|
||||||
import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';
|
import type {BlogContentPaths, BlogMarkdownLoaderOptions} from './types';
|
||||||
import type {LoadContext, Plugin, HtmlTags} from '@docusaurus/types';
|
import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||||
import type {
|
import type {
|
||||||
PluginOptions,
|
PluginOptions,
|
||||||
BlogPostFrontMatter,
|
BlogPostFrontMatter,
|
||||||
|
@ -44,6 +44,8 @@ import type {
|
||||||
BlogPaginated,
|
BlogPaginated,
|
||||||
} from '@docusaurus/plugin-content-blog';
|
} from '@docusaurus/plugin-content-blog';
|
||||||
|
|
||||||
|
const PluginName = 'docusaurus-plugin-content-blog';
|
||||||
|
|
||||||
export default async function pluginContentBlog(
|
export default async function pluginContentBlog(
|
||||||
context: LoadContext,
|
context: LoadContext,
|
||||||
options: PluginOptions,
|
options: PluginOptions,
|
||||||
|
@ -55,22 +57,29 @@ export default async function pluginContentBlog(
|
||||||
localizationDir,
|
localizationDir,
|
||||||
i18n: {currentLocale},
|
i18n: {currentLocale},
|
||||||
} = context;
|
} = 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 {onBrokenMarkdownLinks, baseUrl} = siteConfig;
|
||||||
|
|
||||||
const contentPaths: BlogContentPaths = {
|
const contentPaths: BlogContentPaths = {
|
||||||
contentPath: path.resolve(siteDir, options.path),
|
contentPath: path.resolve(siteDir, options.path),
|
||||||
contentPathLocalized: getPluginI18nPath({
|
contentPathLocalized: getPluginI18nPath({
|
||||||
localizationDir,
|
localizationDir,
|
||||||
pluginName: 'docusaurus-plugin-content-blog',
|
pluginName: PluginName,
|
||||||
pluginId: options.id,
|
pluginId: options.id,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
const pluginId = options.id ?? DEFAULT_PLUGIN_ID;
|
const pluginId = options.id ?? DEFAULT_PLUGIN_ID;
|
||||||
|
|
||||||
const pluginDataDirRoot = path.join(
|
const pluginDataDirRoot = path.join(generatedFilesDir, PluginName);
|
||||||
generatedFilesDir,
|
|
||||||
'docusaurus-plugin-content-blog',
|
|
||||||
);
|
|
||||||
const dataDir = path.join(pluginDataDirRoot, pluginId);
|
const dataDir = path.join(pluginDataDirRoot, pluginId);
|
||||||
// TODO Docusaurus v4 breaking change
|
// TODO Docusaurus v4 breaking change
|
||||||
// module aliasing should be automatic
|
// module aliasing should be automatic
|
||||||
|
@ -84,7 +93,7 @@ export default async function pluginContentBlog(
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'docusaurus-plugin-content-blog',
|
name: PluginName,
|
||||||
|
|
||||||
getPathsToWatch() {
|
getPathsToWatch() {
|
||||||
const {include} = options;
|
const {include} = options;
|
||||||
|
@ -295,15 +304,16 @@ export default async function pluginContentBlog(
|
||||||
},
|
},
|
||||||
|
|
||||||
async postBuild({outDir, content}) {
|
async postBuild({outDir, content}) {
|
||||||
if (!options.feedOptions.type) {
|
if (
|
||||||
return;
|
!content.blogPosts.length ||
|
||||||
}
|
!options.feedOptions.type ||
|
||||||
const {blogPosts} = content;
|
isBlogFeedDisabledBecauseOfHashRouter
|
||||||
if (!blogPosts.length) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await createBlogFeedFiles({
|
await createBlogFeedFiles({
|
||||||
blogPosts,
|
blogPosts: content.blogPosts,
|
||||||
options,
|
options,
|
||||||
outDir,
|
outDir,
|
||||||
siteConfig,
|
siteConfig,
|
||||||
|
@ -312,56 +322,15 @@ export default async function pluginContentBlog(
|
||||||
},
|
},
|
||||||
|
|
||||||
injectHtmlTags({content}) {
|
injectHtmlTags({content}) {
|
||||||
if (!content.blogPosts.length || !options.feedOptions.type) {
|
if (
|
||||||
|
!content.blogPosts.length ||
|
||||||
|
!options.feedOptions.type ||
|
||||||
|
isBlogFeedDisabledBecauseOfHashRouter
|
||||||
|
) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const feedTypes = options.feedOptions.type;
|
return {headTags: createFeedHtmlHeadTags({context, options})};
|
||||||
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,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
"@babel/core": "^7.23.3",
|
"@babel/core": "^7.23.3",
|
||||||
"@babel/preset-env": "^7.23.3",
|
"@babel/preset-env": "^7.23.3",
|
||||||
"@docusaurus/core": "3.3.2",
|
"@docusaurus/core": "3.3.2",
|
||||||
|
"@docusaurus/logger": "3.3.2",
|
||||||
"@docusaurus/theme-common": "3.3.2",
|
"@docusaurus/theme-common": "3.3.2",
|
||||||
"@docusaurus/theme-translations": "3.3.2",
|
"@docusaurus/theme-translations": "3.3.2",
|
||||||
"@docusaurus/types": "3.3.2",
|
"@docusaurus/types": "3.3.2",
|
||||||
|
|
|
@ -11,11 +11,14 @@ import WebpackBar from 'webpackbar';
|
||||||
import Terser from 'terser-webpack-plugin';
|
import Terser from 'terser-webpack-plugin';
|
||||||
import {injectManifest} from 'workbox-build';
|
import {injectManifest} from 'workbox-build';
|
||||||
import {normalizeUrl} from '@docusaurus/utils';
|
import {normalizeUrl} from '@docusaurus/utils';
|
||||||
|
import logger from '@docusaurus/logger';
|
||||||
import {compile} from '@docusaurus/core/lib/webpack/utils';
|
import {compile} from '@docusaurus/core/lib/webpack/utils';
|
||||||
import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations';
|
import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations';
|
||||||
import type {HtmlTags, LoadContext, Plugin} from '@docusaurus/types';
|
import type {HtmlTags, LoadContext, Plugin} from '@docusaurus/types';
|
||||||
import type {PluginOptions} from '@docusaurus/plugin-pwa';
|
import type {PluginOptions} from '@docusaurus/plugin-pwa';
|
||||||
|
|
||||||
|
const PluginName = 'docusaurus-plugin-pwa';
|
||||||
|
|
||||||
const isProd = process.env.NODE_ENV === 'production';
|
const isProd = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
function getSWBabelLoader() {
|
function getSWBabelLoader() {
|
||||||
|
@ -47,6 +50,7 @@ export default function pluginPWA(
|
||||||
outDir,
|
outDir,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
i18n: {currentLocale},
|
i18n: {currentLocale},
|
||||||
|
siteConfig,
|
||||||
} = context;
|
} = context;
|
||||||
const {
|
const {
|
||||||
debug,
|
debug,
|
||||||
|
@ -57,8 +61,15 @@ export default function pluginPWA(
|
||||||
swRegister,
|
swRegister,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
|
if (siteConfig.future.experimental_router === 'hash') {
|
||||||
|
logger.warn(
|
||||||
|
`${PluginName} does not support the Hash Router and will be disabled.`,
|
||||||
|
);
|
||||||
|
return {name: PluginName};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'docusaurus-plugin-pwa',
|
name: PluginName,
|
||||||
|
|
||||||
getThemePath() {
|
getThemePath() {
|
||||||
return '../lib/theme';
|
return '../lib/theme';
|
||||||
|
|
|
@ -12,12 +12,21 @@ import createSitemap from './createSitemap';
|
||||||
import type {PluginOptions, Options} from './options';
|
import type {PluginOptions, Options} from './options';
|
||||||
import type {LoadContext, Plugin} from '@docusaurus/types';
|
import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||||
|
|
||||||
|
const PluginName = 'docusaurus-plugin-sitemap';
|
||||||
|
|
||||||
export default function pluginSitemap(
|
export default function pluginSitemap(
|
||||||
context: LoadContext,
|
context: LoadContext,
|
||||||
options: PluginOptions,
|
options: PluginOptions,
|
||||||
): Plugin<void> {
|
): 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 {
|
return {
|
||||||
name: 'docusaurus-plugin-sitemap',
|
name: PluginName,
|
||||||
|
|
||||||
async postBuild({siteConfig, routes, outDir, head}) {
|
async postBuild({siteConfig, routes, outDir, head}) {
|
||||||
if (siteConfig.noIndex) {
|
if (siteConfig.noIndex) {
|
||||||
|
|
|
@ -5,38 +5,21 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* 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 {normalizeUrl} from '@docusaurus/utils';
|
||||||
import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations';
|
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 {LoadContext, Plugin} from '@docusaurus/types';
|
||||||
import type {ThemeConfig} from '@docusaurus/theme-search-algolia';
|
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> {
|
export default function themeSearchAlgolia(context: LoadContext): Plugin<void> {
|
||||||
const {
|
const {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
siteConfig: {title, url, favicon, themeConfig},
|
siteConfig: {themeConfig},
|
||||||
i18n: {currentLocale},
|
i18n: {currentLocale},
|
||||||
} = context;
|
} = context;
|
||||||
const {
|
const {
|
||||||
|
@ -70,45 +53,17 @@ export default function themeSearchAlgolia(context: LoadContext): Plugin<void> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async postBuild({outDir}) {
|
async postBuild() {
|
||||||
if (searchPagePath) {
|
if (shouldCreateOpenSearchFile({context})) {
|
||||||
const siteUrl = normalizeUrl([url, baseUrl]);
|
await createOpenSearchFile({context});
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
injectHtmlTags() {
|
injectHtmlTags() {
|
||||||
if (!searchPagePath) {
|
if (shouldCreateOpenSearchFile({context})) {
|
||||||
return {};
|
return {headTags: createOpenSearchHeadTags({context})};
|
||||||
}
|
}
|
||||||
|
return {};
|
||||||
return {
|
|
||||||
headTags: [
|
|
||||||
{
|
|
||||||
tagName: 'link',
|
|
||||||
attributes: {
|
|
||||||
rel: 'search',
|
|
||||||
type: 'application/opensearchdescription+xml',
|
|
||||||
title,
|
|
||||||
href: normalizeUrl([baseUrl, OPEN_SEARCH_FILENAME]),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
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 ReportingSeverity = 'ignore' | 'log' | 'warn' | 'throw';
|
||||||
|
|
||||||
|
export type RouterType = 'browser' | 'hash';
|
||||||
|
|
||||||
export type ThemeConfig = {
|
export type ThemeConfig = {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
@ -123,6 +125,24 @@ export type StorageConfig = {
|
||||||
|
|
||||||
export type FutureConfig = {
|
export type FutureConfig = {
|
||||||
experimental_storage: StorageConfig;
|
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 {
|
export {
|
||||||
ReportingSeverity,
|
ReportingSeverity,
|
||||||
|
RouterType,
|
||||||
ThemeConfig,
|
ThemeConfig,
|
||||||
MarkdownConfig,
|
MarkdownConfig,
|
||||||
DefaultParseFrontMatter,
|
DefaultParseFrontMatter,
|
||||||
|
|
|
@ -90,6 +90,30 @@ describe('normalizeUrl', () => {
|
||||||
input: ['http://foobar.com', '', 'test', '/'],
|
input: ['http://foobar.com', '', 'test', '/'],
|
||||||
output: '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'],
|
input: ['/', '', 'hello', '', '/', '/', '', '/', '/world'],
|
||||||
output: '/hello/world',
|
output: '/hello/world',
|
||||||
|
|
|
@ -90,7 +90,7 @@ export function normalizeUrl(rawUrls: string[]): string {
|
||||||
// first plain protocol part.
|
// first plain protocol part.
|
||||||
|
|
||||||
// Remove trailing slash before parameters or hash.
|
// Remove trailing slash before parameters or hash.
|
||||||
str = str.replace(/\/(?<search>\?|&|#[^!])/g, '$1');
|
str = str.replace(/\/(?<search>\?|&|#[^!/])/g, '$1');
|
||||||
|
|
||||||
// Replace ? in parameters with &.
|
// Replace ? in parameters with &.
|
||||||
const parts = str.split('?');
|
const parts = str.split('?');
|
||||||
|
|
|
@ -5,16 +5,24 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* 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 ReactDOM, {type ErrorInfo} from 'react-dom/client';
|
||||||
import {BrowserRouter} from 'react-router-dom';
|
|
||||||
import {HelmetProvider} from 'react-helmet-async';
|
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 ExecutionEnvironment from './exports/ExecutionEnvironment';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import preload from './preload';
|
import preload from './preload';
|
||||||
import docusaurus from './docusaurus';
|
import docusaurus from './docusaurus';
|
||||||
|
|
||||||
|
function Router({children}: {children: ReactNode}): ReactNode {
|
||||||
|
return siteConfig.future.experimental_router === 'hash' ? (
|
||||||
|
<HashRouter>{children}</HashRouter>
|
||||||
|
) : (
|
||||||
|
<BrowserRouter>{children}</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface NodeModule {
|
interface NodeModule {
|
||||||
hot?: {accept: () => void};
|
hot?: {accept: () => void};
|
||||||
|
@ -31,9 +39,9 @@ if (ExecutionEnvironment.canUseDOM) {
|
||||||
|
|
||||||
const app = (
|
const app = (
|
||||||
<HelmetProvider>
|
<HelmetProvider>
|
||||||
<BrowserRouter>
|
<Router>
|
||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
</Router>
|
||||||
</HelmetProvider>
|
</HelmetProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -40,9 +40,9 @@ function Link(
|
||||||
}: Props,
|
}: Props,
|
||||||
forwardedRef: React.ForwardedRef<HTMLAnchorElement>,
|
forwardedRef: React.ForwardedRef<HTMLAnchorElement>,
|
||||||
): JSX.Element {
|
): JSX.Element {
|
||||||
const {
|
const {siteConfig} = useDocusaurusContext();
|
||||||
siteConfig: {trailingSlash, baseUrl},
|
const {trailingSlash, baseUrl} = siteConfig;
|
||||||
} = useDocusaurusContext();
|
const router = siteConfig.future.experimental_router;
|
||||||
const {withBaseUrl} = useBaseUrlUtils();
|
const {withBaseUrl} = useBaseUrlUtils();
|
||||||
const brokenLinks = useBrokenLinks();
|
const brokenLinks = useBrokenLinks();
|
||||||
const innerRef = useRef<HTMLAnchorElement | null>(null);
|
const innerRef = useRef<HTMLAnchorElement | null>(null);
|
||||||
|
@ -81,6 +81,15 @@ function Link(
|
||||||
? maybeAddBaseUrl(targetLinkWithoutPathnameProtocol)
|
? maybeAddBaseUrl(targetLinkWithoutPathnameProtocol)
|
||||||
: undefined;
|
: 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) {
|
if (targetLink && isInternal) {
|
||||||
targetLink = applyTrailingSlash(targetLink, {trailingSlash, baseUrl});
|
targetLink = applyTrailingSlash(targetLink, {trailingSlash, baseUrl});
|
||||||
}
|
}
|
||||||
|
@ -148,8 +157,7 @@ function Link(
|
||||||
const hasInternalTarget = !props.target || props.target === '_self';
|
const hasInternalTarget = !props.target || props.target === '_self';
|
||||||
|
|
||||||
// Should we use a regular <a> tag instead of React-Router Link component?
|
// Should we use a regular <a> tag instead of React-Router Link component?
|
||||||
const isRegularHtmlLink =
|
const isRegularHtmlLink = !targetLink || !isInternal || !hasInternalTarget;
|
||||||
!targetLink || !isInternal || !hasInternalTarget || isAnchorLink;
|
|
||||||
|
|
||||||
if (!noBrokenLinkCheck && (isAnchorLink || !isRegularHtmlLink)) {
|
if (!noBrokenLinkCheck && (isAnchorLink || !isRegularHtmlLink)) {
|
||||||
brokenLinks.collectLink(targetLink!);
|
brokenLinks.collectLink(targetLink!);
|
||||||
|
|
|
@ -7,13 +7,222 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {renderHook} from '@testing-library/react-hooks';
|
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 {Context} from '../../docusaurusContext';
|
||||||
import type {DocusaurusContext} from '@docusaurus/types';
|
import type {DocusaurusContext, FutureConfig} from '@docusaurus/types';
|
||||||
import type {BaseUrlOptions} from '@docusaurus/useBaseUrl';
|
import type {BaseUrlOptions} from '@docusaurus/useBaseUrl';
|
||||||
|
|
||||||
|
type AddBaseUrlParams = Parameters<typeof addBaseUrl>[0];
|
||||||
|
|
||||||
|
const future: FutureConfig = fromPartial({
|
||||||
|
experimental_router: 'browser',
|
||||||
|
});
|
||||||
|
|
||||||
const forcePrepend = {forcePrependBaseUrl: true};
|
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', () => {
|
describe('useBaseUrl', () => {
|
||||||
const createUseBaseUrlMock =
|
const createUseBaseUrlMock =
|
||||||
(context: DocusaurusContext) => (url: string, options?: BaseUrlOptions) =>
|
(context: DocusaurusContext) => (url: string, options?: BaseUrlOptions) =>
|
||||||
|
@ -27,6 +236,7 @@ describe('useBaseUrl', () => {
|
||||||
siteConfig: {
|
siteConfig: {
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
url: 'https://docusaurus.io',
|
url: 'https://docusaurus.io',
|
||||||
|
future,
|
||||||
},
|
},
|
||||||
} as DocusaurusContext);
|
} as DocusaurusContext);
|
||||||
|
|
||||||
|
@ -55,6 +265,7 @@ describe('useBaseUrl', () => {
|
||||||
siteConfig: {
|
siteConfig: {
|
||||||
baseUrl: '/docusaurus/',
|
baseUrl: '/docusaurus/',
|
||||||
url: 'https://docusaurus.io',
|
url: 'https://docusaurus.io',
|
||||||
|
future,
|
||||||
},
|
},
|
||||||
} as DocusaurusContext);
|
} as DocusaurusContext);
|
||||||
|
|
||||||
|
@ -96,6 +307,7 @@ describe('useBaseUrlUtils().withBaseUrl()', () => {
|
||||||
siteConfig: {
|
siteConfig: {
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
url: 'https://docusaurus.io',
|
url: 'https://docusaurus.io',
|
||||||
|
future,
|
||||||
},
|
},
|
||||||
} as DocusaurusContext);
|
} as DocusaurusContext);
|
||||||
|
|
||||||
|
@ -124,6 +336,7 @@ describe('useBaseUrlUtils().withBaseUrl()', () => {
|
||||||
siteConfig: {
|
siteConfig: {
|
||||||
baseUrl: '/docusaurus/',
|
baseUrl: '/docusaurus/',
|
||||||
url: 'https://docusaurus.io',
|
url: 'https://docusaurus.io',
|
||||||
|
future,
|
||||||
},
|
},
|
||||||
} as DocusaurusContext);
|
} as DocusaurusContext);
|
||||||
|
|
||||||
|
|
|
@ -9,19 +9,34 @@ import {useCallback} from 'react';
|
||||||
import useDocusaurusContext from './useDocusaurusContext';
|
import useDocusaurusContext from './useDocusaurusContext';
|
||||||
import {hasProtocol} from './isInternalUrl';
|
import {hasProtocol} from './isInternalUrl';
|
||||||
import type {BaseUrlOptions, BaseUrlUtils} from '@docusaurus/useBaseUrl';
|
import type {BaseUrlOptions, BaseUrlUtils} from '@docusaurus/useBaseUrl';
|
||||||
|
import type {RouterType} from '@docusaurus/types';
|
||||||
|
|
||||||
function addBaseUrl(
|
export function addBaseUrl({
|
||||||
siteUrl: string,
|
siteUrl,
|
||||||
baseUrl: string,
|
baseUrl,
|
||||||
url: string,
|
url,
|
||||||
{forcePrependBaseUrl = false, absolute = false}: BaseUrlOptions = {},
|
options: {forcePrependBaseUrl = false, absolute = false} = {},
|
||||||
): string {
|
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
|
// It never makes sense to add base url to a local anchor url, or one with a
|
||||||
// protocol
|
// protocol
|
||||||
if (!url || url.startsWith('#') || hasProtocol(url)) {
|
if (!url || url.startsWith('#') || hasProtocol(url)) {
|
||||||
return 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) {
|
if (forcePrependBaseUrl) {
|
||||||
return baseUrl + url.replace(/^\//, '');
|
return baseUrl + url.replace(/^\//, '');
|
||||||
}
|
}
|
||||||
|
@ -41,14 +56,14 @@ function addBaseUrl(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBaseUrlUtils(): BaseUrlUtils {
|
export function useBaseUrlUtils(): BaseUrlUtils {
|
||||||
const {
|
const {siteConfig} = useDocusaurusContext();
|
||||||
siteConfig: {baseUrl, url: siteUrl},
|
const {baseUrl, url: siteUrl} = siteConfig;
|
||||||
} = useDocusaurusContext();
|
const router = siteConfig.future.experimental_router;
|
||||||
|
|
||||||
const withBaseUrl = useCallback(
|
const withBaseUrl = useCallback(
|
||||||
(url: string, options?: BaseUrlOptions) =>
|
(url: string, options?: BaseUrlOptions) =>
|
||||||
addBaseUrl(siteUrl, baseUrl, url, options),
|
addBaseUrl({siteUrl, baseUrl, url, options, router}),
|
||||||
[siteUrl, baseUrl],
|
[siteUrl, baseUrl, router],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -20,12 +20,20 @@ import {compile} from '../webpack/utils';
|
||||||
import {PerfLogger} from '../utils';
|
import {PerfLogger} from '../utils';
|
||||||
|
|
||||||
import {loadI18n} from '../server/i18n';
|
import {loadI18n} from '../server/i18n';
|
||||||
import {generateStaticFiles, loadAppRenderer} from '../ssg';
|
import {
|
||||||
import {compileSSRTemplate} from '../templates/templates';
|
generateHashRouterEntrypoint,
|
||||||
|
generateStaticFiles,
|
||||||
|
loadAppRenderer,
|
||||||
|
} from '../ssg';
|
||||||
|
import {
|
||||||
|
compileSSRTemplate,
|
||||||
|
renderHashRouterTemplate,
|
||||||
|
} from '../templates/templates';
|
||||||
import defaultSSRTemplate from '../templates/ssr.html.template';
|
import defaultSSRTemplate from '../templates/ssr.html.template';
|
||||||
|
import type {SSGParams} from '../ssg';
|
||||||
|
|
||||||
import type {Manifest} from 'react-loadable-ssr-addon-v5-slorber';
|
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';
|
import type {SiteCollectedData} from '../common';
|
||||||
|
|
||||||
export type BuildCLIOptions = Pick<
|
export type BuildCLIOptions = Pick<
|
||||||
|
@ -164,7 +172,9 @@ async function buildLocale({
|
||||||
);
|
);
|
||||||
|
|
||||||
const {props} = site;
|
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
|
// We can build the 2 configs in parallel
|
||||||
const [{clientConfig, clientManifestPath}, {serverConfig, serverBundlePath}] =
|
const [{clientConfig, clientManifestPath}, {serverConfig, serverBundlePath}] =
|
||||||
|
@ -181,15 +191,20 @@ async function buildLocale({
|
||||||
);
|
);
|
||||||
|
|
||||||
// Run webpack to build JS bundle (client) and static html files (server).
|
// Run webpack to build JS bundle (client) and static html files (server).
|
||||||
await PerfLogger.async('Bundling with Webpack', () =>
|
await PerfLogger.async('Bundling with Webpack', () => {
|
||||||
compile([clientConfig, serverConfig]),
|
if (router === 'hash') {
|
||||||
);
|
return compile([clientConfig]);
|
||||||
|
} else {
|
||||||
|
return compile([clientConfig, serverConfig]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const {collectedData} = await PerfLogger.async('SSG', () =>
|
const {collectedData} = await PerfLogger.async('SSG', () =>
|
||||||
executeSSG({
|
executeSSG({
|
||||||
props,
|
props,
|
||||||
serverBundlePath,
|
serverBundlePath,
|
||||||
clientManifestPath,
|
clientManifestPath,
|
||||||
|
router,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -220,11 +235,13 @@ async function executeSSG({
|
||||||
props,
|
props,
|
||||||
serverBundlePath,
|
serverBundlePath,
|
||||||
clientManifestPath,
|
clientManifestPath,
|
||||||
|
router,
|
||||||
}: {
|
}: {
|
||||||
props: Props;
|
props: Props;
|
||||||
serverBundlePath: string;
|
serverBundlePath: string;
|
||||||
clientManifestPath: string;
|
clientManifestPath: string;
|
||||||
}) {
|
router: RouterType;
|
||||||
|
}): Promise<{collectedData: SiteCollectedData}> {
|
||||||
const manifest: Manifest = await PerfLogger.async(
|
const manifest: Manifest = await PerfLogger.async(
|
||||||
'Read client manifest',
|
'Read client manifest',
|
||||||
() => fs.readJSON(clientManifestPath, 'utf-8'),
|
() => fs.readJSON(clientManifestPath, 'utf-8'),
|
||||||
|
@ -234,6 +251,27 @@ async function executeSSG({
|
||||||
compileSSRTemplate(props.siteConfig.ssrTemplate ?? defaultSSRTemplate),
|
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', () =>
|
const renderer = await PerfLogger.async('Load App renderer', () =>
|
||||||
loadAppRenderer({
|
loadAppRenderer({
|
||||||
serverBundlePath,
|
serverBundlePath,
|
||||||
|
@ -244,18 +282,7 @@ async function executeSSG({
|
||||||
generateStaticFiles({
|
generateStaticFiles({
|
||||||
pathnames: props.routesPaths,
|
pathnames: props.routesPaths,
|
||||||
renderer,
|
renderer,
|
||||||
params: {
|
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,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -20,12 +20,18 @@ import {
|
||||||
} from '../../server/site';
|
} from '../../server/site';
|
||||||
import {formatPluginName} from '../../server/plugins/pluginsUtils';
|
import {formatPluginName} from '../../server/plugins/pluginsUtils';
|
||||||
import type {StartCLIOptions} from './start';
|
import type {StartCLIOptions} from './start';
|
||||||
import type {LoadedPlugin} from '@docusaurus/types';
|
import type {LoadedPlugin, RouterType} from '@docusaurus/types';
|
||||||
|
|
||||||
export type OpenUrlContext = {
|
export type OpenUrlContext = {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
getOpenUrl: ({baseUrl}: {baseUrl: string}) => string;
|
getOpenUrl: ({
|
||||||
|
baseUrl,
|
||||||
|
router,
|
||||||
|
}: {
|
||||||
|
baseUrl: string;
|
||||||
|
router: RouterType;
|
||||||
|
}) => string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function createOpenUrlContext({
|
export async function createOpenUrlContext({
|
||||||
|
@ -40,9 +46,13 @@ export async function createOpenUrlContext({
|
||||||
return process.exit();
|
return process.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOpenUrl: OpenUrlContext['getOpenUrl'] = ({baseUrl}) => {
|
const getOpenUrl: OpenUrlContext['getOpenUrl'] = ({baseUrl, router}) => {
|
||||||
const urls = prepareUrls(protocol, host, port);
|
const urls = prepareUrls(protocol, host, port);
|
||||||
return normalizeUrl([urls.localUrlForBrowser, baseUrl]);
|
return normalizeUrl([
|
||||||
|
urls.localUrlForBrowser,
|
||||||
|
router === 'hash' ? '/#/' : '',
|
||||||
|
baseUrl,
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {host, port, getOpenUrl};
|
return {host, port, getOpenUrl};
|
||||||
|
@ -83,6 +93,7 @@ export async function createReloadableSite(startParams: StartParams) {
|
||||||
const getOpenUrl = () =>
|
const getOpenUrl = () =>
|
||||||
openUrlContext.getOpenUrl({
|
openUrlContext.getOpenUrl({
|
||||||
baseUrl: site.props.baseUrl,
|
baseUrl: site.props.baseUrl,
|
||||||
|
router: site.props.siteConfig.future.experimental_router,
|
||||||
});
|
});
|
||||||
|
|
||||||
const printOpenUrlMessage = () => {
|
const printOpenUrlMessage = () => {
|
||||||
|
|
|
@ -81,7 +81,8 @@ async function createDevServerConfig({
|
||||||
'access-control-allow-origin': '*',
|
'access-control-allow-origin': '*',
|
||||||
},
|
},
|
||||||
devMiddleware: {
|
devMiddleware: {
|
||||||
publicPath: baseUrl,
|
publicPath:
|
||||||
|
siteConfig.future.experimental_router === 'hash' ? 'auto' : baseUrl,
|
||||||
// Reduce log verbosity, see https://github.com/facebook/docusaurus/pull/5420#issuecomment-906613105
|
// Reduce log verbosity, see https://github.com/facebook/docusaurus/pull/5420#issuecomment-906613105
|
||||||
stats: 'summary',
|
stats: 'summary',
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,6 +8,7 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = `
|
||||||
"clientModules": [],
|
"clientModules": [],
|
||||||
"customFields": {},
|
"customFields": {},
|
||||||
"future": {
|
"future": {
|
||||||
|
"experimental_router": "browser",
|
||||||
"experimental_storage": {
|
"experimental_storage": {
|
||||||
"namespace": false,
|
"namespace": false,
|
||||||
"type": "localStorage",
|
"type": "localStorage",
|
||||||
|
@ -68,6 +69,7 @@ exports[`loadSiteConfig website with ts + js config 1`] = `
|
||||||
"clientModules": [],
|
"clientModules": [],
|
||||||
"customFields": {},
|
"customFields": {},
|
||||||
"future": {
|
"future": {
|
||||||
|
"experimental_router": "browser",
|
||||||
"experimental_storage": {
|
"experimental_storage": {
|
||||||
"namespace": false,
|
"namespace": false,
|
||||||
"type": "localStorage",
|
"type": "localStorage",
|
||||||
|
@ -128,6 +130,7 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = `
|
||||||
"clientModules": [],
|
"clientModules": [],
|
||||||
"customFields": {},
|
"customFields": {},
|
||||||
"future": {
|
"future": {
|
||||||
|
"experimental_router": "browser",
|
||||||
"experimental_storage": {
|
"experimental_storage": {
|
||||||
"namespace": false,
|
"namespace": false,
|
||||||
"type": "localStorage",
|
"type": "localStorage",
|
||||||
|
@ -188,6 +191,7 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = `
|
||||||
"clientModules": [],
|
"clientModules": [],
|
||||||
"customFields": {},
|
"customFields": {},
|
||||||
"future": {
|
"future": {
|
||||||
|
"experimental_router": "browser",
|
||||||
"experimental_storage": {
|
"experimental_storage": {
|
||||||
"namespace": false,
|
"namespace": false,
|
||||||
"type": "localStorage",
|
"type": "localStorage",
|
||||||
|
@ -248,6 +252,7 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = `
|
||||||
"clientModules": [],
|
"clientModules": [],
|
||||||
"customFields": {},
|
"customFields": {},
|
||||||
"future": {
|
"future": {
|
||||||
|
"experimental_router": "browser",
|
||||||
"experimental_storage": {
|
"experimental_storage": {
|
||||||
"namespace": false,
|
"namespace": false,
|
||||||
"type": "localStorage",
|
"type": "localStorage",
|
||||||
|
@ -308,6 +313,7 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = `
|
||||||
"clientModules": [],
|
"clientModules": [],
|
||||||
"customFields": {},
|
"customFields": {},
|
||||||
"future": {
|
"future": {
|
||||||
|
"experimental_router": "browser",
|
||||||
"experimental_storage": {
|
"experimental_storage": {
|
||||||
"namespace": false,
|
"namespace": false,
|
||||||
"type": "localStorage",
|
"type": "localStorage",
|
||||||
|
@ -368,6 +374,7 @@ exports[`loadSiteConfig website with valid async config 1`] = `
|
||||||
"clientModules": [],
|
"clientModules": [],
|
||||||
"customFields": {},
|
"customFields": {},
|
||||||
"future": {
|
"future": {
|
||||||
|
"experimental_router": "browser",
|
||||||
"experimental_storage": {
|
"experimental_storage": {
|
||||||
"namespace": false,
|
"namespace": false,
|
||||||
"type": "localStorage",
|
"type": "localStorage",
|
||||||
|
@ -430,6 +437,7 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = `
|
||||||
"clientModules": [],
|
"clientModules": [],
|
||||||
"customFields": {},
|
"customFields": {},
|
||||||
"future": {
|
"future": {
|
||||||
|
"experimental_router": "browser",
|
||||||
"experimental_storage": {
|
"experimental_storage": {
|
||||||
"namespace": false,
|
"namespace": false,
|
||||||
"type": "localStorage",
|
"type": "localStorage",
|
||||||
|
@ -492,6 +500,7 @@ exports[`loadSiteConfig website with valid config creator function 1`] = `
|
||||||
"clientModules": [],
|
"clientModules": [],
|
||||||
"customFields": {},
|
"customFields": {},
|
||||||
"future": {
|
"future": {
|
||||||
|
"experimental_router": "browser",
|
||||||
"experimental_storage": {
|
"experimental_storage": {
|
||||||
"namespace": false,
|
"namespace": false,
|
||||||
"type": "localStorage",
|
"type": "localStorage",
|
||||||
|
@ -557,6 +566,7 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = `
|
||||||
"customFields": {},
|
"customFields": {},
|
||||||
"favicon": "img/docusaurus.ico",
|
"favicon": "img/docusaurus.ico",
|
||||||
"future": {
|
"future": {
|
||||||
|
"experimental_router": "browser",
|
||||||
"experimental_storage": {
|
"experimental_storage": {
|
||||||
"namespace": false,
|
"namespace": false,
|
||||||
"type": "localStorage",
|
"type": "localStorage",
|
||||||
|
|
|
@ -78,6 +78,7 @@ exports[`load loads props for site with custom i18n path 1`] = `
|
||||||
"clientModules": [],
|
"clientModules": [],
|
||||||
"customFields": {},
|
"customFields": {},
|
||||||
"future": {
|
"future": {
|
||||||
|
"experimental_router": "browser",
|
||||||
"experimental_storage": {
|
"experimental_storage": {
|
||||||
"namespace": false,
|
"namespace": false,
|
||||||
"type": "localStorage",
|
"type": "localStorage",
|
||||||
|
|
|
@ -42,6 +42,7 @@ describe('normalizeConfig', () => {
|
||||||
type: 'sessionStorage',
|
type: 'sessionStorage',
|
||||||
namespace: true,
|
namespace: true,
|
||||||
},
|
},
|
||||||
|
experimental_router: 'hash',
|
||||||
},
|
},
|
||||||
tagline: 'my awesome site',
|
tagline: 'my awesome site',
|
||||||
organizationName: 'facebook',
|
organizationName: 'facebook',
|
||||||
|
@ -648,6 +649,7 @@ describe('future', () => {
|
||||||
type: 'sessionStorage',
|
type: 'sessionStorage',
|
||||||
namespace: 'myNamespace',
|
namespace: 'myNamespace',
|
||||||
},
|
},
|
||||||
|
experimental_router: 'hash',
|
||||||
};
|
};
|
||||||
expect(
|
expect(
|
||||||
normalizeConfig({
|
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', () => {
|
describe('storage', () => {
|
||||||
it('accepts storage - undefined', () => {
|
it('accepts storage - undefined', () => {
|
||||||
expect(
|
expect(
|
||||||
|
@ -707,9 +800,9 @@ describe('future', () => {
|
||||||
}),
|
}),
|
||||||
).toEqual(
|
).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
future: {
|
future: expect.objectContaining({
|
||||||
experimental_storage: storage,
|
experimental_storage: storage,
|
||||||
},
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -757,12 +850,12 @@ describe('future', () => {
|
||||||
}),
|
}),
|
||||||
).toEqual(
|
).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
future: {
|
future: expect.objectContaining({
|
||||||
experimental_storage: {
|
experimental_storage: {
|
||||||
...DEFAULT_STORAGE_CONFIG,
|
...DEFAULT_STORAGE_CONFIG,
|
||||||
...storage,
|
...storage,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -779,12 +872,12 @@ describe('future', () => {
|
||||||
}),
|
}),
|
||||||
).toEqual(
|
).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
future: {
|
future: expect.objectContaining({
|
||||||
experimental_storage: {
|
experimental_storage: {
|
||||||
...DEFAULT_STORAGE_CONFIG,
|
...DEFAULT_STORAGE_CONFIG,
|
||||||
type: 'localStorage',
|
type: 'localStorage',
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -850,12 +943,12 @@ describe('future', () => {
|
||||||
}),
|
}),
|
||||||
).toEqual(
|
).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
future: {
|
future: expect.objectContaining({
|
||||||
experimental_storage: {
|
experimental_storage: {
|
||||||
...DEFAULT_STORAGE_CONFIG,
|
...DEFAULT_STORAGE_CONFIG,
|
||||||
...storage,
|
...storage,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -872,12 +965,12 @@ describe('future', () => {
|
||||||
}),
|
}),
|
||||||
).toEqual(
|
).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
future: {
|
future: expect.objectContaining({
|
||||||
experimental_storage: {
|
experimental_storage: {
|
||||||
...DEFAULT_STORAGE_CONFIG,
|
...DEFAULT_STORAGE_CONFIG,
|
||||||
...storage,
|
...storage,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,6 +8,10 @@
|
||||||
import {loadHtmlTags} from '../htmlTags';
|
import {loadHtmlTags} from '../htmlTags';
|
||||||
import type {LoadedPlugin} from '@docusaurus/types';
|
import type {LoadedPlugin} from '@docusaurus/types';
|
||||||
|
|
||||||
|
function testHtmlTags(plugins: LoadedPlugin[]) {
|
||||||
|
return loadHtmlTags({plugins, router: 'browser'});
|
||||||
|
}
|
||||||
|
|
||||||
const pluginEmpty = {
|
const pluginEmpty = {
|
||||||
name: 'plugin-empty',
|
name: 'plugin-empty',
|
||||||
} as LoadedPlugin;
|
} as LoadedPlugin;
|
||||||
|
@ -85,7 +89,7 @@ const pluginMaybeInjectHeadTags = {
|
||||||
|
|
||||||
describe('loadHtmlTags', () => {
|
describe('loadHtmlTags', () => {
|
||||||
it('works for an empty plugin', () => {
|
it('works for an empty plugin', () => {
|
||||||
const htmlTags = loadHtmlTags([pluginEmpty]);
|
const htmlTags = testHtmlTags([pluginEmpty]);
|
||||||
expect(htmlTags).toMatchInlineSnapshot(`
|
expect(htmlTags).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"headTags": "",
|
"headTags": "",
|
||||||
|
@ -96,7 +100,7 @@ describe('loadHtmlTags', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('only injects headTags', () => {
|
it('only injects headTags', () => {
|
||||||
const htmlTags = loadHtmlTags([pluginHeadTags]);
|
const htmlTags = testHtmlTags([pluginHeadTags]);
|
||||||
expect(htmlTags).toMatchInlineSnapshot(`
|
expect(htmlTags).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"headTags": "<link rel="preconnect" href="www.google-analytics.com">
|
"headTags": "<link rel="preconnect" href="www.google-analytics.com">
|
||||||
|
@ -109,7 +113,7 @@ describe('loadHtmlTags', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('only injects preBodyTags', () => {
|
it('only injects preBodyTags', () => {
|
||||||
const htmlTags = loadHtmlTags([pluginPreBodyTags]);
|
const htmlTags = testHtmlTags([pluginPreBodyTags]);
|
||||||
expect(htmlTags).toMatchInlineSnapshot(`
|
expect(htmlTags).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"headTags": "",
|
"headTags": "",
|
||||||
|
@ -120,7 +124,7 @@ describe('loadHtmlTags', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('only injects postBodyTags', () => {
|
it('only injects postBodyTags', () => {
|
||||||
const htmlTags = loadHtmlTags([pluginPostBodyTags]);
|
const htmlTags = testHtmlTags([pluginPostBodyTags]);
|
||||||
expect(htmlTags).toMatchInlineSnapshot(`
|
expect(htmlTags).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"headTags": "",
|
"headTags": "",
|
||||||
|
@ -132,7 +136,7 @@ describe('loadHtmlTags', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows multiple plugins that inject different part of html tags', () => {
|
it('allows multiple plugins that inject different part of html tags', () => {
|
||||||
const htmlTags = loadHtmlTags([
|
const htmlTags = testHtmlTags([
|
||||||
pluginHeadTags,
|
pluginHeadTags,
|
||||||
pluginPostBodyTags,
|
pluginPostBodyTags,
|
||||||
pluginPreBodyTags,
|
pluginPreBodyTags,
|
||||||
|
@ -150,7 +154,7 @@ describe('loadHtmlTags', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows multiple plugins that might/might not inject html tags', () => {
|
it('allows multiple plugins that might/might not inject html tags', () => {
|
||||||
const htmlTags = loadHtmlTags([
|
const htmlTags = testHtmlTags([
|
||||||
pluginEmpty,
|
pluginEmpty,
|
||||||
pluginHeadTags,
|
pluginHeadTags,
|
||||||
pluginPostBodyTags,
|
pluginPostBodyTags,
|
||||||
|
@ -169,7 +173,7 @@ describe('loadHtmlTags', () => {
|
||||||
});
|
});
|
||||||
it('throws for invalid tag', () => {
|
it('throws for invalid tag', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
loadHtmlTags([
|
testHtmlTags([
|
||||||
// @ts-expect-error: test
|
// @ts-expect-error: test
|
||||||
{
|
{
|
||||||
injectHtmlTags() {
|
injectHtmlTags() {
|
||||||
|
@ -191,7 +195,7 @@ describe('loadHtmlTags', () => {
|
||||||
|
|
||||||
it('throws for invalid tagName', () => {
|
it('throws for invalid tagName', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
loadHtmlTags([
|
testHtmlTags([
|
||||||
{
|
{
|
||||||
// @ts-expect-error: test
|
// @ts-expect-error: test
|
||||||
injectHtmlTags() {
|
injectHtmlTags() {
|
||||||
|
@ -210,7 +214,7 @@ describe('loadHtmlTags', () => {
|
||||||
|
|
||||||
it('throws for invalid tag object', () => {
|
it('throws for invalid tag object', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
loadHtmlTags([
|
testHtmlTags([
|
||||||
{
|
{
|
||||||
// @ts-expect-error: test
|
// @ts-expect-error: test
|
||||||
injectHtmlTags() {
|
injectHtmlTags() {
|
||||||
|
|
|
@ -39,6 +39,7 @@ export const DEFAULT_STORAGE_CONFIG: StorageConfig = {
|
||||||
|
|
||||||
export const DEFAULT_FUTURE_CONFIG: FutureConfig = {
|
export const DEFAULT_FUTURE_CONFIG: FutureConfig = {
|
||||||
experimental_storage: DEFAULT_STORAGE_CONFIG,
|
experimental_storage: DEFAULT_STORAGE_CONFIG,
|
||||||
|
experimental_router: 'browser',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_MARKDOWN_CONFIG: MarkdownConfig = {
|
export const DEFAULT_MARKDOWN_CONFIG: MarkdownConfig = {
|
||||||
|
@ -206,6 +207,9 @@ const STORAGE_CONFIG_SCHEMA = Joi.object({
|
||||||
|
|
||||||
const FUTURE_CONFIG_SCHEMA = Joi.object<FutureConfig>({
|
const FUTURE_CONFIG_SCHEMA = Joi.object<FutureConfig>({
|
||||||
experimental_storage: STORAGE_CONFIG_SCHEMA,
|
experimental_storage: STORAGE_CONFIG_SCHEMA,
|
||||||
|
experimental_router: Joi.string()
|
||||||
|
.equal('browser', 'hash')
|
||||||
|
.default(DEFAULT_FUTURE_CONFIG.experimental_router),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default(DEFAULT_FUTURE_CONFIG);
|
.default(DEFAULT_FUTURE_CONFIG);
|
||||||
|
|
|
@ -14,6 +14,7 @@ import type {
|
||||||
HtmlTagObject,
|
HtmlTagObject,
|
||||||
HtmlTags,
|
HtmlTags,
|
||||||
LoadedPlugin,
|
LoadedPlugin,
|
||||||
|
RouterType,
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
|
|
||||||
function assertIsHtmlTagObject(val: unknown): asserts val is HtmlTagObject {
|
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);
|
assertIsHtmlTagObject(tag);
|
||||||
const isVoidTag = (voidHtmlTags as string[]).includes(tag.tagName);
|
const isVoidTag = (voidHtmlTags as string[]).includes(tag.tagName);
|
||||||
const tagAttributes = tag.attributes ?? {};
|
const tagAttributes = tag.attributes ?? {};
|
||||||
const attributes = Object.keys(tagAttributes)
|
const attributes = Object.keys(tagAttributes)
|
||||||
.map((attr) => {
|
.map((attr) => {
|
||||||
const value = tagAttributes[attr]!;
|
let value = tagAttributes[attr]!;
|
||||||
if (typeof value === 'boolean') {
|
if (typeof value === 'boolean') {
|
||||||
return value ? attr : undefined;
|
return value ? attr : undefined;
|
||||||
}
|
}
|
||||||
|
if (router === 'hash') {
|
||||||
|
value = hashRouterAbsoluteToRelativeTagAttribute(attr, value);
|
||||||
|
}
|
||||||
return `${attr}="${escapeHTML(value)}"`;
|
return `${attr}="${escapeHTML(value)}"`;
|
||||||
})
|
})
|
||||||
.filter((str): str is string => Boolean(str));
|
.filter((str): str is string => Boolean(str));
|
||||||
|
@ -55,10 +75,18 @@ function htmlTagObjectToString(tag: unknown): string {
|
||||||
return openingTag + innerHTML + closingTag;
|
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])
|
return (Array.isArray(tags) ? tags : [tags])
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((val) => (typeof val === 'string' ? val : htmlTagObjectToString(val)))
|
.map((val) =>
|
||||||
|
typeof val === 'string' ? val : htmlTagObjectToString({tag: val, router}),
|
||||||
|
)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,9 +94,13 @@ function createHtmlTagsString(tags: HtmlTags | undefined): string {
|
||||||
* Runs the `injectHtmlTags` lifecycle, and aggregates all plugins' tags into
|
* Runs the `injectHtmlTags` lifecycle, and aggregates all plugins' tags into
|
||||||
* directly render-able HTML markup.
|
* directly render-able HTML markup.
|
||||||
*/
|
*/
|
||||||
export function loadHtmlTags(
|
export function loadHtmlTags({
|
||||||
plugins: LoadedPlugin[],
|
plugins,
|
||||||
): Pick<Props, 'headTags' | 'preBodyTags' | 'postBodyTags'> {
|
router,
|
||||||
|
}: {
|
||||||
|
plugins: LoadedPlugin[];
|
||||||
|
router: RouterType;
|
||||||
|
}): Pick<Props, 'headTags' | 'preBodyTags' | 'postBodyTags'> {
|
||||||
const pluginHtmlTags = plugins.map(
|
const pluginHtmlTags = plugins.map(
|
||||||
(plugin) => plugin.injectHtmlTags?.({content: plugin.content}) ?? {},
|
(plugin) => plugin.injectHtmlTags?.({content: plugin.content}) ?? {},
|
||||||
);
|
);
|
||||||
|
@ -78,7 +110,7 @@ export function loadHtmlTags(
|
||||||
tagTypes,
|
tagTypes,
|
||||||
tagTypes.map((type) =>
|
tagTypes.map((type) =>
|
||||||
pluginHtmlTags
|
pluginHtmlTags
|
||||||
.map((tags) => createHtmlTagsString(tags[type]))
|
.map((tags) => createHtmlTagsString({tags: tags[type], router}))
|
||||||
.join('\n')
|
.join('\n')
|
||||||
.trim(),
|
.trim(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -147,7 +147,10 @@ function createSiteProps(
|
||||||
codeTranslations: siteCodeTranslations,
|
codeTranslations: siteCodeTranslations,
|
||||||
} = context;
|
} = context;
|
||||||
|
|
||||||
const {headTags, preBodyTags, postBodyTags} = loadHtmlTags(plugins);
|
const {headTags, preBodyTags, postBodyTags} = loadHtmlTags({
|
||||||
|
plugins,
|
||||||
|
router: siteConfig.future.experimental_router,
|
||||||
|
});
|
||||||
|
|
||||||
const siteMetadata = createSiteMetadata({plugins, siteVersion});
|
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');
|
return parts.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function generateHashRouterEntrypoint({
|
||||||
|
content,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
content: string;
|
||||||
|
params: SSGParams;
|
||||||
|
}): Promise<void> {
|
||||||
|
await writeStaticFile({
|
||||||
|
pathname: '/',
|
||||||
|
content,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function writeStaticFile({
|
async function writeStaticFile({
|
||||||
content,
|
content,
|
||||||
pathname,
|
pathname,
|
||||||
|
|
|
@ -113,3 +113,41 @@ export function renderSSRTemplate({
|
||||||
|
|
||||||
return ssrTemplate(data);
|
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 = {
|
const props = {
|
||||||
outDir: '',
|
outDir: '',
|
||||||
siteDir: path.resolve(__dirname, '__fixtures__', 'base_test_site'),
|
siteDir: path.resolve(__dirname, '__fixtures__', 'base_test_site'),
|
||||||
siteConfig: {staticDirectories: ['static']},
|
siteConfig: {staticDirectories: ['static'], future: {}},
|
||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
generatedFilesDir: '',
|
generatedFilesDir: '',
|
||||||
routesPaths: [''],
|
routesPaths: [''],
|
||||||
|
|
|
@ -112,7 +112,8 @@ export async function createBaseConfig({
|
||||||
chunkFilename: isProd
|
chunkFilename: isProd
|
||||||
? 'assets/js/[name].[contenthash:8].js'
|
? 'assets/js/[name].[contenthash:8].js'
|
||||||
: '[name].js',
|
: '[name].js',
|
||||||
publicPath: baseUrl,
|
publicPath:
|
||||||
|
siteConfig.future.experimental_router === 'hash' ? 'auto' : baseUrl,
|
||||||
hashFunction: 'xxhash64',
|
hashFunction: 'xxhash64',
|
||||||
},
|
},
|
||||||
// Don't throw warning when asset created is over 250kb
|
// Don't throw warning when asset created is over 250kb
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import logger from '@docusaurus/logger';
|
|
||||||
import merge from 'webpack-merge';
|
import merge from 'webpack-merge';
|
||||||
import WebpackBar from 'webpackbar';
|
import WebpackBar from 'webpackbar';
|
||||||
import webpack from 'webpack';
|
import webpack from 'webpack';
|
||||||
|
@ -15,29 +14,12 @@ import ReactLoadableSSRAddon from 'react-loadable-ssr-addon-v5-slorber';
|
||||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||||
import {createBaseConfig} from './base';
|
import {createBaseConfig} from './base';
|
||||||
import ChunkAssetPlugin from './plugins/ChunkAssetPlugin';
|
import ChunkAssetPlugin from './plugins/ChunkAssetPlugin';
|
||||||
import {formatStatsErrorMessage} from './utils';
|
|
||||||
import CleanWebpackPlugin from './plugins/CleanWebpackPlugin';
|
import CleanWebpackPlugin from './plugins/CleanWebpackPlugin';
|
||||||
|
import ForceTerminatePlugin from './plugins/ForceTerminatePlugin';
|
||||||
|
import {createStaticDirectoriesCopyPlugin} from './plugins/StaticDirectoriesCopyPlugin';
|
||||||
import type {Props} from '@docusaurus/types';
|
import type {Props} from '@docusaurus/types';
|
||||||
import type {Configuration} from 'webpack';
|
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({
|
async function createBaseClientConfig({
|
||||||
props,
|
props,
|
||||||
hydrate,
|
hydrate,
|
||||||
|
@ -68,6 +50,7 @@ async function createBaseClientConfig({
|
||||||
new WebpackBar({
|
new WebpackBar({
|
||||||
name: 'Client',
|
name: 'Client',
|
||||||
}),
|
}),
|
||||||
|
await createStaticDirectoriesCopyPlugin({props}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -129,7 +112,12 @@ export async function createBuildClientConfig({
|
||||||
bundleAnalyzer: boolean;
|
bundleAnalyzer: boolean;
|
||||||
}): Promise<{config: Configuration; clientManifestPath: string}> {
|
}): Promise<{config: Configuration; clientManifestPath: string}> {
|
||||||
// Apply user webpack config.
|
// 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(
|
const clientManifestPath = path.join(
|
||||||
generatedFilesDir,
|
generatedFilesDir,
|
||||||
|
@ -137,7 +125,7 @@ export async function createBuildClientConfig({
|
||||||
);
|
);
|
||||||
|
|
||||||
const config: Configuration = merge(
|
const config: Configuration = merge(
|
||||||
await createBaseClientConfig({props, minify, hydrate: true}),
|
await createBaseClientConfig({props, minify, hydrate}),
|
||||||
{
|
{
|
||||||
plugins: [
|
plugins: [
|
||||||
new ForceTerminatePlugin(),
|
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 path from 'path';
|
||||||
import fs from 'fs-extra';
|
|
||||||
import merge from 'webpack-merge';
|
import merge from 'webpack-merge';
|
||||||
import {NODE_MAJOR_VERSION, NODE_MINOR_VERSION} from '@docusaurus/utils';
|
import {NODE_MAJOR_VERSION, NODE_MINOR_VERSION} from '@docusaurus/utils';
|
||||||
import WebpackBar from 'webpackbar';
|
import WebpackBar from 'webpackbar';
|
||||||
import CopyWebpackPlugin from 'copy-webpack-plugin';
|
|
||||||
import {createBaseConfig} from './base';
|
import {createBaseConfig} from './base';
|
||||||
import type {Props} from '@docusaurus/types';
|
import type {Props} from '@docusaurus/types';
|
||||||
import type {Configuration} from 'webpack';
|
import type {Configuration} from 'webpack';
|
||||||
|
@ -48,48 +46,8 @@ export default async function createServerConfig(params: {
|
||||||
name: 'Server',
|
name: 'Server',
|
||||||
color: 'yellow',
|
color: 'yellow',
|
||||||
}),
|
}),
|
||||||
await createStaticDirectoriesCopyPlugin(params),
|
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {config, serverBundlePath};
|
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',
|
type: 'localStorage',
|
||||||
namespace: true,
|
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.
|
- `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`.
|
- `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).
|
- `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}
|
### `noIndex` {#noIndex}
|
||||||
|
|
||||||
|
|
|
@ -23,8 +23,8 @@ import ConfigLocalized from './docusaurus.config.localized.json';
|
||||||
|
|
||||||
import PrismLight from './src/utils/prismLight';
|
import PrismLight from './src/utils/prismLight';
|
||||||
import PrismDark from './src/utils/prismDark';
|
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 * as Preset from '@docusaurus/preset-classic';
|
||||||
import type {Options as DocsOptions} from '@docusaurus/plugin-content-docs';
|
import type {Options as DocsOptions} from '@docusaurus/plugin-content-docs';
|
||||||
import type {Options as BlogOptions} from '@docusaurus/plugin-content-blog';
|
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
|
// Test with: DOCUSAURUS_CRASH_TEST=true yarn build:website:fast
|
||||||
const crashTest = process.env.DOCUSAURUS_CRASH_TEST === 'true';
|
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 isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
const isDeployPreview =
|
const isDeployPreview =
|
||||||
|
@ -151,6 +154,7 @@ export default async function createConfigAsync() {
|
||||||
experimental_storage: {
|
experimental_storage: {
|
||||||
namespace: true,
|
namespace: true,
|
||||||
},
|
},
|
||||||
|
experimental_router: router,
|
||||||
},
|
},
|
||||||
// Dogfood both settings:
|
// Dogfood both settings:
|
||||||
// - force trailing slashes for deploy previews
|
// - force trailing slashes for deploy previews
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue