Skip to content

Commit

Permalink
refactor: provider useDemoData which is given by getDemoById (#1876)
Browse files Browse the repository at this point in the history
* chore: all pass to lazy

* chore: match route

* chore: meta demos

* chore: support getDemoById

* chore: split code

* chore: back of it

* chore: getDemoById

* chore: support demo sus

* chore: all demos

* chore: demo extract

* chore: clean up

* chore: clean up

* chore: rm fallback
  • Loading branch information
zombieJ authored Sep 11, 2023
1 parent ed76ee6 commit 595a3d8
Show file tree
Hide file tree
Showing 13 changed files with 213 additions and 108 deletions.
10 changes: 0 additions & 10 deletions examples/normal/.dumi/pages/loader-test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,2 @@
import * as demo from '../../docs/index.md?type=demo';
import * as demoIndex from '../../docs/index.md?type=demo-index';
import * as frontmatter from '../../docs/index.md?type=frontmatter';
import * as text from '../../docs/index.md?type=text';

console.log('frontmatter', frontmatter);
console.log('demo', demo);
console.log('demoIndex', demoIndex);
console.log('text', text);

// Customize Page for dumi test
export default () => 'Customize Dumi Test Page';
1 change: 1 addition & 0 deletions examples/normal/src/Foo/demo/work.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => 'External Demo Block';
4 changes: 4 additions & 0 deletions examples/normal/src/Foo/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
```jsx
export default () => 'Hello Foo!';
```

你好,Foo!

<code src="./demo/work.tsx">示例框</code>
8 changes: 5 additions & 3 deletions src/client/pages/Demo/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useParams, useSiteData } from 'dumi';
import { useDemoData, useParams } from 'dumi';
import { createElement, type FC } from 'react';
import './index.less';

const DemoRenderPage: FC = () => {
const { id } = useParams();
const { demos } = useSiteData();
const { component } = demos[id!] || {};

const demoInfo = useDemoData(id!);

const { component } = demoInfo!;

return component && createElement(component);
};
Expand Down
82 changes: 0 additions & 82 deletions src/client/theme-api/DumiDemo.tsx

This file was deleted.

27 changes: 27 additions & 0 deletions src/client/theme-api/DumiDemo/DemoErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Container from 'dumi/theme/builtins/Container';
import React, { type FC, type ReactNode } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

const DemoErrorBoundary: FC<{ children: ReactNode }> = (props) => (
<ErrorBoundary
fallbackRender={({ error }: any) => (
<Container type="error">
<p>
<strong>{error.message || 'This demo has been crashed.'}</strong>
</p>
{error.stack && (
<p>
<details open>
<summary>Error stack</summary>
<pre>{error.stack}</pre>
</details>
</p>
)}
</Container>
)}
>
{props.children}
</ErrorBoundary>
);

export default DemoErrorBoundary;
66 changes: 66 additions & 0 deletions src/client/theme-api/DumiDemo/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { SP_ROUTE_PREFIX } from '@/constants';
import { useAppData, useDemoData, useSiteData } from 'dumi';
import React, { createElement, type FC } from 'react';
import type { IPreviewerProps } from '../types';

import Previewer from 'dumi/theme/builtins/Previewer';
import DemoErrorBoundary from './DemoErrorBoundary';

export interface IDumiDemoProps {
demo: {
id: string;
inline?: boolean;
};
previewerProps: Omit<IPreviewerProps, 'asset' | 'children'>;
}

const InternalDumiDemo = (props: IDumiDemoProps) => {
const { historyType } = useSiteData();
const { basename } = useAppData();
const demoInfo = useDemoData(props.demo.id)!;

// hide debug demo in production
if (process.env.NODE_ENV === 'production' && props.previewerProps.debug)
return null;

const { component, asset } = demoInfo;

const demoNode = (
<DemoErrorBoundary>{createElement(component)}</DemoErrorBoundary>
);

if (props.demo.inline) {
return demoNode;
}

const isHashRoute = historyType === 'hash';

return (
<Previewer
asset={asset}
demoUrl={
// allow user override demoUrl by frontmatter
props.previewerProps.demoUrl ||
// when use hash route, browser can automatically handle relative paths starting with #
`${isHashRoute ? `#` : ''}${basename}${SP_ROUTE_PREFIX}demos/${
props.demo.id
}`
}
{...props.previewerProps}
>
{props.previewerProps.iframe ? null : demoNode}
</Previewer>
);
};

export const DumiDemo: FC<IDumiDemoProps> = React.memo(
InternalDumiDemo,
(prev, next) => {
// compare length for performance
return JSON.stringify(prev).length === JSON.stringify(next).length;
},
);

if (process.env.NODE_ENV !== 'production') {
InternalDumiDemo.displayName = 'DumiDemo';
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import type { PICKED_PKG_FIELDS } from '@/constants';
import type { AtomComponentAsset } from 'dumi-assets-types';
import { createContext, useContext, type ComponentType } from 'react';
import type { ILocalesConfig, IPreviewerProps, IThemeConfig } from './types';
import type { ILocalesConfig, IPreviewerProps, IThemeConfig } from '../types';
import use from './use';

export type DemoInfo = {
component: ComponentType;
asset: IPreviewerProps['asset'];
routeId: string;
};

interface ISiteContext {
pkg: Partial<Record<keyof typeof PICKED_PKG_FIELDS, any>>;
historyType: 'browser' | 'hash' | 'memory';
entryExports: Record<string, any>;
demos: Record<
string,
{
component: ComponentType;
asset: IPreviewerProps['asset'];
routeId: string;
}
>;
demos: Record<string, DemoInfo>;
components: Record<string, AtomComponentAsset>;
locales: ILocalesConfig;
themeConfig: IThemeConfig;
Expand All @@ -25,6 +25,11 @@ interface ISiteContext {
* private field, do not use it in your code
*/
_2_level_nav_available: boolean;

// =================== Demo Map ===================
// Demo map is root providing the id => route mapping info.
// Which is generated by `meta-demos` template
getDemoById: (id: string) => Promise<DemoInfo | null>;
}

export const SiteContext = createContext<ISiteContext>({
Expand All @@ -38,8 +43,22 @@ export const SiteContext = createContext<ISiteContext>({
loading: false,
setLoading: () => {},
_2_level_nav_available: true,
getDemoById: async () => null,
});

export const useSiteData = () => {
return useContext(SiteContext);
};

const cache = new Map<string, Promise<DemoInfo | null>>();

// Async load demo data
export function useDemoData(demoId: string) {
const { getDemoById } = useSiteData();

if (!cache.has(demoId)) {
cache.set(demoId, getDemoById(demoId));
}

return use(cache.get(demoId)!);
}
30 changes: 30 additions & 0 deletions src/client/theme-api/context/use.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copy from React official demo.
// This will be replace if React release new version of use hooks
type ReactPromise<T> = Promise<T> & {
status?: 'pending' | 'fulfilled' | 'rejected';
value?: T;
reason?: any;
};

export default function use<T>(promise: ReactPromise<T>): T {
if (promise.status === 'fulfilled') {
return promise.value!;
} else if (promise.status === 'rejected') {
throw promise.reason;
} else if (promise.status === 'pending') {
throw promise;
} else {
promise.status = 'pending';
promise.then(
(result) => {
promise.status = 'fulfilled';
promise.value = result;
},
(reason) => {
promise.status = 'rejected';
promise.reason = reason;
},
);
throw promise;
}
}
8 changes: 4 additions & 4 deletions src/client/theme-api/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
export {
createIntlCache,
defineMessages,
FormattedDate,
FormattedDateParts,
FormattedDisplayName,
Expand All @@ -12,17 +10,19 @@ export {
FormattedRelativeTime,
FormattedTime,
FormattedTimeParts,
injectIntl,
IntlContext,
IntlProvider,
RawIntlProvider,
createIntlCache,
defineMessages,
injectIntl,
useIntl,
} from 'react-intl';
export { AtomRenderer } from './AtomRenderer';
export { useSiteData } from './context';
export { DumiDemo } from './DumiDemo';
export { DumiDemoGrid } from './DumiDemoGrid';
export { DumiPage } from './DumiPage';
export { useDemoData, useSiteData } from './context';
export { openCodeSandbox } from './openCodeSandbox';
export { openStackBlitz } from './openStackBlitz';
export type { IPreviewerProps } from './types';
Expand Down
12 changes: 12 additions & 0 deletions src/features/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ export default (api: IApi) => {
},
});

// generate meta lazy entry
api.writeTmpFile({
noPluginDir: true,
path: 'dumi/meta/demos.ts',
tplPath: winPath(join(TEMPLATES_DIR, 'meta-demos.ts.tpl')),
context: {
metaFiles: parsedMetaFiles.filter((metaFile) =>
metaFile.file.endsWith('.md'),
),
},
});

// generate runtime plugin, to append page meta to route object
api.writeTmpFile({
noPluginDir: true,
Expand Down
2 changes: 2 additions & 0 deletions src/templates/ContextWrapper.ts.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
import { useOutlet, history } from 'dumi';
import { SiteContext } from '{{{contextPath}}}';
import { demos, components } from '../meta';
import { getDemoById } from '../meta/demos';
import { locales } from '../locales/config';
{{{defaultExport}}}
{{{namedExport}}}
Expand Down Expand Up @@ -43,6 +44,7 @@ export default function DumiContextWrapper() {
hostname: {{{hostname}}},
themeConfig: {{{themeConfig}}},
_2_level_nav_available: {{{_2_level_nav_available}}},
getDemoById,
};

return (
Expand Down
Loading

0 comments on commit 595a3d8

Please sign in to comment.