dumi 2.4.35 → 2.4.37

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.
@@ -13,7 +13,7 @@ export interface IParsedBlockAsset {
13
13
  resolveMap: Record<string, string>;
14
14
  frontmatter: ReturnType<typeof parseCodeFrontmatter>['frontmatter'];
15
15
  }
16
- declare function parseBlockAsset(opts: {
16
+ interface IParseBlockAssetOptions {
17
17
  fileAbsPath: string;
18
18
  fileLocale?: string;
19
19
  id: string;
@@ -21,5 +21,7 @@ declare function parseBlockAsset(opts: {
21
21
  entryPointCode?: string;
22
22
  resolver: typeof sync;
23
23
  techStack: IDumiTechStack;
24
- }): Promise<IParsedBlockAsset>;
24
+ cacheable?: boolean;
25
+ }
26
+ declare function parseBlockAsset(opts: IParseBlockAssetOptions): Promise<IParsedBlockAsset>;
25
27
  export default parseBlockAsset;
@@ -39,7 +39,71 @@ var import_fs = __toESM(require("fs"));
39
39
  var import_path = __toESM(require("path"));
40
40
  var import_plugin_utils = require("umi/plugin-utils");
41
41
  var import_constants = require("../constants");
42
+ var MAX_BLOCK_ASSET_CACHE_SIZE = 512;
43
+ var blockAssetCache = /* @__PURE__ */ new Map();
44
+ function cloneParsedBlockAsset(ret) {
45
+ return {
46
+ asset: JSON.parse(JSON.stringify(ret.asset)),
47
+ resolveMap: { ...ret.resolveMap },
48
+ frontmatter: ret.frontmatter ? JSON.parse(JSON.stringify(ret.frontmatter)) : ret.frontmatter
49
+ };
50
+ }
51
+ function getContentHashFromFile(file) {
52
+ try {
53
+ return (0, import_utils.getContentHash)(import_fs.default.readFileSync(file, "utf-8"));
54
+ } catch {
55
+ return "missing";
56
+ }
57
+ }
58
+ function getDepsKey(deps) {
59
+ return JSON.stringify(
60
+ Array.from(new Set(deps)).sort().map((file) => `${(0, import_plugin_utils.winPath)(file)}:${getContentHashFromFile(file)}`)
61
+ );
62
+ }
63
+ function getParseCacheKey(opts) {
64
+ const entryContent = opts.entryPointCode ?? import_fs.default.readFileSync(opts.fileAbsPath, "utf-8");
65
+ return JSON.stringify({
66
+ file: (0, import_plugin_utils.winPath)(opts.fileAbsPath),
67
+ fileLocale: opts.fileLocale,
68
+ id: opts.id,
69
+ refAtomIds: opts.refAtomIds,
70
+ entryHash: (0, import_utils.getContentHash)(entryContent),
71
+ techStack: opts.techStack.name,
72
+ runtimeOpts: opts.techStack.runtimeOpts,
73
+ hasOnBlockLoad: Boolean(opts.techStack.onBlockLoad),
74
+ hasGenerateMetadata: Boolean(opts.techStack.generateMetadata),
75
+ hasGenerateSources: Boolean(opts.techStack.generateSources)
76
+ });
77
+ }
78
+ function getParseDeps(ret, entryFile) {
79
+ return Object.values(ret.resolveMap).filter(
80
+ (file) => import_path.default.isAbsolute(file) && file !== entryFile
81
+ );
82
+ }
83
+ function getCachedBlockAsset(cacheKey) {
84
+ const cached = blockAssetCache.get(cacheKey);
85
+ if (cached && cached.depsKey === getDepsKey(cached.deps)) {
86
+ return cloneParsedBlockAsset(cached.result);
87
+ }
88
+ }
89
+ function setCachedBlockAsset(cacheKey, ret, deps) {
90
+ if (blockAssetCache.size >= MAX_BLOCK_ASSET_CACHE_SIZE) {
91
+ const firstKey = blockAssetCache.keys().next().value;
92
+ if (firstKey)
93
+ blockAssetCache.delete(firstKey);
94
+ }
95
+ blockAssetCache.set(cacheKey, {
96
+ deps,
97
+ depsKey: getDepsKey(deps),
98
+ result: cloneParsedBlockAsset(ret)
99
+ });
100
+ }
42
101
  async function parseBlockAsset(opts) {
102
+ const cacheKey = opts.cacheable ? getParseCacheKey(opts) : "";
103
+ const cached = cacheKey ? getCachedBlockAsset(cacheKey) : void 0;
104
+ if (cached) {
105
+ return cached;
106
+ }
43
107
  const asset = {
44
108
  type: "BLOCK",
45
109
  id: opts.id,
@@ -163,9 +227,18 @@ async function parseBlockAsset(opts) {
163
227
  }
164
228
  ]
165
229
  });
230
+ let hasError = false;
166
231
  try {
167
232
  await deferrer;
168
233
  } catch {
234
+ hasError = true;
235
+ }
236
+ if (!hasError && cacheKey) {
237
+ setCachedBlockAsset(
238
+ cacheKey,
239
+ result,
240
+ getParseDeps(result, opts.fileAbsPath)
241
+ );
169
242
  }
170
243
  return result;
171
244
  }
@@ -1,11 +1,14 @@
1
- import { useDemo, useLiveDemo, useParams } from 'dumi';
1
+ import { useDemo, useLiveDemo, useLocation, useParams } from 'dumi';
2
2
  import { createElement, useEffect } from 'react';
3
3
  import { useRenderer } from "../../theme-api/useRenderer";
4
4
  import "./index.less";
5
5
  var DemoRenderPage = function DemoRenderPage() {
6
6
  var params = useParams();
7
+ var _useLocation = useLocation(),
8
+ search = _useLocation.search;
7
9
  var id = params.id;
8
- var demo = useDemo(id);
10
+ var routeId = new URLSearchParams(search).get('routeId') || undefined;
11
+ var demo = useDemo(id, undefined, undefined, routeId);
9
12
  var _useRenderer = useRenderer({
10
13
  id: id,
11
14
  component: demo.component,
@@ -1,10 +1,16 @@
1
1
  import { type FC } from 'react';
2
- import type { IPreviewerProps } from '../types';
2
+ import type { IDemoData, IPreviewerProps } from '../types';
3
+ type DemoGetter = () => Promise<{
4
+ demos: Record<string, IDemoData>;
5
+ }>;
3
6
  export interface IDumiDemoProps {
4
7
  demo: {
5
8
  id: string;
6
9
  inline?: boolean;
10
+ loader?: DemoGetter;
11
+ version?: string;
7
12
  };
8
13
  previewerProps: Omit<IPreviewerProps, 'asset' | 'children'>;
9
14
  }
10
15
  export declare const DumiDemo: FC<IDumiDemoProps>;
16
+ export {};
@@ -10,8 +10,11 @@ var InternalDumiDemo = function InternalDumiDemo(props) {
10
10
  historyType = _useSiteData.historyType;
11
11
  var _useAppData = useAppData(),
12
12
  basename = _useAppData.basename;
13
- var id = props.demo.id;
14
- var demo = useDemo(id);
13
+ var _props$demo = props.demo,
14
+ id = _props$demo.id,
15
+ loader = _props$demo.loader,
16
+ version = _props$demo.version;
17
+ var demo = useDemo(id, loader, version);
15
18
  var component = demo.component,
16
19
  asset = demo.asset,
17
20
  renderOpts = demo.renderOpts;
@@ -31,12 +34,13 @@ var InternalDumiDemo = function InternalDumiDemo(props) {
31
34
  return demoNode;
32
35
  }
33
36
  var isHashRoute = historyType === 'hash';
37
+ var routeQuery = demo.routeId ? "?routeId=".concat(encodeURIComponent(demo.routeId)) : '';
34
38
  return /*#__PURE__*/React.createElement(Previewer, _extends({
35
39
  asset: asset,
36
40
  demoUrl:
37
41
  // allow user override demoUrl by frontmatter
38
42
  props.previewerProps.demoUrl || // when use hash route, browser can automatically handle relative paths starting with #
39
- "".concat(isHashRoute ? "#" : '').concat(basename).concat(SP_ROUTE_PREFIX, "demos/").concat(props.demo.id)
43
+ "".concat(isHashRoute ? "#" : '').concat(basename).concat(SP_ROUTE_PREFIX, "demos/").concat(props.demo.id).concat(routeQuery)
40
44
  }, props.previewerProps), props.previewerProps.iframe ? null : demoNode);
41
45
  };
42
46
  export var DumiDemo = /*#__PURE__*/React.memo(InternalDumiDemo, function (prev, next) {
@@ -1,5 +1,6 @@
1
1
  import type { IApi } from '../types';
2
2
  export declare const TABS_META_PATH = "dumi/meta/tabs.ts";
3
3
  export declare const ATOMS_META_PATH = "dumi/meta/atoms.ts";
4
+ export declare const DEMO_INDEX_META_PATH = "dumi/meta/demoIndex.ts";
4
5
  export default _default;
5
6
  declare function _default(api: IApi): void;
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  var meta_exports = {};
31
31
  __export(meta_exports, {
32
32
  ATOMS_META_PATH: () => ATOMS_META_PATH,
33
+ DEMO_INDEX_META_PATH: () => DEMO_INDEX_META_PATH,
33
34
  TABS_META_PATH: () => TABS_META_PATH,
34
35
  default: () => meta_default
35
36
  });
@@ -40,6 +41,7 @@ var import_plugin_utils = require("umi/plugin-utils");
40
41
  var import_tabs = require("./tabs");
41
42
  var TABS_META_PATH = "dumi/meta/tabs.ts";
42
43
  var ATOMS_META_PATH = "dumi/meta/atoms.ts";
44
+ var DEMO_INDEX_META_PATH = "dumi/meta/demoIndex.ts";
43
45
  var meta_default = (api) => {
44
46
  const metaFiles = [];
45
47
  api.register({
@@ -71,8 +73,11 @@ var meta_default = (api) => {
71
73
  key: "dumi.modifyMetaFiles",
72
74
  initialValue: JSON.parse(JSON.stringify(metaFiles))
73
75
  });
76
+ const useUtoopackDemoHMR = api.env === "development" && Boolean(api.config.utoopack);
74
77
  parsedMetaFiles.forEach((metaFile) => {
75
78
  metaFile.isMarkdown = metaFile.file.endsWith(".md");
79
+ metaFile.loadDemoIndex = metaFile.isMarkdown && !useUtoopackDemoHMR;
80
+ metaFile.loadDemoIndexMap = metaFile.isMarkdown && useUtoopackDemoHMR;
76
81
  });
77
82
  api.writeTmpFile({
78
83
  noPluginDir: true,
@@ -93,6 +98,14 @@ var meta_default = (api) => {
93
98
  }
94
99
  }
95
100
  });
101
+ api.writeTmpFile({
102
+ noPluginDir: true,
103
+ path: DEMO_INDEX_META_PATH,
104
+ tplPath: require.resolve("../templates/meta/demoIndex.ts.tpl"),
105
+ context: {
106
+ metaFiles: parsedMetaFiles
107
+ }
108
+ });
96
109
  api.writeTmpFile({
97
110
  noPluginDir: true,
98
111
  path: "dumi/meta/runtime.ts",
@@ -118,5 +131,6 @@ var meta_default = (api) => {
118
131
  // Annotate the CommonJS export names for ESM import in node:
119
132
  0 && (module.exports = {
120
133
  ATOMS_META_PATH,
134
+ DEMO_INDEX_META_PATH,
121
135
  TABS_META_PATH
122
136
  });
@@ -53,6 +53,17 @@ function getDemoSourceFiles(demos = []) {
53
53
  function isRelativePath(path2) {
54
54
  return /^\.{1,2}(?!\w)/.test(path2);
55
55
  }
56
+ function normalizeRouteFile(file) {
57
+ return (0, import_plugin_utils.winPath)(file).replace(/\.(mdx?)\.js$/, ".$1");
58
+ }
59
+ function getRouteId(opts, fileAbsPath) {
60
+ const normalizedFile = normalizeRouteFile(fileAbsPath);
61
+ const route = Object.values(opts.routes ?? {}).find((item) => {
62
+ const routeFile = item.file || item.__absFile;
63
+ return routeFile && normalizeRouteFile(routeFile) === normalizedFile;
64
+ });
65
+ return route == null ? void 0 : route.id;
66
+ }
56
67
  function emitDefault(opts, ret) {
57
68
  const { frontmatter, demos } = ret.meta;
58
69
  const isTabContent = (0, import_tabs.isTabRouteFile)(this.resourcePath);
@@ -133,6 +144,7 @@ function emitDemo(opts, ret) {
133
144
  }).filter((item) => item !== void 0)
134
145
  );
135
146
  }, []);
147
+ const routeId = getRouteId(opts, this.resourcePath);
136
148
  return import_plugin_utils.Mustache.render(
137
149
  `import React from 'react';
138
150
  import '${(0, import_plugin_utils.winPath)(this.resourcePath)}?watch=parent';
@@ -146,6 +158,9 @@ export const demos = {
146
158
  component: {{{component}}},
147
159
  {{/component}}
148
160
  asset: {{{renderAsset}}},
161
+ {{#routeId}}
162
+ routeId: '{{{routeId}}}',
163
+ {{/routeId}}
149
164
  context: {{{renderContext}}},
150
165
  renderOpts: {{{renderRenderOpts}}},
151
166
  },
@@ -154,6 +169,7 @@ export const demos = {
154
169
  {
155
170
  demos,
156
171
  dedupedDemosDeps,
172
+ routeId,
157
173
  renderAsset: function renderAsset() {
158
174
  if (!("asset" in this))
159
175
  return "null";
@@ -291,7 +307,9 @@ function emitText(opts, ret) {
291
307
  function emit(opts, ret) {
292
308
  const { demos, embeds } = ret.meta;
293
309
  embeds.forEach((file) => this.addDependency(file));
294
- getDemoSourceFiles(demos).forEach((file) => this.addDependency(file));
310
+ if (opts.mode !== "demo-index") {
311
+ getDemoSourceFiles(demos).forEach((file) => this.addDependency(file));
312
+ }
295
313
  if (this.resourceQuery.includes("watch=parent"))
296
314
  return null;
297
315
  switch (opts.mode) {
@@ -320,6 +338,7 @@ function mdLoader(content) {
320
338
  var _a, _b, _c;
321
339
  let opts = this.getOptions();
322
340
  const loaderContextPath = opts[import_utoopackLoaders.UTOOPACK_LOADER_CTX_KEY];
341
+ const useUtoopackDemoHMR = process.env.NODE_ENV !== "production" && Boolean(loaderContextPath);
323
342
  if (loaderContextPath) {
324
343
  const ctx = require(loaderContextPath);
325
344
  if (!((_a = opts.techStacks) == null ? void 0 : _a.length)) {
@@ -346,6 +365,7 @@ function mdLoader(content) {
346
365
  const baseCacheKey = [
347
366
  this.resourcePath,
348
367
  (0, import_utils.getContentHash)(content),
368
+ useUtoopackDemoHMR,
349
369
  JSON.stringify(import_plugin_utils.lodash.omit(opts, ["mode", "builtins", "onResolveDemos"]))
350
370
  ].join(":");
351
371
  const cacheKey = [
@@ -364,7 +384,8 @@ function mdLoader(content) {
364
384
  }
365
385
  deferrer[cacheKey] = (0, import_transformer.default)(content, {
366
386
  ...import_plugin_utils.lodash.omit(opts, ["mode", "builtins", "onResolveDemos"]),
367
- fileAbsPath: (0, import_plugin_utils.winPath)(this.resourcePath)
387
+ fileAbsPath: (0, import_plugin_utils.winPath)(this.resourcePath),
388
+ useUtoopackDemoHMR
368
389
  });
369
390
  deferrer[cacheKey].then((ret) => {
370
391
  depsMapping[this.resourcePath] = ret.meta.embeds.concat(
@@ -48,6 +48,7 @@ declare module 'vfile' {
48
48
  export interface IMdTransformerOptions {
49
49
  cwd: string;
50
50
  fileAbsPath: string;
51
+ useUtoopackDemoHMR?: boolean;
51
52
  alias: ResolveOptions['alias'];
52
53
  parentAbsPath?: string;
53
54
  techStacks: IDumiTechStack[];
@@ -113,6 +113,7 @@ var transformer_default = async (raw, opts) => {
113
113
  fileAbsPath: opts.fileAbsPath,
114
114
  fileLocaleLessPath,
115
115
  fileLocale,
116
+ useUtoopackDemoHMR: opts.useUtoopackDemoHMR,
116
117
  resolve: opts.resolve,
117
118
  resolver
118
119
  }).use(import_rehypeSlug.default).use(import_rehypeLink.default, {
@@ -10,6 +10,7 @@ type IRehypeDemoOptions = Pick<IMdTransformerOptions, 'techStacks' | 'cwd' | 'fi
10
10
  resolver: typeof sync;
11
11
  fileLocaleLessPath: string;
12
12
  fileLocale?: string;
13
+ useUtoopackDemoHMR?: boolean;
13
14
  };
14
15
  export default function rehypeDemo(opts: IRehypeDemoOptions): Transformer<Root>;
15
16
  export {};
@@ -47,6 +47,8 @@ var toString;
47
47
  var isElement;
48
48
  var DEMO_NODE_CONTAINER = "$demo-container";
49
49
  var DEMO_PROP_VALUE_KEY = "$demo-prop-value-key";
50
+ var DEMO_LOADER_PLACEHOLDER = "__DUMI_DEMO_LOADER__";
51
+ var DEMO_LOADER_PLACEHOLDER_VALUE = DEMO_LOADER_PLACEHOLDER;
50
52
  var DUMI_DEMO_TAG = "DumiDemo";
51
53
  var DUMI_DEMO_GRID_TAG = "DumiDemoGrid";
52
54
  var SKIP_DEMO_PARSE = "pure";
@@ -249,9 +251,15 @@ function rehypeDemo(opts) {
249
251
  });
250
252
  }
251
253
  const propDemo = { id: parseOpts.id };
254
+ if (opts.useUtoopackDemoHMR) {
255
+ propDemo.loader = DEMO_LOADER_PLACEHOLDER_VALUE;
256
+ }
252
257
  demoIds.push(parseOpts.id);
253
258
  deferrers.push(
254
- (0, import_block.default)(parseOpts).then(
259
+ (0, import_block.default)({
260
+ ...parseOpts,
261
+ cacheable: opts.useUtoopackDemoHMR
262
+ }).then(
255
263
  async ({ asset, resolveMap, frontmatter }) => {
256
264
  var _a2, _b2, _c2;
257
265
  if (demoIds.indexOf(parseOpts.id) !== demoIds.lastIndexOf(parseOpts.id)) {
@@ -287,6 +295,11 @@ function rehypeDemo(opts) {
287
295
  if (originalProps[key])
288
296
  asset[key] = originalProps[key];
289
297
  });
298
+ if (opts.useUtoopackDemoHMR) {
299
+ propDemo.version = (0, import_utils.getContentHash)(
300
+ JSON.stringify(asset.dependencies)
301
+ );
302
+ }
290
303
  if (/ inline/.test(String((_b2 = codeNode.data) == null ? void 0 : _b2.meta)) || originalProps.inline) {
291
304
  propDemo.inline = true;
292
305
  return {
@@ -368,7 +381,16 @@ function rehypeDemo(opts) {
368
381
  await Promise.all(deferrers).then((demos) => {
369
382
  vFile.data.demos = demos;
370
383
  replaceNodes.forEach((node) => {
371
- const value = JSON.stringify(node.data[DEMO_PROP_VALUE_KEY]);
384
+ const demoLoader = `() => import('${(0, import_plugin_utils.winPath)(
385
+ opts.fileAbsPath
386
+ )}?type=demo')`;
387
+ let value = JSON.stringify(node.data[DEMO_PROP_VALUE_KEY]);
388
+ if (opts.useUtoopackDemoHMR) {
389
+ value = value.replace(
390
+ new RegExp(`"${DEMO_LOADER_PLACEHOLDER}"`, "g"),
391
+ demoLoader
392
+ );
393
+ }
372
394
  if (node.JSXAttributes[0].type === "JSXAttribute") {
373
395
  node.JSXAttributes[0].value = value;
374
396
  } else {
@@ -0,0 +1,10 @@
1
+ export const demoIndexMap = {
2
+ {{#metaFiles}}
3
+ {{#loadDemoIndexMap}}
4
+ '{{{id}}}': () =>
5
+ import('{{{file}}}?type=demo-index').then(
6
+ ({ demoIndex }) => demoIndex,
7
+ ),
8
+ {{/loadDemoIndexMap}}
9
+ {{/metaFiles}}
10
+ };
@@ -34,19 +34,53 @@ export function use<T>(promise: ReactPromise<T>): T {
34
34
  }
35
35
  }
36
36
 
37
- const demoIdMap = Object.keys(filesMeta).reduce((total, current) => {
38
- if (filesMeta[current].demoIndex) {
39
- const { ids, getter } = filesMeta[current].demoIndex;
37
+ type DemoGetter = () => Promise<{ demos: Record<string, IDemoData> }>;
38
+ type DemoIndex = {
39
+ ids: string[];
40
+ getter: DemoGetter;
41
+ };
42
+ type DemoIndexGetter = () => Promise<DemoIndex>;
40
43
 
41
- ids.forEach((id) => {
42
- total[id] = getter;
43
- });
44
- }
44
+ const demoIndexes = Object.entries(filesMeta)
45
+ .map(([id, meta]) => ({
46
+ id,
47
+ demoIndex: meta.demoIndex,
48
+ }))
49
+ .filter(
50
+ (item): item is { id: string; demoIndex: DemoIndex } =>
51
+ Boolean(item.demoIndex),
52
+ );
53
+
54
+ const demoIdMap = demoIndexes.reduce<Record<string, DemoGetter>>(
55
+ (total, { demoIndex }) => {
56
+ if (demoIndex) {
57
+ const { ids, getter } = demoIndex;
58
+
59
+ ids.forEach((id) => {
60
+ total[id] = getter;
61
+ });
62
+ }
45
63
 
46
- return total;
47
- }, {});
64
+ return total;
65
+ },
66
+ {},
67
+ );
48
68
 
49
69
  const demosCache = new Map<string, Promise<IDemoData | undefined>>();
70
+ let demoIndexMapPromise:
71
+ | Promise<Record<string, DemoIndexGetter | undefined>>
72
+ | undefined;
73
+
74
+ function loadDemoIndexMap() {
75
+ if (!demoIndexMapPromise) {
76
+ demoIndexMapPromise = import('./demoIndex').then(
77
+ ({ demoIndexMap }) =>
78
+ demoIndexMap as Record<string, DemoIndexGetter | undefined>,
79
+ );
80
+ }
81
+
82
+ return demoIndexMapPromise;
83
+ }
50
84
 
51
85
  /**
52
86
  * expand context for source omit extension
@@ -65,36 +99,128 @@ function expandDemoContext(context?: IDemoData['context']) {
65
99
  }
66
100
  }
67
101
 
102
+ function getDemoIdCandidates(id: string) {
103
+ const ids = [id];
104
+ const localeLessId = id.replace(/-[a-z]{2}(?:-[A-Z]{2})?$/, '');
105
+
106
+ if (localeLessId !== id) {
107
+ ids.push(localeLessId);
108
+ }
109
+
110
+ return ids;
111
+ }
112
+
113
+ async function getDemoFromDemoIndex(
114
+ id: string,
115
+ demoIndexGetter: DemoIndexGetter,
116
+ ) {
117
+ const demoIndex = await demoIndexGetter();
118
+ const demoId = getDemoIdCandidates(id).find((candidate) =>
119
+ demoIndex.ids.includes(candidate),
120
+ );
121
+ const getter = demoId ? demoIndex.getter : undefined;
122
+
123
+ if (!getter) return undefined;
124
+
125
+ demoIdMap[id] = getter;
126
+ const { demos } = await getter();
127
+ const demo = demos[id] ?? demos[demoId!];
128
+
129
+ if (!demo) return undefined;
130
+
131
+ expandDemoContext(demo.context);
132
+ return demo;
133
+ }
134
+
135
+ async function getDemoFromRouteIndex(id: string, routeId: string) {
136
+ const demoIndexMap = await loadDemoIndexMap();
137
+ const demoIndexGetter = demoIndexMap[routeId];
138
+
139
+ if (!demoIndexGetter) return getDemoFromIndexMap(id);
140
+
141
+ return (
142
+ (await getDemoFromDemoIndex(id, demoIndexGetter)) ??
143
+ getDemoFromIndexMap(id)
144
+ );
145
+ }
146
+
147
+ async function getDemoFromIndexMap(id: string) {
148
+ const demoIndexMap = await loadDemoIndexMap();
149
+ const demoIndexGetters = Object.values(demoIndexMap).filter(
150
+ (item): item is DemoIndexGetter => Boolean(item),
151
+ );
152
+
153
+ for (const demoIndexGetter of demoIndexGetters) {
154
+ const demo = await getDemoFromDemoIndex(id, demoIndexGetter).catch(
155
+ () => undefined,
156
+ );
157
+ if (demo) return demo;
158
+ }
159
+ }
160
+
68
161
  /**
69
162
  * use demo data by id
70
163
  */
71
- export function useDemo(id: string): IDemoData | undefined {
72
- if (!demosCache.get(id)) {
164
+ export function useDemo(
165
+ id: string,
166
+ loader?: DemoGetter,
167
+ version?: string,
168
+ routeId?: string,
169
+ ): IDemoData | undefined {
170
+ const cacheKey = version
171
+ ? `${id}:${version}`
172
+ : routeId
173
+ ? `${id}:route=${routeId}`
174
+ : id;
175
+ const getter = loader ?? demoIdMap[id];
176
+
177
+ if (!demosCache.get(cacheKey)) {
73
178
  demosCache.set(
74
- id,
75
- demoIdMap[id]?.().then(({ demos }) => {
76
- // expand context for omit ext
77
- expandDemoContext(demos[id].context);
78
- return demos[id];
79
- }),
179
+ cacheKey,
180
+ getter
181
+ ? getter().then(({ demos }) => {
182
+ const demo = demos[id];
183
+ if (!demo) return undefined;
184
+
185
+ // expand context for omit ext
186
+ expandDemoContext(demo.context);
187
+ return demo;
188
+ })
189
+ : routeId
190
+ ? getDemoFromRouteIndex(id, routeId)
191
+ : getDemoFromIndexMap(id),
80
192
  );
193
+
194
+ // Reuse local demo data for consumers that still call useDemo(id), such as useLiveDemo.
195
+ demosCache.set(id, demosCache.get(cacheKey)!);
81
196
  }
82
197
 
83
- return use(demosCache.get(id)!);
198
+ return use(demosCache.get(cacheKey)!);
84
199
  }
85
200
 
86
201
  /**
87
202
  * get all demos
88
203
  */
89
204
  export async function getFullDemos() {
90
- const demoFilesMeta = Object.entries(filesMeta).filter(
91
- ([_id, meta]) => meta.demoIndex,
205
+ const demoIndexMap = await loadDemoIndexMap();
206
+ const lazyDemoIndexes = await Promise.all(
207
+ Object.entries(demoIndexMap).map(async ([id, demoIndexGetter]) => ({
208
+ id,
209
+ demoIndex: await demoIndexGetter?.().catch(() => undefined),
210
+ })),
211
+ );
212
+ const allDemoIndexes = [
213
+ ...demoIndexes,
214
+ ...lazyDemoIndexes,
215
+ ].filter(
216
+ (item): item is { id: string; demoIndex: DemoIndex } =>
217
+ Boolean(item.demoIndex),
92
218
  );
93
219
 
94
220
  return Promise.all(
95
- demoFilesMeta.map(async ([id, meta]) => ({
221
+ allDemoIndexes.map(async ({ id, demoIndex }) => ({
96
222
  id,
97
- demos: (await meta.demoIndex.getter()).demos as Record<string, IDemoData>,
223
+ demos: (await demoIndex.getter()).demos as Record<string, IDemoData>,
98
224
  })),
99
225
  ).then((ret) =>
100
226
  ret.reduce<Record<string, IDemoData>>((total, { id, demos }) => {
@@ -1,10 +1,10 @@
1
1
  {{#metaFiles}}
2
- {{#isMarkdown}}
2
+ {{#loadDemoIndex}}
3
3
  import { frontmatter as fm{{{index}}}, toc as t{{{index}}}, demoIndex as dmi{{{index}}} } from '{{{file}}}?type=frontmatter';
4
- {{/isMarkdown}}
5
- {{^isMarkdown}}
4
+ {{/loadDemoIndex}}
5
+ {{^loadDemoIndex}}
6
6
  import { frontmatter as fm{{{index}}}, toc as t{{{index}}} } from '{{{file}}}?type=frontmatter';
7
- {{/isMarkdown}}
7
+ {{/loadDemoIndex}}
8
8
  {{/metaFiles}}
9
9
 
10
10
  export const filesMeta = {
@@ -12,9 +12,9 @@ export const filesMeta = {
12
12
  '{{{id}}}': {
13
13
  frontmatter: fm{{{index}}},
14
14
  toc: t{{{index}}},
15
- {{#isMarkdown}}
15
+ {{#loadDemoIndex}}
16
16
  demoIndex: dmi{{{index}}},
17
- {{/isMarkdown}}
17
+ {{/loadDemoIndex}}
18
18
  {{#tabs}}
19
19
  tabs: {{{tabs}}},
20
20
  {{/tabs}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dumi",
3
- "version": "2.4.35",
3
+ "version": "2.4.37",
4
4
  "description": "📖 Documentation Generator of React Component",
5
5
  "keywords": [
6
6
  "generator",