@docusaurus/core 2.4.0 → 3.0.0-alpha.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.
Files changed (50) hide show
  1. package/bin/beforeCli.mjs +31 -5
  2. package/bin/docusaurus.mjs +9 -1
  3. package/lib/babel/preset.js +6 -1
  4. package/lib/client/BaseUrlIssueBanner/index.d.ts +0 -7
  5. package/lib/client/BaseUrlIssueBanner/index.js +12 -22
  6. package/lib/client/BaseUrlIssueBanner/styles.module.css +1 -1
  7. package/lib/client/ClientLifecyclesDispatcher.js +2 -2
  8. package/lib/client/LinksCollector.d.ts +2 -2
  9. package/lib/client/PendingNavigation.d.ts +2 -2
  10. package/lib/client/clientEntry.js +27 -13
  11. package/lib/client/exports/ErrorBoundary.d.ts +1 -1
  12. package/lib/client/exports/Link.d.ts +1 -1
  13. package/lib/client/exports/Link.js +1 -1
  14. package/lib/client/exports/useIsomorphicLayoutEffect.d.ts +21 -0
  15. package/lib/client/exports/useIsomorphicLayoutEffect.js +24 -0
  16. package/lib/client/serverEntry.js +26 -18
  17. package/lib/{webpack/plugins/LogPlugin.d.ts → client/serverRenderer.d.ts} +2 -5
  18. package/lib/client/serverRenderer.js +61 -0
  19. package/lib/commands/build.d.ts +2 -1
  20. package/lib/commands/build.js +9 -3
  21. package/lib/commands/deploy.d.ts +1 -1
  22. package/lib/commands/serve.d.ts +1 -1
  23. package/lib/commands/start.d.ts +1 -1
  24. package/lib/commands/start.js +15 -5
  25. package/lib/commands/swizzle/actions.d.ts +2 -2
  26. package/lib/commands/swizzle/common.d.ts +3 -3
  27. package/lib/commands/swizzle/components.d.ts +1 -1
  28. package/lib/commands/writeTranslations.d.ts +1 -1
  29. package/lib/server/configValidation.js +19 -1
  30. package/lib/server/getHostPort.d.ts +1 -1
  31. package/lib/server/index.d.ts +1 -1
  32. package/lib/server/plugins/configs.d.ts +2 -2
  33. package/lib/server/plugins/routeConfig.js +3 -1
  34. package/lib/server/plugins/synthetic.js +1 -2
  35. package/lib/server/routes.d.ts +1 -1
  36. package/lib/server/routes.js +2 -18
  37. package/lib/server/translations/translations.d.ts +2 -2
  38. package/lib/server/translations/translationsExtractor.d.ts +1 -1
  39. package/lib/webpack/aliases/index.d.ts +1 -1
  40. package/lib/webpack/client.d.ts +1 -1
  41. package/lib/webpack/client.js +10 -4
  42. package/lib/webpack/plugins/CleanWebpackPlugin.d.ts +1 -1
  43. package/lib/webpack/plugins/WaitPlugin.d.ts +1 -1
  44. package/lib/webpack/server.js +2 -2
  45. package/lib/webpack/templates/ssr.html.template.d.ts +1 -1
  46. package/lib/webpack/templates/ssr.html.template.js +2 -7
  47. package/lib/webpack/utils.d.ts +3 -1
  48. package/lib/webpack/utils.js +26 -8
  49. package/package.json +51 -51
  50. 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';
@@ -104,10 +105,35 @@ export default async function beforeCli() {
104
105
  .filter((p) => p.startsWith('@docusaurus'))
105
106
  .map((p) => p.concat('@latest'))
106
107
  .join(' ');
107
- const isYarnUsed = await fs.pathExists(path.resolve('yarn.lock'));
108
- const upgradeCommand = isYarnUsed
109
- ? `yarn upgrade ${siteDocusaurusPackagesForUpdate}`
110
- : `npm i ${siteDocusaurusPackagesForUpdate}`;
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
+
128
+ const getUpgradeCommand = async () => {
129
+ const yarnVersion = await getYarnVersion();
130
+ if (!yarnVersion) {
131
+ return `npm i ${siteDocusaurusPackagesForUpdate}`;
132
+ }
133
+ return yarnVersion >= 2
134
+ ? `yarn up ${siteDocusaurusPackagesForUpdate}`
135
+ : `yarn upgrade ${siteDocusaurusPackagesForUpdate}`;
136
+ };
111
137
 
112
138
  /** @type {import('boxen').Options} */
113
139
  const boxenOptions = {
@@ -124,7 +150,7 @@ export default async function beforeCli() {
124
150
  )} → ${logger.green(`${notifier.update.latest}`)}
125
151
 
126
152
  To upgrade Docusaurus packages with the latest version, run the following command:
127
- ${logger.code(upgradeCommand)}`,
153
+ ${logger.code(await getUpgradeCommand())}`,
128
154
  boxenOptions,
129
155
  );
130
156
 
@@ -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
- logger.error(err instanceof Error ? err.stack : err);
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);
@@ -36,7 +36,12 @@ function getTransformOptions(isServer) {
36
36
  exclude: ['transform-typeof-symbol'],
37
37
  },
38
38
  ],
39
- require.resolve('@babel/preset-react'),
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: [
@@ -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,17 +4,18 @@
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, { useLayoutEffect } from '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';
11
11
  import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
12
12
  // Double-security: critical CSS will hide the banner if CSS can load!
13
13
  import './styles.module.css';
14
- const BannerContainerId = 'docusaurus-base-url-issue-banner-container';
15
- const BannerId = 'docusaurus-base-url-issue-banner';
16
- const SuggestionContainerId = 'docusaurus-base-url-issue-banner-suggestion-container';
17
- const InsertBannerWindowAttribute = '__DOCUSAURUS_INSERT_BASEURL_BANNER';
14
+ // __ prefix allows search crawlers (Algolia/DocSearch) to ignore anchors
15
+ // https://github.com/facebook/docusaurus/issues/8883#issuecomment-1516328368
16
+ const BannerContainerId = '__docusaurus-base-url-issue-banner-container';
17
+ const BannerId = '__docusaurus-base-url-issue-banner';
18
+ const SuggestionContainerId = '__docusaurus-base-url-issue-banner-suggestion-container';
18
19
  // It is important to not use React to render this banner
19
20
  // otherwise Google would index it, even if it's hidden with some critical CSS!
20
21
  // See https://github.com/facebook/docusaurus/issues/4028
@@ -34,24 +35,19 @@ function createInlineHtmlBanner(baseUrl) {
34
35
  function createInlineScript(baseUrl) {
35
36
  /* language=js */
36
37
  return `
37
- window['${InsertBannerWindowAttribute}'] = true;
38
-
39
- document.addEventListener('DOMContentLoaded', maybeInsertBanner);
40
-
41
- function maybeInsertBanner() {
42
- var shouldInsert = window['${InsertBannerWindowAttribute}'];
38
+ document.addEventListener('DOMContentLoaded', function maybeInsertBanner() {
39
+ var shouldInsert = typeof window['docusaurus'] === 'undefined';
43
40
  shouldInsert && insertBanner();
44
- }
41
+ });
45
42
 
46
43
  function insertBanner() {
47
- var bannerContainer = document.getElementById('${BannerContainerId}');
48
- if (!bannerContainer) {
49
- return;
50
- }
44
+ var bannerContainer = document.createElement('div');
45
+ bannerContainer.id = '${BannerContainerId}';
51
46
  var bannerHtml = ${JSON.stringify(createInlineHtmlBanner(baseUrl))
52
47
  // See https://redux.js.org/recipes/server-rendering/#security-considerations
53
48
  .replace(/</g, '\\\u003c')};
54
49
  bannerContainer.innerHTML = bannerHtml;
50
+ document.body.prepend(bannerContainer);
55
51
  var suggestionContainer = document.getElementById('${SuggestionContainerId}');
56
52
  var actualHomePagePath = window.location.pathname;
57
53
  var suggestedBaseUrl = actualHomePagePath.substr(-1) === '/'
@@ -63,11 +59,6 @@ function insertBanner() {
63
59
  }
64
60
  function BaseUrlIssueBanner() {
65
61
  const { siteConfig: { baseUrl }, } = useDocusaurusContext();
66
- // useLayoutEffect fires before DOMContentLoaded.
67
- // It gives the opportunity to avoid inserting the banner in the first place
68
- useLayoutEffect(() => {
69
- window[InsertBannerWindowAttribute] = false;
70
- }, []);
71
62
  return (<>
72
63
  {!ExecutionEnvironment.canUseDOM && (
73
64
  // Safe to use `ExecutionEnvironment`, because `Head` is purely
@@ -75,7 +66,6 @@ function BaseUrlIssueBanner() {
75
66
  <Head>
76
67
  <script>{createInlineScript(baseUrl)}</script>
77
68
  </Head>)}
78
- <div id={BannerContainerId}/>
79
69
  </>);
80
70
  }
81
71
  /**
@@ -5,6 +5,6 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- :global(#docusaurus-base-url-issue-banner-container) {
8
+ :global(#__docusaurus-base-url-issue-banner-container) {
9
9
  display: none;
10
10
  }
@@ -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
- useLayoutEffect(() => {
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
- declare type LinksCollector = {
8
+ type LinksCollector = {
9
9
  collectLink: (link: string) => void;
10
10
  };
11
- declare type StatefulLinksCollector = LinksCollector & {
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
- declare type Props = {
9
+ type Props = {
10
10
  readonly location: Location;
11
11
  readonly children: JSX.Element;
12
12
  };
13
- declare type State = {
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
- // For production, attempt to hydrate existing markup for performant
20
- // first-load experience.
21
- // For development, there is no existing markup so we had to render it.
22
- // We also preload async component to avoid first-load loading screen.
23
- const renderMethod = process.env.NODE_ENV === 'production' ? ReactDOM.hydrate : ReactDOM.render;
24
- preload(window.location.pathname).then(() => {
25
- renderMethod(<HelmetProvider>
26
- <BrowserRouter>
27
- <App />
28
- </BrowserRouter>
29
- </HelmetProvider>, document.getElementById('__docusaurus'));
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
- declare type State = {
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<Pick<Props, "location" | "children" | "replace" | "slot" | "style" | "title" | "component" | "exact" | "sensitive" | "strict" | "type" | "key" | "id" | "lang" | "dir" | "property" | "rel" | "href" | "isNavLink" | "className" | "to" | "innerRef" | "download" | "hrefLang" | "media" | "ping" | "target" | "referrerPolicy" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "contentEditable" | "contextMenu" | "draggable" | "hidden" | "placeholder" | "spellCheck" | "tabIndex" | "translate" | "radioGroup" | "role" | "about" | "datatype" | "inlist" | "prefix" | "resource" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "color" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "inputMode" | "is" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "activeClassName" | "activeStyle" | "isActive" | "data-noBrokenLinkCheck" | "autoAddBaseUrl"> & React.RefAttributes<HTMLAnchorElement>>;
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;
@@ -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 (err) {
35
- // We are not using logger in this file, because it seems to fail with some
36
- // compilers / some polyfill methods. This is very likely a bug, but in the
37
- // long term, when we output native ES modules in SSR, the bug will be gone.
38
- // prettier-ignore
39
- console.error(chalk.red(`${chalk.bold('[ERROR]')} Docusaurus server-side rendering could not render static page with path ${chalk.cyan.underline(locals.path)}.`));
40
- const isNotDefinedErrorRegex = /(?:window|document|localStorage|navigator|alert|location|buffer|self) is not defined/i;
41
- if (isNotDefinedErrorRegex.test(err.message)) {
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 appHtml = ReactDOMServer.renderToString(
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(chalk.red(`${chalk.bold('[ERROR]')} Minification of page ${chalk.cyan.underline(locals.path)} failed.`));
134
+ console.error(`Minification of page ${locals.path} failed.`);
135
+ console.error(err);
128
136
  throw err;
129
137
  }
130
138
  }
@@ -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
- import WebpackBar from 'webpackbar';
8
- import type { Compiler } from 'webpack';
9
- export default class LogPlugin extends WebpackBar {
10
- apply(compiler: Compiler): void;
11
- }
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
+ }
@@ -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 declare type BuildCLIOptions = Pick<LoadContextOptions, 'config' | 'locale' | 'outDir'> & {
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>;
@@ -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.error `Unable to build website for locale name=${locale}.`;
51
- throw err;
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 }),
@@ -5,7 +5,7 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
  import { type LoadContextOptions } from '../server';
8
- export declare type DeployCLIOptions = Pick<LoadContextOptions, 'config' | 'locale' | 'outDir'> & {
8
+ export type DeployCLIOptions = Pick<LoadContextOptions, 'config' | 'locale' | 'outDir'> & {
9
9
  skipBuild?: boolean;
10
10
  };
11
11
  export declare function deploy(siteDirParam?: string, cliOptions?: Partial<DeployCLIOptions>): Promise<void>;
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { type HostPortOptions } from '../server/getHostPort';
8
8
  import type { LoadContextOptions } from '../server';
9
- export declare type ServeCLIOptions = HostPortOptions & Pick<LoadContextOptions, 'config'> & {
9
+ export type ServeCLIOptions = HostPortOptions & Pick<LoadContextOptions, 'config'> & {
10
10
  dir?: string;
11
11
  build?: boolean;
12
12
  open?: boolean;
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { type LoadContextOptions } from '../server';
8
8
  import { type HostPortOptions } from '../server/getHostPort';
9
- export declare type StartCLIOptions = HostPortOptions & Pick<LoadContextOptions, 'locale' | 'config'> & {
9
+ export type StartCLIOptions = HostPortOptions & Pick<LoadContextOptions, 'locale' | 'config'> & {
10
10
  hotOnly?: boolean;
11
11
  open?: boolean;
12
12
  poll?: boolean | number;