Skip to content

Commit

Permalink
feat: merge chunk & optimize meta loading (#1906)
Browse files Browse the repository at this point in the history
* feat: use new loader for search

* feat: search support tsx

* feat: split meta chunk

* feat: load meta on hover

* chore: code clena
  • Loading branch information
MadCcc committed Sep 22, 2023
1 parent d969e98 commit e0cac83
Show file tree
Hide file tree
Showing 12 changed files with 133 additions and 132 deletions.
4 changes: 2 additions & 2 deletions src/client/theme-api/useRouteMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import { useMemo } from 'react';
import use from './context/use';
import type { IRouteMeta } from './types';

const cache = new Map<string, ReturnType<getRouteMetaById>>();
const cache = new Map<string, any>();

const useAsyncRouteMeta = (id: string) => {
if (!cache.has(id)) {
cache.set(id, getRouteMetaById(id));
}

return use<ReturnType<getRouteMetaById>>(cache.get(id));
return use<any>(cache.get(id)!);
};

const emptyMeta = {
Expand Down
14 changes: 9 additions & 5 deletions src/client/theme-api/useSiteSearch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,10 @@ export const useSiteSearch = () => {
const setter = useCallback((val: string) => {
setLoading(true);
setKeywords(val);

if (val) {
setEnabled(true);
}
}, []);

const loadSearchData = () => setEnabled(true);

const [filledRoutes, demos] = useSearchData(enabled);
const mergedLoading = loading || !filledRoutes;

Expand Down Expand Up @@ -112,5 +110,11 @@ export const useSiteSearch = () => {
}
}, [keywords, filledRoutes]);

return { keywords, setKeywords: setter, result, loading: mergedLoading };
return {
keywords,
setKeywords: setter,
result,
loading: mergedLoading,
loadSearchData,
};
};
5 changes: 3 additions & 2 deletions src/client/theme-api/useSiteSearch/useSearchData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ export default function useSearchData(enabled: boolean): ReturnData {

React.useEffect(() => {
if (enabled) {
loadFilesMeta().then((data) => {
loadFilesMeta(Object.keys(routes)).then((data) => {
setFilesMeta(data);
});
}
}, [enabled]);
}, [enabled, routes]);

return React.useMemo(() => {
if (!filesMeta) {
Expand Down Expand Up @@ -65,6 +65,7 @@ export default function useSearchData(enabled: boolean): ReturnData {
Object.values(meta.demos).forEach((demo) => {
demo.routeId = id;
});

// merge demos
Object.assign(acc, meta.demos);

Expand Down
3 changes: 2 additions & 1 deletion src/client/theme-default/slots/SearchBar/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type NativeInputProps = React.DetailedHTMLProps<

type InputProps = {
onChange: (keywords: string) => void;
} & Pick<NativeInputProps, 'onFocus' | 'onBlur'>;
} & Pick<NativeInputProps, 'onFocus' | 'onBlur' | 'onMouseEnter'>;

export const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
const intl = useIntl();
Expand All @@ -29,6 +29,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
}}
onFocus={props.onFocus}
onBlur={props.onBlur}
onMouseEnter={props.onMouseEnter}
onKeyDown={(ev) => {
if (['ArrowDown', 'ArrowUp'].includes(ev.key)) ev.preventDefault();
// esc to blur input
Expand Down
10 changes: 8 additions & 2 deletions src/client/theme-default/slots/SearchBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const SearchBar: FC = () => {
const inputRef = useRef<HTMLInputElement>(null);
const modalInputRef = useRef<HTMLInputElement>(null);
const [symbol, setSymbol] = useState('⌘');
const { keywords, setKeywords, result, loading } = useSiteSearch();
const { keywords, setKeywords, result, loading, loadSearchData } =
useSiteSearch();
const [modalVisible, setModalVisible] = useState(false);

useEffect(() => {
Expand Down Expand Up @@ -77,7 +78,12 @@ const SearchBar: FC = () => {
<div className="dumi-default-search-bar">
<IconSearch className="dumi-default-search-bar-svg" />
<Input
onFocus={() => setFocusing(true)}
onFocus={() => {
setFocusing(true);
}}
onMouseEnter={() => {
loadSearchData();
}}
onBlur={() => {
// wait for item click
setTimeout(() => {
Expand Down
29 changes: 0 additions & 29 deletions src/features/compile/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { IDemoLoaderOptions } from '@/loaders/demo';
import type { IMdLoaderOptions } from '@/loaders/markdown';
import ReactTechStack from '@/techStacks/react';
import type { IApi, IDumiTechStack } from '@/types';
import { addAtomMeta, addExampleAssets } from '../assets';

export default (api: IApi) => {
api.describe({ key: 'dumi:compile' });
Expand Down Expand Up @@ -51,34 +50,6 @@ export default (api: IApi) => {
.rule('dumi-md')
.type('javascript/auto')
.test(/\.md$/)
// get meta for each markdown file
// TODO: should be removed
.oneOf('md-meta')
.resourceQuery(/meta$/)
.use('babel-loader')
.loader(babelInUmi.loader)
.options(babelInUmi.options)
.end()
.use('md-meta-loader')
.loader(loaderPath)
.options({
...loaderBaseOpts,
mode: 'meta',
onResolveDemos(demos) {
const assets = demos.reduce<Parameters<typeof addExampleAssets>[0]>(
(ret, demo) => {
if ('asset' in demo) ret.push(demo.asset);
return ret;
},
[],
);

addExampleAssets(assets);
},
onResolveAtomMeta: addAtomMeta,
} as IMdLoaderOptions)
.end()
.end()
// get page demo for each markdown file
.oneOf('md-demo')
.resourceQuery(/demo$/)
Expand Down
11 changes: 11 additions & 0 deletions src/features/meta.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TEMPLATES_DIR } from '@/constants';
import type { IApi } from '@/types';
import { generateMetaChunkName } from '@/utils';
import path, { join } from 'path';
import type { IRoute } from 'umi';
import { winPath } from 'umi/plugin-utils';
Expand Down Expand Up @@ -93,6 +94,16 @@ export default (api: IApi) => {
tplPath: winPath(join(TEMPLATES_DIR, 'meta-route.ts.tpl')),
context: {
metaFiles: mdFiles,
chunkName: function chunkName(this) {
if (!('file' in this)) {
return '';
}
return `/* webpackChunkName: "${generateMetaChunkName(
this.file,
api.cwd,
api.config.locales?.map(({ id }) => id),
)}" */`;
},
},
});

Expand Down
84 changes: 20 additions & 64 deletions src/loaders/markdown/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { isTabRouteFile } from '@/features/tabs';
import type { IThemeLoadResult } from '@/features/theme/loader';
import { getCache } from '@/utils';
import { generateMetaChunkName, getCache } from '@/utils';
import fs from 'fs';
import { lodash, Mustache, winPath } from 'umi/plugin-utils';
import { Mustache, lodash, winPath } from 'umi/plugin-utils';
import transform, {
type IMdTransformerOptions,
type IMdTransformerResult,
Expand Down Expand Up @@ -65,59 +65,6 @@ function getDemoSourceFiles(demos: IMdTransformerResult['meta']['demos'] = []) {
}, []);
}

function emitMeta(
this: any,
opts: IMdLoaderDemosModeOptions,
ret: IMdTransformerResult,
) {
const { frontmatter, toc, texts, demos } = ret.meta;

return Mustache.render(
`import React from 'react';
export const demos = {
{{#demos}}
'{{{id}}}': {
component: {{{component}}},
asset: {{{renderAsset}}}
},
{{/demos}}
};
export const frontmatter = {{{frontmatter}}};
export const toc = {{{toc}}};
export const texts = {{{texts}}};
`,
{
demos,
frontmatter: JSON.stringify(frontmatter),
toc: JSON.stringify(toc),
texts: JSON.stringify(texts),
renderAsset: function renderAsset(this: NonNullable<typeof demos>[0]) {
// do not render asset for inline demo
if (!('asset' in this)) return 'null';

// render asset for normal demo
let { asset } = this;
const { sources } = this;

// use raw-loader to load all source files
Object.keys(this.sources).forEach((file: string) => {
// handle un-existed source file, e.g. custom tech-stack return custom dependencies
if (!asset.dependencies[file]) return;

// to avoid modify original asset object
asset = lodash.cloneDeep(asset);
asset.dependencies[
file
].value = `{{{require('-!${sources[file]}?dumi-raw').default}}}`;
});

return JSON.stringify(asset, null, 2).replace(/"{{{|}}}"/g, '');
},
},
);
}

function emitDefault(
this: any,
opts: IMdLoaderDefaultModeOptions,
Expand All @@ -142,21 +89,29 @@ function emitDefault(
.map((item) => `import ${item.specifier} from '${item.source}';`)
.join('\n')}
import React from 'react';
${isTabContent ? `` : `import { DumiPage } from 'dumi';`}
import { texts as ${CONTENT_TEXTS_OBJ_NAME} } from '${winPath(
this.resourcePath,
)}?type=text';
${
isTabContent
? `import { useTabMeta } from 'dumi';`
: `import { DumiPage, useRouteMeta } from 'dumi';`
}
// export named function for fastRefresh
// ref: https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/docs/TROUBLESHOOTING.md#edits-always-lead-to-full-reload
function DumiMarkdownContent() {
const { texts: ${CONTENT_TEXTS_OBJ_NAME} } = use${
isTabContent ? 'TabMeta' : 'RouteMeta'
}();
return ${isTabContent ? ret.content : `<DumiPage>${ret.content}</DumiPage>`};
}
export default DumiMarkdownContent;`;
}

function emitDemo(opts: IMdLoaderDemoModeOptions, ret: IMdTransformerResult) {
function emitDemo(
this: any,
opts: IMdLoaderDemoModeOptions,
ret: IMdTransformerResult,
) {
const { demos } = ret.meta;

return Mustache.render(
Expand Down Expand Up @@ -212,7 +167,11 @@ function emitDemoIndex(
};`,
{
ids: JSON.stringify(demos?.map((demo) => demo.id)),
getter: `() => import('${winPath(this.resourcePath)}?type=demo')`,
getter: `() => import(/* webpackChunkName: "${generateMetaChunkName(
this.resourcePath,
opts.cwd,
opts.locales.map(({ id }) => id),
)}" */'${winPath(this.resourcePath)}?type=demo')`,
},
);
}
Expand Down Expand Up @@ -251,9 +210,6 @@ function emit(this: any, opts: IMdLoaderOptions, ret: IMdTransformerResult) {
getDemoSourceFiles(demos).forEach((file) => this.addDependency(file));

switch (opts.mode) {
// TODO: Should be removed
case 'meta':
return emitMeta.call(this, opts, ret);
case 'demo':
return emitDemo.call(this, opts, ret);
case 'demo-index':
Expand Down
4 changes: 2 additions & 2 deletions src/templates/meta-demos.ts.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { demoIndex as dmi{{{index}}} } from '{{{file}}}?type=demo-index';
{{/metaFiles}}

const demoIndexes: Record<string, { ids: string[], getter: () => Promise<any> }> = {
export const demoIndexes: Record<string, { ids: string[], getter: () => Promise<any> }> = {
{{#metaFiles}}
'{{{id}}}': dmi{{{index}}},
{{/metaFiles}}
Expand Down Expand Up @@ -31,4 +31,4 @@ export const getDemoById = async (id: string) => {
const { demos }: any = await getter() || {};

return demos?.[id];
};
};
2 changes: 1 addition & 1 deletion src/templates/meta-route.ts.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { filesFrontmatter } from './frontmatter';
const files = {
{{#metaFiles}}
'{{{id}}}': {
textGetter: () => import('{{{file}}}?type=text'),
textGetter: () => import({{{chunkName}}}'{{{file}}}?type=text'),
{{#tabs}}
tabs: {{{tabs}}},
{{/tabs}}
Expand Down
44 changes: 21 additions & 23 deletions src/templates/meta-search.ts.tpl
Original file line number Diff line number Diff line change
@@ -1,43 +1,41 @@
// This will bundle all the site demos and meta data into one file
// which should only async load on search
const filesMetaTabs = {
{{#metaFiles}}
{{#tabs}}
'{{{id}}}': {{{tabs}}},
{{/tabs}}
{{/metaFiles}}
}
import { getRouteMetaById } from './route-meta';
import { demoIndexes } from './demos';
import { filesFrontmatter } from './frontmatter';

// generate demos data in runtime, for reuse route.id to reduce bundle size
export const demos = {};

/** @private Internal usage. Safe to refactor. */
export async function loadFilesMeta() {
export async function loadFilesMeta(idList: string[]) {
const metaMap: Record<string, any> = {};
const idList = [
{{#metaFiles}}
'{{{id}}}',
{{/metaFiles}}
];

{{#metaFiles}}
metaMap['{{{id}}}'] = import('{{{file}}}?type=meta');
if (idList.includes('{{{id}}}')) {
metaMap['{{{id}}}'] = async () => {
const routeMeta = await getRouteMetaById('{{{id}}}');
const demo = await demoIndexes['{{{id}}}']?.getter() || {};
return {
frontmatter: filesFrontmatter['{{{id}}}'] ?? {},
toc: routeMeta?.toc ?? [],
texts: routeMeta?.texts ?? [],
tabs: routeMeta?.tabs ?? [],
demos: demo?.demos ?? {},
};
};
}
{{/metaFiles}}

// Wait for all meta data to be loaded
const metaList = await Promise.all(idList.map(id => metaMap[id]));
const metaList = await Promise.all(Object.entries(metaMap).map(([id, getter]) => getter()));

// Merge into filesMeta
const filesMeta = {};

idList.forEach((id, index) => {
const meta = metaList[index];
filesMeta[id] = {
...meta,
tabs: filesMetaTabs[id],
};
Object.entries(metaMap).forEach(([id], index) => {
filesMeta[id] = metaList[index];
});

return filesMeta;
}
}
Loading

0 comments on commit e0cac83

Please sign in to comment.