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(preset): entity support #1247

Closed
wants to merge 1 commit into from
Closed
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
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,8 @@
]
]
}
},
"dependencies": {
"typescript-parser": "^2.6.2"
}
}
6 changes: 6 additions & 0 deletions packages/assets-types/typings/atom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,14 @@ export type AtomPropsDefinition = Record<string, {
* property whether required
*/
required?: true;
links?: AtomLink[];
}[]>;

export interface AtomLink {
url: string;
title: string;
}

export default interface AtomAsset {
/**
* The export module identifier of atom asset
Expand Down
2 changes: 1 addition & 1 deletion packages/preset-dumi/src/api-parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as parser from 'react-docgen-typescript-dumi-tmp';
import { buildFilter as getBuiltinFilter } from 'react-docgen-typescript-dumi-tmp/lib/buildFilter';
import FileCache from '../utils/cache';
import ctx from '../context';
import type { AtomPropsDefinition } from 'dumi-assets-types';
import type { AtomLink, AtomPropsDefinition } from 'dumi-assets-types';
import type {
PropFilter as IPropFilter,
PropItem as IPropItem,
Expand Down
1 change: 1 addition & 0 deletions packages/preset-dumi/src/theme/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const FALLBACK_THEME = `${THEME_PREFIX}default`;
export const REQUIRED_THEME_BUILTINS = [
'Alert',
'API',
'Entity',
'Badge',
'Example',
'Previewer',
Expand Down
2 changes: 1 addition & 1 deletion packages/preset-dumi/src/transformer/remark/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export default function api(): IDumiUnifiedTransformer {
sourcePath: path.dirname(this.data('fileAbsPath')),
silent: true,
});

parseOpts.componentName = vFile.data.componentName;
definitions = parser(sourcePath, parseOpts);
identifier = vFile.data.componentName;
Expand Down
1 change: 1 addition & 0 deletions packages/preset-dumi/src/transformer/remark/domWarn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const NO_PARAGRAPH_RULES: { type: string; tagName: string; [key: string]: any }[
{ type: 'element', tagName: 'code', properties: { src: Boolean } },
{ type: 'element', tagName: 'embed', properties: { src: Boolean } },
{ type: 'element', tagName: 'API' },
{ type: 'element', tagName: 'Entity' },
];

/**
Expand Down
238 changes: 238 additions & 0 deletions packages/preset-dumi/src/transformer/remark/entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import fs from 'fs';
import path from 'path';
import type { Node } from 'unist';
import deepmerge from 'deepmerge';
import is from 'hast-util-is-element';
import has from 'hast-util-has-property';
import visit from 'unist-util-visit';
import { parseElmAttrToProps } from './utils';
import parser from '../../api-parser';
import { getModuleResolvePath } from '../../utils/moduleResolver';
import { listenFileOnceChange } from '../../utils/watcher';
import ctx from '../../context';
import type { ArgsType } from '@umijs/utils';
import type { IDumiUnifiedTransformer, IDumiElmNode } from '.';

import { TypescriptParser, ClassLikeDeclaration } from 'typescript-parser';
import { AtomPropsDefinition } from 'dumi-assets-types';

const tparser = new TypescriptParser();

function applyEntityData(identifier: string, definitions: ReturnType<typeof parser>) {
if (identifier && definitions) {
ctx.umi?.applyPlugins({
key: 'dumi.detectApi',
type: ctx.umi.ApplyPluginsType.event,
args: {
identifier,
data: definitions,
},
});
}
}

/**
* serialize api node to [title, node, title, node, ...]
* @param node original api node
* @param identifier api parse identifier, mapping in .umi/dumi/apis.json
* @param definitions api definitions
*/
function serializeEntityNodes(
node: IDumiElmNode,
identifier: string,
definitions: ReturnType<typeof parser>,
) {
const parsedAttrs = parseElmAttrToProps(node.properties);
const expts: string[] = parsedAttrs.exports || Object.keys(definitions);
const showTitle = !parsedAttrs.hideTitle;

return expts.reduce<(IDumiElmNode | Node)[]>((list, expt, i) => {
// render large API title if it is default export
// or it is the first export and the exports attribute was not custom

const isInsertAPITitle = expt === 'default' || (!i && !parsedAttrs.exports);
// render sub title for non-default export
const isInsertSubTitle = expt !== 'default';
const entityNode = deepmerge({}, node);

// insert API title
if (showTitle && isInsertAPITitle) {
list.push(
{
type: 'element',
tagName: 'h2',
properties: {},
children: [{ type: 'text', value: '其他实体类型定义' }],
},
{
type: 'text',
value: '\n',
},
);
}

// insert export sub title
if (showTitle && isInsertSubTitle) {
list.push(
{
type: 'element',
tagName: 'h3',
properties: { id: `entity-${expt.toLowerCase()}` },
children: [{ type: 'text', value: expt }],
},
{
type: 'text',
value: '\n',
},
);
}

// insert API Node
delete entityNode.properties.exports;
entityNode.properties.identifier = identifier;
entityNode.properties.export = expt;

entityNode._dumi_parsed = true;
list.push(entityNode);

return list;
}, []);
}

/**
* detect component name via file path
*/
function guessComponentName(fileAbsPath: string) {
const parsed = path.parse(fileAbsPath);

if (['index', 'index.d'].includes(parsed.name)) {
// button/index.tsx => button
// packages/button/src/index.tsx => button
// packages/button/lib/index.d.ts => button
// windows: button\\src\\index.tsx => button
// windows: button\\lib\\index.d.ts => button
return path.basename(parsed.dir.replace(/(\/|\\)(src|lib)$/, ''));
}

// components/button.tsx => button
return parsed.name;
}

/**
* watch component change to update api data
* @param absPath component absolute path
* @param identifier api identifier
* @param parseOpts extra parse options
*/
function watchComponentUpdate(
absPath: string,
identifier: string,
parseOpts: ArgsType<typeof parser>[1],
) {
listenFileOnceChange(absPath, () => {
let definitions: ReturnType<typeof parser>;

try {
definitions = parseDefinitions(absPath);
} catch (err) {
/* noting */
}

// update api data
applyEntityData(identifier, definitions);

// watch next turn
// FIXME: workaround for resolve no such file error
/* istanbul ignore next */
setTimeout(
() => {
watchComponentUpdate(absPath, identifier, parseOpts);
},
fs.existsSync(absPath) ? 0 : 50,
);
});
}

/**
* remark plugin for parse embed tag to external module
*/
export default function entity(): IDumiUnifiedTransformer {
return (ast, vFile) => {
visit<IDumiElmNode>(ast, 'element', (node, i, parent) => {
if (is(node, 'Entity') && !node._dumi_parsed) {
let identifier: string;
let definitions: ReturnType<typeof parser>;
const parseOpts = parseElmAttrToProps(node.properties);
// console.log('parseOpts', parseOpts)

if (has(node, 'src')) {
const src = node.properties.src || '';
let absPath = path.join(path.dirname(this.data('fileAbsPath')), src);
try {
absPath = getModuleResolvePath({
basePath: process.cwd(),
sourcePath: src,
silent: true,
});
} catch (err) {
// nothing
}
// guess component name if there has no identifier property
const componentName = node.properties.identifier || guessComponentName(absPath);

parseOpts.componentName = componentName;
identifier = 'entity-' + componentName || src;
definitions = parseDefinitions(absPath)

// trigger listener to update previewer props after this file changed
watchComponentUpdate(absPath, identifier, parseOpts);
} else if (vFile.data.componentName) {
try {
const sourcePath = getModuleResolvePath({
basePath: process.cwd(),
sourcePath: path.dirname(this.data('fileAbsPath')),
silent: true,
});

parseOpts.componentName = vFile.data.componentName;

definitions = parseDefinitions(sourcePath);
identifier = 'entity-' + vFile.data.componentName;

// trigger listener to update previewer props after this file changed
watchComponentUpdate(sourcePath, identifier, parseOpts);
} catch (err) {
/* noting */
}
}

if (identifier && definitions) {
// replace original node
parent.children.splice(i, 1, ...serializeEntityNodes(node, identifier, definitions));

// apply api data
applyEntityData(identifier, definitions);
}
}
});
};
}
function parseDefinitions(sourcePath: string): AtomPropsDefinition {
const parsed = tparser.parseFileSync(sourcePath, '');
const declarations = parsed.declarations;
const definitions: AtomPropsDefinition = {};
declarations.forEach(declaration => {
if (Object.hasOwn(declaration, 'properties')) {
const d = declaration as ClassLikeDeclaration;
definitions[declaration.name] = d.properties.map(property => {
return {
identifier: property.name,
type: property.type,
description: '',
required: !property.isOptional ? true : undefined,
};
});
}
});
return definitions;
}
3 changes: 3 additions & 0 deletions packages/preset-dumi/src/transformer/remark/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import codeBlock from './codeBlock';
import code from './code';
import embed from './embed';
import api from './api';
import entity from './entity';
import mdComponent from './mdComponent';
import link from './link';
import img from './img';
Expand Down Expand Up @@ -139,6 +140,8 @@ export default (source: string, fileAbsPath: string, type: 'jsx' | 'html', maste
.use(debug('comments'))
.use(code)
.use(debug('code'))
.use(entity)
.use(debug('entity'))
.use(api)
.use(debug('api'))
.use(mdComponent)
Expand Down
5 changes: 5 additions & 0 deletions packages/preset-dumi/src/transformer/remark/raw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export default (): IDumiUnifiedTransformer => ast => {
if (typeof node.value === 'string' && /<dumi-raw-a-p-i/.test(node.value)) {
node.value = node.value.replace(/ ?\/?>/g, '></dumi-raw-a-p-i>');
}

// special process <Entity /> for same reason in above
if (typeof node.value === 'string' && /<dumi-raw-entity/.test(node.value)) {
node.value = node.value.replace(/ ?\/?>/g, '></dumi-raw-entity>');
}
});

// raw to hast tree
Expand Down
Loading