@docusaurus/core 2.4.1 → 3.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/beforeCli.mjs +35 -10
- package/bin/docusaurus.mjs +9 -1
- package/lib/babel/preset.js +6 -1
- package/lib/client/App.js +2 -0
- package/lib/client/BaseUrlIssueBanner/index.d.ts +0 -7
- package/lib/client/BaseUrlIssueBanner/index.js +7 -19
- package/lib/client/ClientLifecyclesDispatcher.js +2 -2
- package/lib/client/LinksCollector.d.ts +2 -2
- package/lib/client/PendingNavigation.d.ts +2 -2
- package/lib/client/clientEntry.js +27 -13
- package/lib/client/exports/ErrorBoundary.d.ts +1 -1
- package/lib/client/exports/Link.d.ts +1 -1
- package/lib/client/exports/Link.js +1 -1
- package/lib/client/exports/useIsomorphicLayoutEffect.d.ts +21 -0
- package/lib/client/exports/useIsomorphicLayoutEffect.js +24 -0
- package/lib/{webpack/plugins/LogPlugin.d.ts → client/hasHydratedDataAttribute.d.ts} +2 -5
- package/lib/client/hasHydratedDataAttribute.js +17 -0
- package/lib/client/serverEntry.js +26 -18
- package/lib/client/serverRenderer.d.ts +8 -0
- package/lib/client/serverRenderer.js +61 -0
- package/lib/commands/build.d.ts +2 -1
- package/lib/commands/build.js +9 -3
- package/lib/commands/deploy.d.ts +1 -1
- package/lib/commands/deploy.js +8 -0
- package/lib/commands/serve.d.ts +1 -1
- package/lib/commands/start.d.ts +1 -1
- package/lib/commands/start.js +15 -5
- package/lib/commands/swizzle/actions.d.ts +2 -2
- package/lib/commands/swizzle/common.d.ts +3 -3
- package/lib/commands/swizzle/components.d.ts +1 -1
- package/lib/commands/writeTranslations.d.ts +1 -1
- package/lib/server/configValidation.js +23 -1
- package/lib/server/getHostPort.d.ts +1 -1
- package/lib/server/index.d.ts +1 -1
- package/lib/server/index.js +4 -2
- package/lib/server/plugins/configs.d.ts +2 -2
- package/lib/server/plugins/routeConfig.js +3 -1
- package/lib/server/plugins/synthetic.js +1 -2
- package/lib/server/routes.d.ts +1 -1
- package/lib/server/routes.js +2 -18
- package/lib/server/translations/translations.d.ts +2 -2
- package/lib/server/translations/translationsExtractor.d.ts +1 -1
- package/lib/webpack/aliases/index.d.ts +1 -1
- package/lib/webpack/client.d.ts +1 -1
- package/lib/webpack/client.js +10 -4
- package/lib/webpack/plugins/CleanWebpackPlugin.d.ts +1 -1
- package/lib/webpack/plugins/CleanWebpackPlugin.js +8 -0
- package/lib/webpack/plugins/WaitPlugin.d.ts +1 -1
- package/lib/webpack/server.js +2 -2
- package/lib/webpack/templates/ssr.html.template.d.ts +1 -1
- package/lib/webpack/templates/ssr.html.template.js +2 -7
- package/lib/webpack/utils.d.ts +3 -1
- package/lib/webpack/utils.js +31 -9
- package/package.json +57 -57
- package/lib/webpack/plugins/LogPlugin.js +0 -33
package/bin/beforeCli.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import fs from 'fs-extra';
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import {createRequire} from 'module';
|
|
13
|
+
import shell from 'shelljs';
|
|
13
14
|
import logger from '@docusaurus/logger';
|
|
14
15
|
import semver from 'semver';
|
|
15
16
|
import updateNotifier from 'update-notifier';
|
|
@@ -105,18 +106,33 @@ export default async function beforeCli() {
|
|
|
105
106
|
.map((p) => p.concat('@latest'))
|
|
106
107
|
.join(' ');
|
|
107
108
|
|
|
109
|
+
const getYarnVersion = async () => {
|
|
110
|
+
if (!(await fs.pathExists(path.resolve('yarn.lock')))) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const yarnVersionResult = shell.exec('yarn --version', {silent: true});
|
|
115
|
+
if (yarnVersionResult?.code === 0) {
|
|
116
|
+
const majorVersion = parseInt(
|
|
117
|
+
yarnVersionResult.stdout?.trim().split('.')[0] ?? '',
|
|
118
|
+
10,
|
|
119
|
+
);
|
|
120
|
+
if (!Number.isNaN(majorVersion)) {
|
|
121
|
+
return majorVersion;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return undefined;
|
|
126
|
+
};
|
|
127
|
+
|
|
108
128
|
const getUpgradeCommand = async () => {
|
|
109
|
-
const
|
|
110
|
-
if (!
|
|
129
|
+
const yarnVersion = await getYarnVersion();
|
|
130
|
+
if (!yarnVersion) {
|
|
111
131
|
return `npm i ${siteDocusaurusPackagesForUpdate}`;
|
|
112
132
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
));
|
|
117
|
-
return isYarnClassicUsed
|
|
118
|
-
? `yarn upgrade ${siteDocusaurusPackagesForUpdate}`
|
|
119
|
-
: `yarn up ${siteDocusaurusPackagesForUpdate}`;
|
|
133
|
+
return yarnVersion >= 2
|
|
134
|
+
? `yarn up ${siteDocusaurusPackagesForUpdate}`
|
|
135
|
+
: `yarn upgrade ${siteDocusaurusPackagesForUpdate}`;
|
|
120
136
|
};
|
|
121
137
|
|
|
122
138
|
/** @type {import('boxen').Options} */
|
|
@@ -125,7 +141,16 @@ export default async function beforeCli() {
|
|
|
125
141
|
margin: 1,
|
|
126
142
|
align: 'center',
|
|
127
143
|
borderColor: 'yellow',
|
|
128
|
-
borderStyle:
|
|
144
|
+
borderStyle: {
|
|
145
|
+
topLeft: ' ',
|
|
146
|
+
topRight: ' ',
|
|
147
|
+
bottomLeft: ' ',
|
|
148
|
+
bottomRight: ' ',
|
|
149
|
+
top: '-',
|
|
150
|
+
bottom: '-',
|
|
151
|
+
left: ' ',
|
|
152
|
+
right: ' ',
|
|
153
|
+
},
|
|
129
154
|
};
|
|
130
155
|
|
|
131
156
|
const docusaurusUpdateMessage = boxen(
|
package/bin/docusaurus.mjs
CHANGED
|
@@ -37,6 +37,10 @@ cli.version(DOCUSAURUS_VERSION).usage('<command> [options]');
|
|
|
37
37
|
cli
|
|
38
38
|
.command('build [siteDir]')
|
|
39
39
|
.description('Build website.')
|
|
40
|
+
.option(
|
|
41
|
+
'--dev',
|
|
42
|
+
'Builds the website in dev mode, including full React error messages.',
|
|
43
|
+
)
|
|
40
44
|
.option(
|
|
41
45
|
'--bundle-analyzer',
|
|
42
46
|
'visualize size of webpack output files with an interactive zoomable tree map (default: false)',
|
|
@@ -244,7 +248,11 @@ if (!process.argv.slice(2).length) {
|
|
|
244
248
|
cli.parse(process.argv);
|
|
245
249
|
|
|
246
250
|
process.on('unhandledRejection', (err) => {
|
|
247
|
-
|
|
251
|
+
console.log('');
|
|
252
|
+
// Do not use logger.error here: it does not print error causes
|
|
253
|
+
console.error(err);
|
|
254
|
+
console.log('');
|
|
255
|
+
|
|
248
256
|
logger.info`Docusaurus version: number=${DOCUSAURUS_VERSION}
|
|
249
257
|
Node version: number=${process.version}`;
|
|
250
258
|
process.exit(1);
|
package/lib/babel/preset.js
CHANGED
|
@@ -36,7 +36,12 @@ function getTransformOptions(isServer) {
|
|
|
36
36
|
exclude: ['transform-typeof-symbol'],
|
|
37
37
|
},
|
|
38
38
|
],
|
|
39
|
-
|
|
39
|
+
[
|
|
40
|
+
require.resolve('@babel/preset-react'),
|
|
41
|
+
{
|
|
42
|
+
runtime: 'automatic',
|
|
43
|
+
},
|
|
44
|
+
],
|
|
40
45
|
require.resolve('@babel/preset-typescript'),
|
|
41
46
|
],
|
|
42
47
|
plugins: [
|
package/lib/client/App.js
CHANGED
|
@@ -20,6 +20,7 @@ import SiteMetadataDefaults from './SiteMetadataDefaults';
|
|
|
20
20
|
// TODO, quick fix for CSS insertion order
|
|
21
21
|
// eslint-disable-next-line import/order
|
|
22
22
|
import ErrorBoundary from '@docusaurus/ErrorBoundary';
|
|
23
|
+
import HasHydratedDataAttribute from './hasHydratedDataAttribute';
|
|
23
24
|
export default function App() {
|
|
24
25
|
const routeElement = renderRoutes(routes);
|
|
25
26
|
const location = useLocation();
|
|
@@ -34,6 +35,7 @@ export default function App() {
|
|
|
34
35
|
{routeElement}
|
|
35
36
|
</PendingNavigation>
|
|
36
37
|
</Root>
|
|
38
|
+
<HasHydratedDataAttribute />
|
|
37
39
|
</BrowserContextProvider>
|
|
38
40
|
</DocusaurusContextProvider>
|
|
39
41
|
</ErrorBoundary>);
|
|
@@ -6,12 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
/// <reference types="react" />
|
|
8
8
|
import './styles.module.css';
|
|
9
|
-
declare const InsertBannerWindowAttribute = "__DOCUSAURUS_INSERT_BASEURL_BANNER";
|
|
10
|
-
declare global {
|
|
11
|
-
interface Window {
|
|
12
|
-
[InsertBannerWindowAttribute]: boolean;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
9
|
/**
|
|
16
10
|
* We want to help the users with a bad baseUrl configuration (very common
|
|
17
11
|
* error). Help message is inlined, and hidden if JS or CSS is able to load.
|
|
@@ -24,4 +18,3 @@ declare global {
|
|
|
24
18
|
* @see https://github.com/facebook/docusaurus/pull/3621
|
|
25
19
|
*/
|
|
26
20
|
export default function MaybeBaseUrlIssueBanner(): JSX.Element | null;
|
|
27
|
-
export {};
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* This source code is licensed under the MIT license found in the
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
|
-
import React
|
|
7
|
+
import React from 'react';
|
|
8
8
|
import { useLocation } from '@docusaurus/router';
|
|
9
9
|
import Head from '@docusaurus/Head';
|
|
10
10
|
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
|
@@ -16,7 +16,6 @@ import './styles.module.css';
|
|
|
16
16
|
const BannerContainerId = '__docusaurus-base-url-issue-banner-container';
|
|
17
17
|
const BannerId = '__docusaurus-base-url-issue-banner';
|
|
18
18
|
const SuggestionContainerId = '__docusaurus-base-url-issue-banner-suggestion-container';
|
|
19
|
-
const InsertBannerWindowAttribute = '__DOCUSAURUS_INSERT_BASEURL_BANNER';
|
|
20
19
|
// It is important to not use React to render this banner
|
|
21
20
|
// otherwise Google would index it, even if it's hidden with some critical CSS!
|
|
22
21
|
// See https://github.com/facebook/docusaurus/issues/4028
|
|
@@ -36,24 +35,19 @@ function createInlineHtmlBanner(baseUrl) {
|
|
|
36
35
|
function createInlineScript(baseUrl) {
|
|
37
36
|
/* language=js */
|
|
38
37
|
return `
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
document.addEventListener('DOMContentLoaded', maybeInsertBanner);
|
|
42
|
-
|
|
43
|
-
function maybeInsertBanner() {
|
|
44
|
-
var shouldInsert = window['${InsertBannerWindowAttribute}'];
|
|
38
|
+
document.addEventListener('DOMContentLoaded', function maybeInsertBanner() {
|
|
39
|
+
var shouldInsert = typeof window['docusaurus'] === 'undefined';
|
|
45
40
|
shouldInsert && insertBanner();
|
|
46
|
-
}
|
|
41
|
+
});
|
|
47
42
|
|
|
48
43
|
function insertBanner() {
|
|
49
|
-
var bannerContainer = document.
|
|
50
|
-
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
44
|
+
var bannerContainer = document.createElement('div');
|
|
45
|
+
bannerContainer.id = '${BannerContainerId}';
|
|
53
46
|
var bannerHtml = ${JSON.stringify(createInlineHtmlBanner(baseUrl))
|
|
54
47
|
// See https://redux.js.org/recipes/server-rendering/#security-considerations
|
|
55
48
|
.replace(/</g, '\\\u003c')};
|
|
56
49
|
bannerContainer.innerHTML = bannerHtml;
|
|
50
|
+
document.body.prepend(bannerContainer);
|
|
57
51
|
var suggestionContainer = document.getElementById('${SuggestionContainerId}');
|
|
58
52
|
var actualHomePagePath = window.location.pathname;
|
|
59
53
|
var suggestedBaseUrl = actualHomePagePath.substr(-1) === '/'
|
|
@@ -65,11 +59,6 @@ function insertBanner() {
|
|
|
65
59
|
}
|
|
66
60
|
function BaseUrlIssueBanner() {
|
|
67
61
|
const { siteConfig: { baseUrl }, } = useDocusaurusContext();
|
|
68
|
-
// useLayoutEffect fires before DOMContentLoaded.
|
|
69
|
-
// It gives the opportunity to avoid inserting the banner in the first place
|
|
70
|
-
useLayoutEffect(() => {
|
|
71
|
-
window[InsertBannerWindowAttribute] = false;
|
|
72
|
-
}, []);
|
|
73
62
|
return (<>
|
|
74
63
|
{!ExecutionEnvironment.canUseDOM && (
|
|
75
64
|
// Safe to use `ExecutionEnvironment`, because `Head` is purely
|
|
@@ -77,7 +66,6 @@ function BaseUrlIssueBanner() {
|
|
|
77
66
|
<Head>
|
|
78
67
|
<script>{createInlineScript(baseUrl)}</script>
|
|
79
68
|
</Head>)}
|
|
80
|
-
<div id={BannerContainerId}/>
|
|
81
69
|
</>);
|
|
82
70
|
}
|
|
83
71
|
/**
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* This source code is licensed under the MIT license found in the
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
|
-
import { useLayoutEffect } from 'react';
|
|
8
7
|
import clientModules from '@generated/client-modules';
|
|
8
|
+
import useIsomorphicLayoutEffect from './exports/useIsomorphicLayoutEffect';
|
|
9
9
|
export function dispatchLifecycleAction(lifecycleAction, ...args) {
|
|
10
10
|
const callbacks = clientModules.map((clientModule) => {
|
|
11
11
|
const lifecycleFunction = (clientModule.default?.[lifecycleAction] ??
|
|
@@ -36,7 +36,7 @@ function scrollAfterNavigation({ location, previousLocation, }) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
function ClientLifecyclesDispatcher({ children, location, previousLocation, }) {
|
|
39
|
-
|
|
39
|
+
useIsomorphicLayoutEffect(() => {
|
|
40
40
|
if (previousLocation !== location) {
|
|
41
41
|
scrollAfterNavigation({ location, previousLocation });
|
|
42
42
|
dispatchLifecycleAction('onRouteDidUpdate', { previousLocation, location });
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
7
|
import { type ReactNode } from 'react';
|
|
8
|
-
|
|
8
|
+
type LinksCollector = {
|
|
9
9
|
collectLink: (link: string) => void;
|
|
10
10
|
};
|
|
11
|
-
|
|
11
|
+
type StatefulLinksCollector = LinksCollector & {
|
|
12
12
|
getCollectedLinks: () => string[];
|
|
13
13
|
};
|
|
14
14
|
export declare const createStatefulLinksCollector: () => StatefulLinksCollector;
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import React from 'react';
|
|
8
8
|
import type { Location } from 'history';
|
|
9
|
-
|
|
9
|
+
type Props = {
|
|
10
10
|
readonly location: Location;
|
|
11
11
|
readonly children: JSX.Element;
|
|
12
12
|
};
|
|
13
|
-
|
|
13
|
+
type State = {
|
|
14
14
|
nextRouteHasLoaded: boolean;
|
|
15
15
|
};
|
|
16
16
|
declare class PendingNavigation extends React.Component<Props, State> {
|
|
@@ -5,29 +5,43 @@
|
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
7
|
import React from 'react';
|
|
8
|
-
import ReactDOM from 'react-dom';
|
|
8
|
+
import ReactDOM from 'react-dom/client';
|
|
9
9
|
import { BrowserRouter } from 'react-router-dom';
|
|
10
10
|
import { HelmetProvider } from 'react-helmet-async';
|
|
11
11
|
import ExecutionEnvironment from './exports/ExecutionEnvironment';
|
|
12
12
|
import App from './App';
|
|
13
13
|
import preload from './preload';
|
|
14
14
|
import docusaurus from './docusaurus';
|
|
15
|
+
const hydrate = Boolean(process.env.HYDRATE_CLIENT_ENTRY);
|
|
15
16
|
// Client-side render (e.g: running in browser) to become single-page
|
|
16
17
|
// application (SPA).
|
|
17
18
|
if (ExecutionEnvironment.canUseDOM) {
|
|
18
19
|
window.docusaurus = docusaurus;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
const container = document.getElementById('__docusaurus');
|
|
21
|
+
const app = (<HelmetProvider>
|
|
22
|
+
<BrowserRouter>
|
|
23
|
+
<App />
|
|
24
|
+
</BrowserRouter>
|
|
25
|
+
</HelmetProvider>);
|
|
26
|
+
const onRecoverableError = (error) => {
|
|
27
|
+
console.error('Docusaurus React Root onRecoverableError:', error);
|
|
28
|
+
};
|
|
29
|
+
const renderApp = () => {
|
|
30
|
+
if (hydrate) {
|
|
31
|
+
React.startTransition(() => {
|
|
32
|
+
ReactDOM.hydrateRoot(container, app, {
|
|
33
|
+
onRecoverableError,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const root = ReactDOM.createRoot(container, { onRecoverableError });
|
|
39
|
+
React.startTransition(() => {
|
|
40
|
+
root.render(app);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
preload(window.location.pathname).then(renderApp);
|
|
31
45
|
// Webpack Hot Module Replacement API
|
|
32
46
|
if (module.hot) {
|
|
33
47
|
// Self-accepting method/ trick
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
/// <reference types="@docusaurus/module-type-aliases" />
|
|
8
8
|
import React, { type ReactNode } from 'react';
|
|
9
9
|
import type { Props } from '@docusaurus/ErrorBoundary';
|
|
10
|
-
|
|
10
|
+
type State = {
|
|
11
11
|
error: Error | null;
|
|
12
12
|
};
|
|
13
13
|
export default class ErrorBoundary extends React.Component<Props, State> {
|
|
@@ -7,5 +7,5 @@
|
|
|
7
7
|
/// <reference types="@docusaurus/module-type-aliases" />
|
|
8
8
|
import React from 'react';
|
|
9
9
|
import type { Props } from '@docusaurus/Link';
|
|
10
|
-
declare const _default: React.ForwardRefExoticComponent<
|
|
10
|
+
declare const _default: React.ForwardRefExoticComponent<Omit<Props, "ref"> & React.RefAttributes<HTMLAnchorElement>>;
|
|
11
11
|
export default _default;
|
|
@@ -103,7 +103,7 @@ function Link({ isNavLink, to, href, activeClassName, isActive, 'data-noBrokenLi
|
|
|
103
103
|
linksCollector.collectLink(targetLink);
|
|
104
104
|
}
|
|
105
105
|
return isRegularHtmlLink ? (
|
|
106
|
-
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
|
106
|
+
// eslint-disable-next-line jsx-a11y/anchor-has-content, @docusaurus/no-html-links
|
|
107
107
|
<a ref={innerRef} href={targetLink} {...(targetLinkUnprefixed &&
|
|
108
108
|
!isInternal && { target: '_blank', rel: 'noopener noreferrer' })} {...props}/>) : (<LinkComponent {...props} onMouseEnter={onInteractionEnter} onTouchStart={onInteractionEnter} innerRef={handleRef} to={targetLink}
|
|
109
109
|
// Avoid "React does not recognize the `activeClassName` prop on a DOM
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import { useEffect } from 'react';
|
|
8
|
+
/**
|
|
9
|
+
* This hook is like `useLayoutEffect`, but without the SSR warning.
|
|
10
|
+
* It seems hacky but it's used in many React libs (Redux, Formik...).
|
|
11
|
+
* Also mentioned here: https://github.com/facebook/react/issues/16956
|
|
12
|
+
*
|
|
13
|
+
* It is useful when you need to update a ref as soon as possible after a React
|
|
14
|
+
* render (before `useEffect`).
|
|
15
|
+
*
|
|
16
|
+
* TODO should become unnecessary in React v19?
|
|
17
|
+
* https://github.com/facebook/react/pull/26395
|
|
18
|
+
* This was added in core with Docusaurus v3 but kept undocumented on purpose
|
|
19
|
+
*/
|
|
20
|
+
declare const useIsomorphicLayoutEffect: typeof useEffect;
|
|
21
|
+
export default useIsomorphicLayoutEffect;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import { useEffect, useLayoutEffect } from 'react';
|
|
8
|
+
import ExecutionEnvironment from './ExecutionEnvironment';
|
|
9
|
+
/**
|
|
10
|
+
* This hook is like `useLayoutEffect`, but without the SSR warning.
|
|
11
|
+
* It seems hacky but it's used in many React libs (Redux, Formik...).
|
|
12
|
+
* Also mentioned here: https://github.com/facebook/react/issues/16956
|
|
13
|
+
*
|
|
14
|
+
* It is useful when you need to update a ref as soon as possible after a React
|
|
15
|
+
* render (before `useEffect`).
|
|
16
|
+
*
|
|
17
|
+
* TODO should become unnecessary in React v19?
|
|
18
|
+
* https://github.com/facebook/react/pull/26395
|
|
19
|
+
* This was added in core with Docusaurus v3 but kept undocumented on purpose
|
|
20
|
+
*/
|
|
21
|
+
const useIsomorphicLayoutEffect = ExecutionEnvironment.canUseDOM
|
|
22
|
+
? useLayoutEffect
|
|
23
|
+
: useEffect;
|
|
24
|
+
export default useIsomorphicLayoutEffect;
|
|
@@ -4,8 +4,5 @@
|
|
|
4
4
|
* This source code is licensed under the MIT license found in the
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export default class LogPlugin extends WebpackBar {
|
|
10
|
-
apply(compiler: Compiler): void;
|
|
11
|
-
}
|
|
7
|
+
/// <reference types="react" />
|
|
8
|
+
export default function HasHydratedDataAttribute(): JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import Head from '@docusaurus/Head';
|
|
9
|
+
import useIsBrowser from '@docusaurus/useIsBrowser';
|
|
10
|
+
// See https://github.com/facebook/docusaurus/pull/9256
|
|
11
|
+
// Docusaurus adds a <html data-has-hydrated="true"> after hydration
|
|
12
|
+
export default function HasHydratedDataAttribute() {
|
|
13
|
+
const isBrowser = useIsBrowser();
|
|
14
|
+
return (<Head>
|
|
15
|
+
<html data-has-hydrated={isBrowser}/>
|
|
16
|
+
</Head>);
|
|
17
|
+
}
|
|
@@ -9,14 +9,13 @@ import path from 'path';
|
|
|
9
9
|
import fs from 'fs-extra';
|
|
10
10
|
// eslint-disable-next-line no-restricted-imports
|
|
11
11
|
import _ from 'lodash';
|
|
12
|
-
import chalk from 'chalk';
|
|
13
12
|
import * as eta from 'eta';
|
|
14
13
|
import { StaticRouter } from 'react-router-dom';
|
|
15
|
-
import ReactDOMServer from 'react-dom/server';
|
|
16
14
|
import { HelmetProvider } from 'react-helmet-async';
|
|
17
15
|
import { getBundles } from 'react-loadable-ssr-addon-v5-slorber';
|
|
18
16
|
import Loadable from 'react-loadable';
|
|
19
17
|
import { minify } from 'html-minifier-terser';
|
|
18
|
+
import { renderStaticApp } from './serverRenderer';
|
|
20
19
|
import preload from './preload';
|
|
21
20
|
import App from './App';
|
|
22
21
|
import { createStatefulLinksCollector, LinksCollectorProvider, } from './LinksCollector';
|
|
@@ -27,24 +26,31 @@ function renderSSRTemplate(ssrTemplate, data) {
|
|
|
27
26
|
const compiled = getCompiledSSRTemplate(ssrTemplate);
|
|
28
27
|
return compiled(data, eta.defaultConfig);
|
|
29
28
|
}
|
|
29
|
+
function buildSSRErrorMessage({ error, pathname, }) {
|
|
30
|
+
const parts = [
|
|
31
|
+
`Docusaurus server-side rendering could not render static page with path ${pathname} because of error: ${error.message}`,
|
|
32
|
+
];
|
|
33
|
+
const isNotDefinedErrorRegex = /(?:window|document|localStorage|navigator|alert|location|buffer|self) is not defined/i;
|
|
34
|
+
if (isNotDefinedErrorRegex.test(error.message)) {
|
|
35
|
+
// prettier-ignore
|
|
36
|
+
parts.push(`It looks like you are using code that should run on the client-side only.
|
|
37
|
+
To get around it, try using \`<BrowserOnly>\` (https://docusaurus.io/docs/docusaurus-core/#browseronly) or \`ExecutionEnvironment\` (https://docusaurus.io/docs/docusaurus-core/#executionenvironment).
|
|
38
|
+
It might also require to wrap your client code in \`useEffect\` hook and/or import a third-party library dynamically (if any).`);
|
|
39
|
+
}
|
|
40
|
+
return parts.join('\n');
|
|
41
|
+
}
|
|
30
42
|
export default async function render(locals) {
|
|
31
43
|
try {
|
|
32
44
|
return await doRender(locals);
|
|
33
45
|
}
|
|
34
|
-
catch (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// prettier-ignore
|
|
43
|
-
console.info(`${chalk.cyan.bold('[INFO]')} It looks like you are using code that should run on the client-side only.
|
|
44
|
-
To get around it, try using ${chalk.cyan('`<BrowserOnly>`')} (${chalk.cyan.underline('https://docusaurus.io/docs/docusaurus-core/#browseronly')}) or ${chalk.cyan('`ExecutionEnvironment`')} (${chalk.cyan.underline('https://docusaurus.io/docs/docusaurus-core/#executionenvironment')}).
|
|
45
|
-
It might also require to wrap your client code in ${chalk.cyan('`useEffect`')} hook and/or import a third-party library dynamically (if any).`);
|
|
46
|
-
}
|
|
47
|
-
throw err;
|
|
46
|
+
catch (errorUnknown) {
|
|
47
|
+
const error = errorUnknown;
|
|
48
|
+
const message = buildSSRErrorMessage({ error, pathname: locals.path });
|
|
49
|
+
const ssrError = new Error(message, { cause: error });
|
|
50
|
+
// It is important to log the error here because the stacktrace causal chain
|
|
51
|
+
// is not available anymore upper in the tree (this SSR runs in eval)
|
|
52
|
+
console.error(ssrError);
|
|
53
|
+
throw ssrError;
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
56
|
// Renderer for static-site-generator-webpack-plugin (async rendering).
|
|
@@ -56,7 +62,7 @@ async function doRender(locals) {
|
|
|
56
62
|
const routerContext = {};
|
|
57
63
|
const helmetContext = {};
|
|
58
64
|
const linksCollector = createStatefulLinksCollector();
|
|
59
|
-
const
|
|
65
|
+
const app = (
|
|
60
66
|
// @ts-expect-error: we are migrating away from react-loadable anyways
|
|
61
67
|
<Loadable.Capture report={(moduleName) => modules.add(moduleName)}>
|
|
62
68
|
<HelmetProvider context={helmetContext}>
|
|
@@ -67,6 +73,7 @@ async function doRender(locals) {
|
|
|
67
73
|
</StaticRouter>
|
|
68
74
|
</HelmetProvider>
|
|
69
75
|
</Loadable.Capture>);
|
|
76
|
+
const appHtml = await renderStaticApp(app);
|
|
70
77
|
onLinksCollected(location, linksCollector.getCollectedLinks());
|
|
71
78
|
const { helmet } = helmetContext;
|
|
72
79
|
const htmlAttributes = helmet.htmlAttributes.toString();
|
|
@@ -124,7 +131,8 @@ async function doRender(locals) {
|
|
|
124
131
|
}
|
|
125
132
|
catch (err) {
|
|
126
133
|
// prettier-ignore
|
|
127
|
-
console.error(
|
|
134
|
+
console.error(`Minification of page ${locals.path} failed.`);
|
|
135
|
+
console.error(err);
|
|
128
136
|
throw err;
|
|
129
137
|
}
|
|
130
138
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import type { ReactNode } from 'react';
|
|
8
|
+
export declare function renderStaticApp(app: ReactNode): Promise<string>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import { renderToPipeableStream } from 'react-dom/server';
|
|
8
|
+
import { Writable } from 'stream';
|
|
9
|
+
export async function renderStaticApp(app) {
|
|
10
|
+
// Inspired from
|
|
11
|
+
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
|
|
12
|
+
// https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/static-entry.js
|
|
13
|
+
const writableStream = new WritableAsPromise();
|
|
14
|
+
const { pipe } = renderToPipeableStream(app, {
|
|
15
|
+
onError(error) {
|
|
16
|
+
writableStream.destroy(error);
|
|
17
|
+
},
|
|
18
|
+
onAllReady() {
|
|
19
|
+
pipe(writableStream);
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
return writableStream.getPromise();
|
|
23
|
+
}
|
|
24
|
+
// WritableAsPromise inspired by https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/server-utils/writable-as-promise.js
|
|
25
|
+
/* eslint-disable no-underscore-dangle */
|
|
26
|
+
class WritableAsPromise extends Writable {
|
|
27
|
+
_output;
|
|
28
|
+
_deferred;
|
|
29
|
+
constructor() {
|
|
30
|
+
super();
|
|
31
|
+
this._output = ``;
|
|
32
|
+
this._deferred = {
|
|
33
|
+
promise: null,
|
|
34
|
+
resolve: () => null,
|
|
35
|
+
reject: () => null,
|
|
36
|
+
};
|
|
37
|
+
this._deferred.promise = new Promise((resolve, reject) => {
|
|
38
|
+
this._deferred.resolve = resolve;
|
|
39
|
+
this._deferred.reject = reject;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
_write(chunk, _enc, next) {
|
|
43
|
+
this._output += chunk.toString();
|
|
44
|
+
next();
|
|
45
|
+
}
|
|
46
|
+
_destroy(error, next) {
|
|
47
|
+
if (error instanceof Error) {
|
|
48
|
+
this._deferred.reject(error);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
next();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
end() {
|
|
55
|
+
this._deferred.resolve(this._output);
|
|
56
|
+
return this.destroy();
|
|
57
|
+
}
|
|
58
|
+
getPromise() {
|
|
59
|
+
return this._deferred.promise;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/lib/commands/build.d.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
7
|
import { type LoadContextOptions } from '../server';
|
|
8
|
-
export
|
|
8
|
+
export type BuildCLIOptions = Pick<LoadContextOptions, 'config' | 'locale' | 'outDir'> & {
|
|
9
9
|
bundleAnalyzer?: boolean;
|
|
10
10
|
minify?: boolean;
|
|
11
|
+
dev?: boolean;
|
|
11
12
|
};
|
|
12
13
|
export declare function build(siteDirParam?: string, cliOptions?: Partial<BuildCLIOptions>, forceTerminate?: boolean): Promise<string>;
|
package/lib/commands/build.js
CHANGED
|
@@ -32,6 +32,11 @@ forceTerminate = true) {
|
|
|
32
32
|
process.env.BABEL_ENV = 'production';
|
|
33
33
|
process.env.NODE_ENV = 'production';
|
|
34
34
|
process.env.DOCUSAURUS_CURRENT_LOCALE = cliOptions.locale;
|
|
35
|
+
if (cliOptions.dev) {
|
|
36
|
+
logger_1.default.info `Building in dev mode`;
|
|
37
|
+
process.env.BABEL_ENV = 'development';
|
|
38
|
+
process.env.NODE_ENV = 'development';
|
|
39
|
+
}
|
|
35
40
|
const siteDir = await fs_extra_1.default.realpath(siteDirParam);
|
|
36
41
|
['SIGINT', 'SIGTERM'].forEach((sig) => {
|
|
37
42
|
process.on(sig, () => process.exit());
|
|
@@ -47,8 +52,9 @@ forceTerminate = true) {
|
|
|
47
52
|
});
|
|
48
53
|
}
|
|
49
54
|
catch (err) {
|
|
50
|
-
logger_1.default.
|
|
51
|
-
|
|
55
|
+
throw new Error(logger_1.default.interpolate `Unable to build website for locale name=${locale}.`, {
|
|
56
|
+
cause: err,
|
|
57
|
+
});
|
|
52
58
|
}
|
|
53
59
|
}
|
|
54
60
|
const context = await (0, server_1.loadContext)({
|
|
@@ -96,7 +102,7 @@ async function buildLocale({ siteDir, locale, cliOptions, forceTerminate, isLast
|
|
|
96
102
|
// Apply user webpack config.
|
|
97
103
|
const { outDir, generatedFilesDir, plugins, siteConfig: { baseUrl, onBrokenLinks, staticDirectories: staticDirectoriesOption, }, routes, } = props;
|
|
98
104
|
const clientManifestPath = path_1.default.join(generatedFilesDir, 'client-manifest.json');
|
|
99
|
-
let clientConfig = (0, webpack_merge_1.default)(await (0, client_1.default)(props, cliOptions.minify), {
|
|
105
|
+
let clientConfig = (0, webpack_merge_1.default)(await (0, client_1.default)(props, cliOptions.minify, true), {
|
|
100
106
|
plugins: [
|
|
101
107
|
// Remove/clean build folders before building bundles.
|
|
102
108
|
new CleanWebpackPlugin_1.default({ verbose: false }),
|