Skip to content

Commit

Permalink
feat(core): rework client modules lifecycles, officially make API pub…
Browse files Browse the repository at this point in the history
…lic (#6732)
  • Loading branch information
Josh-Cena committed Apr 29, 2022
1 parent 2429bfb commit ae788c5
Show file tree
Hide file tree
Showing 16 changed files with 259 additions and 126 deletions.
20 changes: 9 additions & 11 deletions packages/docusaurus-plugin-google-analytics/src/analytics.ts
Expand Up @@ -5,21 +5,19 @@
* LICENSE file in the root directory of this source tree.
*/

import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import type {ClientModule} from '@docusaurus/types';

export default (function analyticsModule() {
if (!ExecutionEnvironment.canUseDOM) {
return null;
}

return {
onRouteUpdate({location}: {location: Location}) {
const clientModule: ClientModule = {
onRouteDidUpdate({location, previousLocation}) {
if (previousLocation && location.pathname !== previousLocation.pathname) {
// Set page so that subsequent hits on this page are attributed
// to this page. This is recommended for Single-page Applications.
window.ga('set', 'page', location.pathname);
// Always refer to the variable on window in-case it gets
// overridden elsewhere.
window.ga('send', 'pageview');
},
};
})();
}
},
};

export default clientModule;
26 changes: 12 additions & 14 deletions packages/docusaurus-plugin-google-gtag/src/gtag.ts
Expand Up @@ -5,20 +5,16 @@
* LICENSE file in the root directory of this source tree.
*/

import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import globalData from '@generated/globalData';
import type {PluginOptions} from '@docusaurus/plugin-google-gtag';
import type {ClientModule} from '@docusaurus/types';

export default (function gtagModule() {
if (!ExecutionEnvironment.canUseDOM) {
return null;
}
const {trackingID} = globalData['docusaurus-plugin-google-gtag']!
.default as PluginOptions;

const {trackingID} = globalData['docusaurus-plugin-google-gtag']!
.default as PluginOptions;

return {
onRouteUpdate({location}: {location: Location}) {
const clientModule: ClientModule = {
onRouteDidUpdate({location, previousLocation}) {
if (previousLocation && location.pathname !== previousLocation.pathname) {
// Always refer to the variable on window in case it gets overridden
// elsewhere.
window.gtag('config', trackingID, {
Expand All @@ -27,9 +23,11 @@ export default (function gtagModule() {
});
window.gtag('event', 'page_view', {
page_title: document.title,
page_location: location.href,
page_location: window.location.href,
page_path: location.pathname,
});
},
};
})();
}
},
};

export default clientModule;
2 changes: 2 additions & 0 deletions packages/docusaurus-theme-classic/package.json
Expand Up @@ -35,6 +35,7 @@
"copy-text-to-clipboard": "^3.0.1",
"infima": "0.2.0-alpha.38",
"lodash": "^4.17.21",
"nprogress": "^0.2.0",
"postcss": "^8.4.12",
"prism-react-renderer": "^1.3.1",
"prismjs": "^1.28.0",
Expand All @@ -48,6 +49,7 @@
"@docusaurus/module-type-aliases": "2.0.0-beta.18",
"@docusaurus/types": "2.0.0-beta.18",
"@types/mdx-js__react": "^1.5.5",
"@types/nprogress": "^0.2.0",
"@types/prismjs": "^1.26.0",
"@types/rtlcss": "^3.1.4",
"cross-env": "^7.0.3",
Expand Down
1 change: 1 addition & 0 deletions packages/docusaurus-theme-classic/src/index.ts
Expand Up @@ -138,6 +138,7 @@ export default function docusaurusThemeClassic(
require.resolve(getInfimaCSSFile(direction)),
'./prism-include-languages',
'./admonitions.css',
'./nprogress',
];

if (customCss) {
Expand Down
Expand Up @@ -11,12 +11,16 @@
* https://github.com/rstacruz/nprogress/blob/master/nprogress.css
*/

:root {
--docusaurus-progress-bar-color: var(--ifm-color-primary);
}

#nprogress {
pointer-events: none;
}

#nprogress .bar {
background: #29d;
background: var(--docusaurus-progress-bar-color);
position: fixed;
z-index: 1031;
top: 0;
Expand All @@ -30,7 +34,8 @@
right: 0;
width: 100px;
height: 100%;
box-shadow: 0 0 10px #29d, 0 0 5px #29d;
box-shadow: 0 0 10px var(--docusaurus-progress-bar-color),
0 0 5px var(--docusaurus-progress-bar-color);
opacity: 1;
transform: rotate(3deg) translate(0, -4px);
}
31 changes: 31 additions & 0 deletions packages/docusaurus-theme-classic/src/nprogress.ts
@@ -0,0 +1,31 @@
/**
* 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 nprogress from 'nprogress';
import './nprogress.css';
import type {ClientModule} from '@docusaurus/types';

nprogress.configure({showSpinner: false});

const delay = 200;

const clientModule: ClientModule = {
onRouteUpdate({location, previousLocation}) {
if (previousLocation && location.pathname !== previousLocation.pathname) {
const progressBarTimeout = window.setTimeout(() => {
nprogress.start();
}, delay);
return () => window.clearTimeout(progressBarTimeout);
}
return undefined;
},
onRouteDidUpdate() {
nprogress.done();
},
};

export default clientModule;
7 changes: 5 additions & 2 deletions packages/docusaurus-types/src/index.d.ts
Expand Up @@ -601,11 +601,14 @@ export type TOCItem = {
};

export type ClientModule = {
onRouteDidUpdate?: (args: {
previousLocation: Location | null;
location: Location;
}) => (() => void) | void;
onRouteUpdate?: (args: {
previousLocation: Location | null;
location: Location;
}) => void;
onRouteUpdateDelayed?: (args: {location: Location}) => void;
}) => (() => void) | void;
};

/** What the user configures. */
Expand Down
2 changes: 0 additions & 2 deletions packages/docusaurus/package.json
Expand Up @@ -77,7 +77,6 @@
"leven": "^3.1.0",
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.6.0",
"nprogress": "^0.2.0",
"postcss": "^8.4.12",
"postcss-loader": "^6.2.1",
"prompts": "^2.4.2",
Expand Down Expand Up @@ -108,7 +107,6 @@
"@docusaurus/module-type-aliases": "2.0.0-beta.18",
"@docusaurus/types": "2.0.0-beta.18",
"@types/detect-port": "^1.3.2",
"@types/nprogress": "^0.2.0",
"@types/react-dom": "^18.0.2",
"@types/react-router-config": "^5.0.6",
"@types/rtl-detect": "^1.0.0",
Expand Down
7 changes: 2 additions & 5 deletions packages/docusaurus/src/client/App.tsx
Expand Up @@ -6,6 +6,7 @@
*/

import React from 'react';
import '@generated/client-modules';

import routes from '@generated/routes';
import {useLocation} from '@docusaurus/router';
Expand All @@ -19,8 +20,6 @@ import SiteMetadataDefaults from './SiteMetadataDefaults';
import Root from '@theme/Root';
import SiteMetadata from '@theme/SiteMetadata';

import './clientLifecyclesDispatcher';

// TODO, quick fix for CSS insertion order
import ErrorBoundary from '@docusaurus/ErrorBoundary';
import Error from '@theme/Error';
Expand All @@ -36,9 +35,7 @@ export default function App(): JSX.Element {
<SiteMetadataDefaults />
<SiteMetadata />
<BaseUrlIssueBanner />
<PendingNavigation
location={normalizeLocation(location)}
delay={200}>
<PendingNavigation location={normalizeLocation(location)}>
{routeElement}
</PendingNavigation>
</Root>
Expand Down
55 changes: 55 additions & 0 deletions packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx
@@ -0,0 +1,55 @@
/**
* 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 {useLayoutEffect, type ReactElement} from 'react';
import clientModules from '@generated/client-modules';
import type {ClientModule} from '@docusaurus/types';
import type {Location} from 'history';

export function dispatchLifecycleAction<K extends keyof ClientModule>(
lifecycleAction: K,
...args: Parameters<NonNullable<ClientModule[K]>>
): () => void {
const callbacks = clientModules.map((clientModule) => {
const lifecycleFunction = (clientModule?.default?.[lifecycleAction] ??
clientModule[lifecycleAction]) as
| ((
...a: Parameters<NonNullable<ClientModule[K]>>
) => (() => void) | void)
| undefined;

return lifecycleFunction?.(...args);
});
return () => callbacks.forEach((cb) => cb?.());
}

function ClientLifecyclesDispatcher({
children,
location,
previousLocation,
}: {
children: ReactElement;
location: Location;
previousLocation: Location | null;
}): JSX.Element {
useLayoutEffect(() => {
if (previousLocation !== location) {
const {hash} = location;
if (!hash) {
window.scrollTo(0, 0);
} else {
const id = decodeURIComponent(hash.substring(1));
const element = document.getElementById(id);
element?.scrollIntoView();
}
dispatchLifecycleAction('onRouteDidUpdate', {previousLocation, location});
}
}, [previousLocation, location]);
return children;
}

export default ClientLifecyclesDispatcher;
71 changes: 24 additions & 47 deletions packages/docusaurus/src/client/PendingNavigation.tsx
Expand Up @@ -7,18 +7,14 @@

import React from 'react';
import {Route} from 'react-router-dom';
import nprogress from 'nprogress';

import clientLifecyclesDispatcher from './clientLifecyclesDispatcher';
import ClientLifecyclesDispatcher, {
dispatchLifecycleAction,
} from './ClientLifecyclesDispatcher';
import ExecutionEnvironment from './exports/ExecutionEnvironment';
import preload from './preload';
import type {Location} from 'history';

import './nprogress.css';

nprogress.configure({showSpinner: false});

type Props = {
readonly delay: number;
readonly location: Location;
readonly children: JSX.Element;
};
Expand All @@ -28,14 +24,19 @@ type State = {

class PendingNavigation extends React.Component<Props, State> {
private previousLocation: Location | null;
private progressBarTimeout: number | null;
private routeUpdateCleanupCb: () => void;

constructor(props: Props) {
super(props);

// previousLocation doesn't affect rendering, hence not stored in state.
this.previousLocation = null;
this.progressBarTimeout = null;
this.routeUpdateCleanupCb = ExecutionEnvironment.canUseDOM
? dispatchLifecycleAction('onRouteUpdate', {
previousLocation: null,
location: this.props.location,
})!
: () => {};
this.state = {
nextRouteHasLoaded: true,
};
Expand All @@ -56,56 +57,32 @@ class PendingNavigation extends React.Component<Props, State> {
// Save the location first.
this.previousLocation = this.props.location;
this.setState({nextRouteHasLoaded: false});
this.startProgressBar();
this.routeUpdateCleanupCb = dispatchLifecycleAction('onRouteUpdate', {
previousLocation: this.previousLocation,
location: nextLocation,
})!;

// Load data while the old screen remains.
preload(nextLocation.pathname)
.then(() => {
clientLifecyclesDispatcher.onRouteUpdate({
previousLocation: this.previousLocation,
location: nextLocation,
});
this.setState({nextRouteHasLoaded: true}, this.stopProgressBar);
const {hash} = nextLocation;
if (!hash) {
window.scrollTo(0, 0);
} else {
const id = decodeURIComponent(hash.substring(1));
const element = document.getElementById(id);
element?.scrollIntoView();
}
this.routeUpdateCleanupCb?.();
this.setState({nextRouteHasLoaded: true});
})
.catch((e) => console.warn(e));
return false;
}

private clearProgressBarTimeout() {
if (this.progressBarTimeout) {
window.clearTimeout(this.progressBarTimeout);
this.progressBarTimeout = null;
}
}

private startProgressBar() {
this.clearProgressBarTimeout();
this.progressBarTimeout = window.setTimeout(() => {
clientLifecyclesDispatcher.onRouteUpdateDelayed({
location: this.props.location,
});
nprogress.start();
}, this.props.delay);
}

private stopProgressBar() {
this.clearProgressBarTimeout();
nprogress.done();
}

override render(): JSX.Element {
const {children, location} = this.props;
// Use a controlled <Route> to trick all descendants into rendering the old
// location.
return <Route location={location} render={() => children} />;
return (
<ClientLifecyclesDispatcher
previousLocation={this.previousLocation}
location={location}>
<Route location={location} render={() => children} />
</ClientLifecyclesDispatcher>
);
}
}

Expand Down

0 comments on commit ae788c5

Please sign in to comment.