Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): rework client modules lifecycles, officially make API public #6732

Merged
merged 21 commits into from
Apr 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 9 additions & 11 deletions packages/docusaurus-plugin-google-analytics/src/analytics.ts
Original file line number Diff line number Diff line change
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) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check actually isn't necessary before either...

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export default function docusaurusThemeClassic(
require.resolve(getInfimaCSSFile(direction)),
'./prism-include-languages',
'./admonitions.css',
'./nprogress',
];

if (customCss) {
Expand Down
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -601,11 +601,14 @@ export type TOCItem = {
};

export type ClientModule = {
onRouteDidUpdate?: (args: {
Josh-Cena marked this conversation as resolved.
Show resolved Hide resolved
previousLocation: Location | null;
location: Location;
}) => (() => void) | void;
onRouteUpdate?: (args: {
previousLocation: Location | null;
location: Location;
}) => void;
onRouteUpdateDelayed?: (args: {location: Location}) => void;
Josh-Cena marked this conversation as resolved.
Show resolved Hide resolved
}) => (() => void) | void;
};

/** What the user configures. */
Expand Down
2 changes: 0 additions & 2 deletions packages/docusaurus/package.json
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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}>
Josh-Cena marked this conversation as resolved.
Show resolved Hide resolved
<PendingNavigation location={normalizeLocation(location)}>
{routeElement}
</PendingNavigation>
</Root>
Expand Down
55 changes: 55 additions & 0 deletions packages/docusaurus/src/client/ClientLifecyclesDispatcher.tsx
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Loading