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: split markdown meta loader #1875

Merged
merged 6 commits into from
Sep 8, 2023
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
10 changes: 10 additions & 0 deletions examples/normal/.dumi/pages/loader-test.tsx
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
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';
49 changes: 49 additions & 0 deletions src/features/compile/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export default (api: IApi) => {
.type('javascript/auto')
.test(/\.md$/)
// get meta for each markdown file
// TODO: should be removed
Copy link
Member

Choose a reason for hiding this comment

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

留着 meta 是为了让编译能通过对么,下面 onResolveDemos 的钩子函数和里面的参数类型是不是可以先删了,不然调用两遍资产元数据会有问题

.oneOf('md-meta')
.resourceQuery(/meta$/)
.use('babel-loader')
Expand All @@ -78,6 +79,54 @@ export default (api: IApi) => {
} as IMdLoaderOptions)
.end()
.end()
// get page demo for each markdown file
.oneOf('md-demo')
.resourceQuery(/demo$/)
.use('babel-loader')
.loader(babelInUmi.loader)
.options(babelInUmi.options)
.end()
.use('md-demo-loader')
.loader(loaderPath)
.options({
...loaderBaseOpts,
mode: 'demo',
})
.end()
.end()
// get page demo-index for each markdown file
.oneOf('md-demo-index')
.resourceQuery(/demo-index$/)
.use('md-demo-index-loader')
.loader(loaderPath)
.options({
...loaderBaseOpts,
mode: 'demo-index',
})
.end()
.end()
// get page frontmatter for each markdown file
.oneOf('md-frontmatter')
.resourceQuery(/frontmatter$/)
.use('md-frontmatter-loader')
.loader(loaderPath)
.options({
...loaderBaseOpts,
mode: 'frontmatter',
})
.end()
.end()
// get page text for each markdown file
.oneOf('md-text')
.resourceQuery(/text$/)
.use('md-text-loader')
.loader(loaderPath)
.options({
...loaderBaseOpts,
mode: 'text',
})
.end()
.end()
// get page component for each markdown file
.oneOf('md')
.use('babel-loader')
Expand Down
270 changes: 203 additions & 67 deletions src/loaders/markdown/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@ interface IMdLoaderDefaultModeOptions
extends Omit<IMdTransformerOptions, 'fileAbsPath'> {
mode?: 'markdown';
builtins: IThemeLoadResult['builtins'];
}

interface IMdLoaderDemosModeOptions
extends Omit<IMdLoaderDefaultModeOptions, 'builtins' | 'mode'> {
mode: 'meta';
onResolveDemos?: (
demos: NonNullable<IMdTransformerResult['meta']['demos']>,
) => void;
Expand All @@ -27,9 +22,38 @@ interface IMdLoaderDemosModeOptions
) => void;
}

interface IMdLoaderDemosModeOptions
extends Omit<IMdLoaderDefaultModeOptions, 'builtins' | 'mode'> {
mode: 'meta';
}

interface IMdLoaderDemoModeOptions
extends Omit<IMdLoaderDefaultModeOptions, 'builtins' | 'mode'> {
mode: 'demo';
}

interface IMdLoaderDemoIndexModeOptions
extends Omit<IMdLoaderDefaultModeOptions, 'builtins' | 'mode'> {
mode: 'demo-index';
}

interface IMdLoaderFrontmatterModeOptions
extends Omit<IMdLoaderDefaultModeOptions, 'builtins' | 'mode'> {
mode: 'frontmatter';
}

interface IMdLoaderTextModeOptions
extends Omit<IMdLoaderDefaultModeOptions, 'builtins' | 'mode'> {
mode: 'text';
}

export type IMdLoaderOptions =
| IMdLoaderDefaultModeOptions
| IMdLoaderDemosModeOptions;
| IMdLoaderDemosModeOptions
| IMdLoaderDemoModeOptions
| IMdLoaderFrontmatterModeOptions
| IMdLoaderTextModeOptions
| IMdLoaderDemoIndexModeOptions;

function getDemoSourceFiles(demos: IMdTransformerResult['meta']['demos'] = []) {
return demos.reduce<string[]>((ret, demo) => {
Expand All @@ -41,30 +65,15 @@ function getDemoSourceFiles(demos: IMdTransformerResult['meta']['demos'] = []) {
}, []);
}

function emit(this: any, opts: IMdLoaderOptions, ret: IMdTransformerResult) {
const { demos, embeds } = ret.meta;

// declare embedded files as loader dependency, for re-compiling when file changed
embeds!.forEach((file) => this.addDependency(file));

// declare demo source files as loader dependency, for re-compiling when file changed
getDemoSourceFiles(demos).forEach((file) => this.addDependency(file));

if (opts.mode === 'meta') {
const { frontmatter, toc, texts } = ret.meta;

// apply demos resolve hook
if (demos && opts.onResolveDemos) {
opts.onResolveDemos(demos);
}
function emitMeta(
this: any,
opts: IMdLoaderDemosModeOptions,
ret: IMdTransformerResult,
) {
const { frontmatter, toc, texts, demos } = ret.meta;

// apply atom meta resolve hook
if (frontmatter!.atomId && opts.onResolveAtomMeta) {
opts.onResolveAtomMeta(frontmatter!.atomId, frontmatter);
}

return Mustache.render(
`import React from 'react';
return Mustache.render(
`import React from 'react';

export const demos = {
{{#demos}}
Expand All @@ -78,43 +87,60 @@ 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, '');
},
{
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, '');
},
);
} else {
// do not wrap DumiPage for tab content
const isTabContent = isTabRouteFile(this.resourcePath);

// import all builtin components, may be used by markdown content
return `${Object.values(opts.builtins)
.map((item) => `import ${item.specifier} from '${item.source}';`)
.join('\n')}
},
);
}

function emitDefault(
this: any,
opts: IMdLoaderDefaultModeOptions,
ret: IMdTransformerResult,
) {
const { frontmatter, demos } = ret.meta;
// do not wrap DumiPage for tab content
const isTabContent = isTabRouteFile(this.resourcePath);

// apply demos resolve hook
if (demos && opts.onResolveDemos) {
opts.onResolveDemos(demos);
}

// apply atom meta resolve hook
if (frontmatter!.atomId && opts.onResolveAtomMeta) {
opts.onResolveAtomMeta(frontmatter!.atomId, frontmatter);
}

// import all builtin components, may be used by markdown content
return `${Object.values(opts.builtins)
.map((item) => `import ${item.specifier} from '${item.source}';`)
.join('\n')}
import React from 'react';
${
isTabContent
Expand All @@ -126,12 +152,122 @@ ${
// 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'
}();
isTabContent ? 'TabMeta' : 'RouteMeta'
}();
return ${isTabContent ? ret.content : `<DumiPage>${ret.content}</DumiPage>`};
}

export default DumiMarkdownContent;`;
}

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

return Mustache.render(
`import React from 'react';

export const demos = {
{{#demos}}
'{{{id}}}': {
component: {{{component}}},
asset: {{{renderAsset}}}
},
{{/demos}}
};`,
{
demos,
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 emitDemoIndex(
this: any,
opts: IMdLoaderDemoIndexModeOptions,
ret: IMdTransformerResult,
) {
const { demos } = ret.meta;

return Mustache.render(
`export const demoIndex = {
ids: {{{ids}}},
getter: {{{getter}}}
};`,
{
ids: JSON.stringify(demos?.map((demo) => demo.id)),
getter: `() => import('${winPath(this.resourcePath)}?type=demo')`,
},
);
}

function emitFrontmatter(
opts: IMdLoaderFrontmatterModeOptions,
ret: IMdTransformerResult,
) {
const { frontmatter } = ret.meta;

return Mustache.render(`export const frontmatter = {{{frontmatter}}};`, {
frontmatter: JSON.stringify(frontmatter),
});
}

function emitText(opts: IMdLoaderTextModeOptions, ret: IMdTransformerResult) {
const { texts, toc } = ret.meta;

return Mustache.render(
`export const toc = {{{toc}}};
export const texts = {{{texts}}};`,
{
toc: JSON.stringify(toc),
texts: JSON.stringify(texts),
},
);
}

function emit(this: any, opts: IMdLoaderOptions, ret: IMdTransformerResult) {
const { demos, embeds } = ret.meta;

// declare embedded files as loader dependency, for re-compiling when file changed
embeds!.forEach((file) => this.addDependency(file));

// declare demo source files as loader dependency, for re-compiling when file changed
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':
return emitDemoIndex.call(this, opts, ret);
case 'frontmatter':
return emitFrontmatter.call(this, opts, ret);
case 'text':
return emitText.call(this, opts, ret);
default:
return emitDefault.call(this, opts, ret);
}
}

Expand Down
Loading