component-auto-docs 0.1.1 → 0.1.2
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.
- package/README.md +125 -34
- package/bin/component-auto-docs.mjs +0 -0
- package/core/generate-component-docs.mjs +590 -83
- package/package.json +2 -1
- package/templates/src/docs/runtime/component-doc-page.vue +166 -71
- package/templates/src/docs/runtime/use-auto-component-doc.ts +87 -44
- package/SHARING.md +0 -435
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { parse as parseSfc } from '@vue/compiler-sfc';
|
|
4
5
|
import ts from 'typescript';
|
|
5
6
|
|
|
6
7
|
const rootDir = process.cwd();
|
|
@@ -107,7 +108,7 @@ function createPageRouteEntry(routePath, title) {
|
|
|
107
108
|
"style": {
|
|
108
109
|
"navigationBarTitleText": "${title}"
|
|
109
110
|
}
|
|
110
|
-
|
|
111
|
+
},
|
|
111
112
|
`;
|
|
112
113
|
}
|
|
113
114
|
|
|
@@ -280,12 +281,29 @@ function readLiteralValue(node, sourceFile) {
|
|
|
280
281
|
return node.getText(sourceFile);
|
|
281
282
|
}
|
|
282
283
|
|
|
283
|
-
function
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const block = blocks.find((item) => matcher(item[1]));
|
|
284
|
+
function formatSfcParseError(error) {
|
|
285
|
+
if (typeof error === 'string') return error;
|
|
286
|
+
if (error?.message) return error.message;
|
|
287
287
|
|
|
288
|
-
return
|
|
288
|
+
return String(error);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function readSfcBlocks(filePath) {
|
|
292
|
+
const source = fs.readFileSync(filePath, 'utf8');
|
|
293
|
+
const { descriptor, errors } = parseSfc(source, { filename: filePath });
|
|
294
|
+
|
|
295
|
+
if (errors.length) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`Failed to parse Vue SFC ${toPosix(path.relative(rootDir, filePath))}: ${errors
|
|
298
|
+
.map(formatSfcParseError)
|
|
299
|
+
.join('; ')}`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
script: descriptor.scriptSetup?.content || descriptor.script?.content || '',
|
|
305
|
+
template: descriptor.template?.content || '',
|
|
306
|
+
};
|
|
289
307
|
}
|
|
290
308
|
|
|
291
309
|
function parseJsdoc(script) {
|
|
@@ -675,6 +693,7 @@ function normalizeExamples(metaExamples = [], jsdocExamples = []) {
|
|
|
675
693
|
const normalized = typeof example === 'string' ? { title: '示例', description: '', code: example } : example;
|
|
676
694
|
const code = normalized.code?.trim();
|
|
677
695
|
if (!code || seen.has(code)) continue;
|
|
696
|
+
if (normalized.title === '基础用法' && normalized.description?.startsWith('自动根据 ')) continue;
|
|
678
697
|
|
|
679
698
|
seen.add(code);
|
|
680
699
|
examples.push({
|
|
@@ -687,6 +706,525 @@ function normalizeExamples(metaExamples = [], jsdocExamples = []) {
|
|
|
687
706
|
return examples;
|
|
688
707
|
}
|
|
689
708
|
|
|
709
|
+
function escapeRegExp(text) {
|
|
710
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function kebabToCamel(text) {
|
|
714
|
+
return text.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function decodeHtmlText(text) {
|
|
718
|
+
return text
|
|
719
|
+
.replace(/</g, '<')
|
|
720
|
+
.replace(/>/g, '>')
|
|
721
|
+
.replace(/"/g, '"')
|
|
722
|
+
.replace(/'/g, "'")
|
|
723
|
+
.replace(/&/g, '&');
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function getObjectKeyText(nameNode, sourceFile) {
|
|
727
|
+
if (!nameNode) return '';
|
|
728
|
+
if (ts.isIdentifier(nameNode) || ts.isStringLiteralLike(nameNode) || ts.isNumericLiteral(nameNode)) {
|
|
729
|
+
return nameNode.text;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return nameNode.getText(sourceFile).replace(/^['"]|['"]$/g, '');
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function getIdentifierSampleValue(name) {
|
|
736
|
+
const samples = {
|
|
737
|
+
text: '这是一段足够长的公告文字,用于触发滚动预览效果',
|
|
738
|
+
title: '标题',
|
|
739
|
+
content: '内容',
|
|
740
|
+
label: '选项',
|
|
741
|
+
value: '1',
|
|
742
|
+
name: '名称',
|
|
743
|
+
status: 'done',
|
|
744
|
+
time: '10:30',
|
|
745
|
+
src: '/static/icon/avatar.svg',
|
|
746
|
+
url: '/static/icon/avatar.svg',
|
|
747
|
+
poster: '/static/icon/avatar.svg',
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
return samples[name] || name;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function evaluateStaticNode(node, sourceFile, bindings = {}, options = {}) {
|
|
754
|
+
if (!node) return undefined;
|
|
755
|
+
|
|
756
|
+
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
|
|
757
|
+
return evaluateStaticNode(node.expression, sourceFile, bindings, options);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (ts.isStringLiteralLike(node) || ts.isNoSubstitutionTemplateLiteral(node)) return decodeHtmlText(node.text);
|
|
761
|
+
if (ts.isNumericLiteral(node)) return Number(node.text);
|
|
762
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
763
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
764
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
765
|
+
|
|
766
|
+
if (ts.isIdentifier(node)) {
|
|
767
|
+
if (Object.prototype.hasOwnProperty.call(bindings, node.text)) return bindings[node.text];
|
|
768
|
+
|
|
769
|
+
return options.allowIdentifierFallback ? getIdentifierSampleValue(node.text) : undefined;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
773
|
+
return node.elements
|
|
774
|
+
.map((item) => evaluateStaticNode(item, sourceFile, bindings, { ...options, allowIdentifierFallback: true }))
|
|
775
|
+
.filter((item) => item !== undefined);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
779
|
+
const result = {};
|
|
780
|
+
|
|
781
|
+
for (const property of node.properties) {
|
|
782
|
+
if (ts.isPropertyAssignment(property)) {
|
|
783
|
+
const key = getObjectKeyText(property.name, sourceFile);
|
|
784
|
+
if (!key) continue;
|
|
785
|
+
const value = evaluateStaticNode(property.initializer, sourceFile, bindings, {
|
|
786
|
+
...options,
|
|
787
|
+
allowIdentifierFallback: true,
|
|
788
|
+
});
|
|
789
|
+
if (value !== undefined) result[key] = value;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
793
|
+
const key = property.name.text;
|
|
794
|
+
result[key] = Object.prototype.hasOwnProperty.call(bindings, key) ? bindings[key] : getIdentifierSampleValue(key);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return result;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return undefined;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function evaluateStaticExpression(expression, bindings = {}) {
|
|
805
|
+
const source = `const __value = (${expression});`;
|
|
806
|
+
const sourceFile = ts.createSourceFile('preview-expression.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
807
|
+
const statement = sourceFile.statements[0];
|
|
808
|
+
const declaration = statement?.declarationList?.declarations?.[0];
|
|
809
|
+
|
|
810
|
+
return evaluateStaticNode(declaration?.initializer, sourceFile, bindings, { allowIdentifierFallback: false });
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function parseExampleBindings(code) {
|
|
814
|
+
const bindings = {};
|
|
815
|
+
const declarationPattern = /(?:const|let|var)\s+([\w$\u00a0-\uffff]+)\s*=\s*([^;\n]+)/g;
|
|
816
|
+
let match;
|
|
817
|
+
|
|
818
|
+
while ((match = declarationPattern.exec(code))) {
|
|
819
|
+
const [, name, expression] = match;
|
|
820
|
+
const value = evaluateStaticExpression(expression, bindings);
|
|
821
|
+
if (value !== undefined) bindings[name] = value;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return bindings;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function parseExampleExpression(expression, bindings = {}) {
|
|
828
|
+
const value = expression.trim();
|
|
829
|
+
if (!value) return undefined;
|
|
830
|
+
if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
|
|
831
|
+
if (value === 'true') return true;
|
|
832
|
+
if (value === 'false') return false;
|
|
833
|
+
if (value === 'null') return null;
|
|
834
|
+
|
|
835
|
+
const quoted = value.match(/^(['"])([\s\S]*)\1$/);
|
|
836
|
+
if (quoted) return decodeHtmlText(quoted[2]);
|
|
837
|
+
|
|
838
|
+
if (value.startsWith('{') || value.startsWith('[')) {
|
|
839
|
+
try {
|
|
840
|
+
return JSON.parse(value);
|
|
841
|
+
} catch {
|
|
842
|
+
// Fall through to the TypeScript AST evaluator for JS-style literals.
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return evaluateStaticExpression(value, bindings);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function coerceStaticExampleValue(rawValue, prop) {
|
|
850
|
+
if (rawValue === undefined) return true;
|
|
851
|
+
|
|
852
|
+
const type = prop?.type || '';
|
|
853
|
+
const value = decodeHtmlText(rawValue);
|
|
854
|
+
if (type.includes('array') || type.includes('Array') || type.includes('object') || type.includes('Object')) {
|
|
855
|
+
const parsed = parseExampleExpression(value);
|
|
856
|
+
if (parsed !== undefined) return parsed;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (type.includes('boolean')) return value === 'true';
|
|
860
|
+
if (type.includes('number') || type.includes('Number')) {
|
|
861
|
+
const numberValue = Number(value);
|
|
862
|
+
return Number.isFinite(numberValue) ? numberValue : value;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return value;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function normalizeSlotContent(content) {
|
|
869
|
+
return decodeHtmlText(content)
|
|
870
|
+
.replace(/<[^>]+>/g, '')
|
|
871
|
+
.replace(/\s+/g, ' ')
|
|
872
|
+
.trim();
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const previewKinds = new Set([
|
|
876
|
+
'inline',
|
|
877
|
+
'form',
|
|
878
|
+
'data-list',
|
|
879
|
+
'measure',
|
|
880
|
+
'overlay',
|
|
881
|
+
'page-shell',
|
|
882
|
+
'native',
|
|
883
|
+
'composite',
|
|
884
|
+
]);
|
|
885
|
+
|
|
886
|
+
const commentPreviewKindRules = [
|
|
887
|
+
{
|
|
888
|
+
kind: 'page-shell',
|
|
889
|
+
keywords: ['页面根', '页面容器', '页面壳', '页面布局', '标题栏', '导航栏', '状态栏', '安全区', 'safearea', 'status bar'],
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
kind: 'measure',
|
|
893
|
+
keywords: ['跑马灯', '内容超出', '超出时', '横向滚动', '循环滚动', '滚动到', '吸顶', '尺寸测量', '测量', 'selectorquery'],
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
kind: 'overlay',
|
|
897
|
+
keywords: ['弹窗', '弹层', '浮层', '遮罩', 'popover', 'popup', 'dialog', 'modal'],
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
kind: 'native',
|
|
901
|
+
keywords: ['上传', '图片预览', '预览图片', '二维码', 'qrcode', 'canvas', 'video', '视频', '拨打', '联系', '原生能力', '相册'],
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
kind: 'composite',
|
|
905
|
+
keywords: ['装修', '设计页', '楼层', '复合', '业务组件', '复杂业务'],
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
kind: 'data-list',
|
|
909
|
+
keywords: ['列表', '数据列表', '树形', '树节点', '时间线', '步骤', '瀑布流', 'tabs', '选项卡'],
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
kind: 'form',
|
|
913
|
+
keywords: ['输入', '表单', '选择', '选择器', '勾选', '单选', '多选', '开关', '验证码', 'textarea', 'input', 'radio', 'checkbox', 'switch'],
|
|
914
|
+
},
|
|
915
|
+
];
|
|
916
|
+
|
|
917
|
+
function normalizePreviewKind(kind) {
|
|
918
|
+
return previewKinds.has(kind) ? kind : '';
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function textIncludesAny(text, keywords) {
|
|
922
|
+
const normalizedText = text.toLowerCase();
|
|
923
|
+
|
|
924
|
+
return keywords.some((keyword) => normalizedText.includes(keyword.toLowerCase()));
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function inferPreviewKindFromText(text) {
|
|
928
|
+
if (!text) return '';
|
|
929
|
+
|
|
930
|
+
for (const rule of commentPreviewKindRules) {
|
|
931
|
+
if (textIncludesAny(text, rule.keywords)) return rule.kind;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return '';
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function inferPreviewKindFromStructure(componentName, props, events, slots) {
|
|
938
|
+
const lowerName = componentName.toLowerCase();
|
|
939
|
+
const propNames = new Set(props.map((prop) => prop.name));
|
|
940
|
+
const hasModel = propNames.has('modelValue');
|
|
941
|
+
const hasListData = props.some((prop) => ['items', 'list', 'range', 'tabs', 'arr', 'data', 'options'].includes(prop.name));
|
|
942
|
+
const hasUploadLike = /upload|qrcode|video|contact/.test(lowerName);
|
|
943
|
+
const hasPageShellName = /page|header|footer|safearea|status-bar/.test(lowerName);
|
|
944
|
+
const hasOverlayName = /dialog|pop|popover|modal|popup/.test(lowerName);
|
|
945
|
+
const hasMeasureName = /marquee|tabs-scroll|swiper/.test(lowerName);
|
|
946
|
+
const hasDataName = /list|tree|timeline|steps|tabs/.test(lowerName);
|
|
947
|
+
|
|
948
|
+
if (hasPageShellName) return 'page-shell';
|
|
949
|
+
if (hasMeasureName) return 'measure';
|
|
950
|
+
if (hasOverlayName) return 'overlay';
|
|
951
|
+
if (hasUploadLike) return 'native';
|
|
952
|
+
if (/design|calculate|citys/.test(lowerName)) return 'composite';
|
|
953
|
+
if (hasDataName || hasListData) return 'data-list';
|
|
954
|
+
if (hasModel || /input|textarea|radio|checkbox|switch|slider|sms|search/.test(lowerName)) return 'form';
|
|
955
|
+
if (events.some((event) => event.name === 'click') || slots.length) return 'inline';
|
|
956
|
+
|
|
957
|
+
return 'inline';
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function inferPreviewKind(componentName, props, events, slots, jsdoc, metaPreview) {
|
|
961
|
+
const explicitKind = normalizePreviewKind(metaPreview?.kind);
|
|
962
|
+
if (explicitKind) return explicitKind;
|
|
963
|
+
|
|
964
|
+
const jsdocKind = inferPreviewKindFromText(jsdoc.description);
|
|
965
|
+
if (jsdocKind) return jsdocKind;
|
|
966
|
+
|
|
967
|
+
return inferPreviewKindFromStructure(componentName, props, events, slots);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function isBooleanType(type = '') {
|
|
971
|
+
return type.toLowerCase().includes('boolean');
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function isNumberType(type = '') {
|
|
975
|
+
return type.includes('number') || type.includes('Number');
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function isArrayType(type = '') {
|
|
979
|
+
return type.includes('array') || type.includes('Array') || type.endsWith('[]');
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function isObjectType(type = '') {
|
|
983
|
+
return type.includes('object') || type.includes('Object') || /\{[\s\S]*\}/.test(type);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function getPropByName(props, name) {
|
|
987
|
+
return props.find((prop) => prop.name === name);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function getFirstOptionValue(previewProps) {
|
|
991
|
+
const options = previewProps.items || previewProps.options || previewProps.range || previewProps.tabs || previewProps.list;
|
|
992
|
+
if (!Array.isArray(options) || !options.length) return undefined;
|
|
993
|
+
|
|
994
|
+
const first = options[0];
|
|
995
|
+
if (first && typeof first === 'object') return first.value ?? first.id ?? first.key ?? first.name ?? first.label;
|
|
996
|
+
|
|
997
|
+
return first;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function createListMock(componentName, propName) {
|
|
1001
|
+
if (/timeline/.test(componentName)) {
|
|
1002
|
+
return [
|
|
1003
|
+
{ label: '提交订单', time: '10:00' },
|
|
1004
|
+
{ label: '商家确认', time: '10:30' },
|
|
1005
|
+
];
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (/steps/.test(componentName)) {
|
|
1009
|
+
return [
|
|
1010
|
+
{ name: '提交订单', status: 'done' },
|
|
1011
|
+
{ name: '支付成功', status: 'active' },
|
|
1012
|
+
{ name: '等待发货', status: 'todo' },
|
|
1013
|
+
];
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (/tree/.test(componentName)) {
|
|
1017
|
+
return [
|
|
1018
|
+
{
|
|
1019
|
+
label: '一级节点',
|
|
1020
|
+
value: '1',
|
|
1021
|
+
children: [{ label: '二级节点', value: '1-1' }],
|
|
1022
|
+
},
|
|
1023
|
+
];
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (/radar/.test(componentName)) {
|
|
1027
|
+
return [
|
|
1028
|
+
{ n: '服务', p: 90 },
|
|
1029
|
+
{ n: '物流', p: 80 },
|
|
1030
|
+
{ n: '质量', p: 88 },
|
|
1031
|
+
];
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
if (propName === 'tabs') return ['精选', '最新', '销量'];
|
|
1035
|
+
if (propName === 'range') return ['男', '女', '保密'];
|
|
1036
|
+
if (propName === 'arr') {
|
|
1037
|
+
return [
|
|
1038
|
+
{ src: '/static/icon/avatar.svg', title: '示例图片' },
|
|
1039
|
+
{ src: '/static/icon/tab-home.png', title: '示例图片' },
|
|
1040
|
+
];
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
return [
|
|
1044
|
+
{ label: '选项一', value: '1' },
|
|
1045
|
+
{ label: '选项二', value: '2' },
|
|
1046
|
+
];
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function createObjectMock(propName) {
|
|
1050
|
+
if (propName === 'value') return { label: '示例', value: '1' };
|
|
1051
|
+
|
|
1052
|
+
return { label: '示例', value: '1' };
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function createNamedPropMock(componentName, prop, kind, previewProps) {
|
|
1056
|
+
const { name, type } = prop;
|
|
1057
|
+
|
|
1058
|
+
if (name === 'modelValue') {
|
|
1059
|
+
const firstOptionValue = getFirstOptionValue(previewProps);
|
|
1060
|
+
if (firstOptionValue !== undefined) return firstOptionValue;
|
|
1061
|
+
if (kind === 'overlay' && isBooleanType(type)) return false;
|
|
1062
|
+
if (isBooleanType(type)) return true;
|
|
1063
|
+
if (isNumberType(type)) return 1;
|
|
1064
|
+
|
|
1065
|
+
return '1';
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (name === 'checked') return true;
|
|
1069
|
+
if (name === 'active') return 0;
|
|
1070
|
+
if (name === 'percent') return 60;
|
|
1071
|
+
if (name === 'num' || name === 'count') return 3;
|
|
1072
|
+
if (name === 'phone') return '13800138000';
|
|
1073
|
+
if (name === 'text') {
|
|
1074
|
+
return kind === 'measure' || /marquee/.test(componentName)
|
|
1075
|
+
? '这是一段足够长的公告文字,用于触发跑马灯滚动预览效果'
|
|
1076
|
+
: '示例文本';
|
|
1077
|
+
}
|
|
1078
|
+
if (name === 'title') return '标题';
|
|
1079
|
+
if (name === 'placeholder') return '请选择';
|
|
1080
|
+
if (name === 'src' || name === 'poster' || name === 'icon') return '/static/icon/avatar.svg';
|
|
1081
|
+
if (name === 'code') return 'preview-code';
|
|
1082
|
+
if (name === 'label') return 'label';
|
|
1083
|
+
if (name === 'time') return 'time';
|
|
1084
|
+
if (name === 'valueKey') return 'value';
|
|
1085
|
+
if (name === 'labelKey') return 'label';
|
|
1086
|
+
|
|
1087
|
+
if (['items', 'list', 'range', 'tabs', 'arr', 'options'].includes(name) || isArrayType(type)) {
|
|
1088
|
+
return createListMock(componentName, name);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
if (isObjectType(type)) return createObjectMock(name);
|
|
1092
|
+
if (isBooleanType(type)) return false;
|
|
1093
|
+
if (isNumberType(type)) return 1;
|
|
1094
|
+
|
|
1095
|
+
return '';
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function shouldAutoMockProp(prop, kind) {
|
|
1099
|
+
if (kind === 'overlay') return ['modelValue', 'title'].includes(prop.name);
|
|
1100
|
+
if (kind === 'measure') return ['text', 'tabs', 'arr', 'list'].includes(prop.name);
|
|
1101
|
+
if (kind === 'data-list') return ['items', 'list', 'range', 'tabs', 'arr', 'options', 'active', 'modelValue'].includes(prop.name);
|
|
1102
|
+
if (kind === 'form') return ['items', 'range', 'options', 'tabs', 'modelValue'].includes(prop.name);
|
|
1103
|
+
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function applyPreviewMocks(componentName, props, preview, kind) {
|
|
1108
|
+
const previewProps = { ...(preview.props || {}) };
|
|
1109
|
+
|
|
1110
|
+
for (const prop of props) {
|
|
1111
|
+
if (Object.prototype.hasOwnProperty.call(previewProps, prop.name)) continue;
|
|
1112
|
+
if (!shouldAutoMockProp(prop, kind)) continue;
|
|
1113
|
+
|
|
1114
|
+
const value = createNamedPropMock(componentName, prop, kind, previewProps);
|
|
1115
|
+
if (value !== '' && value !== undefined) previewProps[prop.name] = value;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
if (kind === 'overlay') {
|
|
1119
|
+
const modelProp = getPropByName(props, 'modelValue');
|
|
1120
|
+
if (modelProp && isBooleanType(modelProp.type)) previewProps.modelValue = false;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
const previewSlots = { ...(preview.slots || {}) };
|
|
1124
|
+
if (!previewSlots.default && kind === 'overlay') previewSlots.default = '这里是弹层内容';
|
|
1125
|
+
if (!previewSlots.default && props.length === 0) previewSlots.default = '内容';
|
|
1126
|
+
|
|
1127
|
+
return {
|
|
1128
|
+
...preview,
|
|
1129
|
+
props: previewProps,
|
|
1130
|
+
slots: previewSlots,
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function inferPreviewFromExamples(componentName, props, examples, kind) {
|
|
1135
|
+
const escapedName = escapeRegExp(componentName);
|
|
1136
|
+
const propMap = new Map(props.map((prop) => [prop.name.toLowerCase(), prop]));
|
|
1137
|
+
|
|
1138
|
+
for (const example of examples) {
|
|
1139
|
+
const code = example.code || '';
|
|
1140
|
+
const bindings = parseExampleBindings(code);
|
|
1141
|
+
const closedMatch = code.match(new RegExp(`<${escapedName}\\b([^>]*)>([\\s\\S]*?)<\\/${escapedName}>`, 'i'));
|
|
1142
|
+
const selfClosingMatch = closedMatch ? null : code.match(new RegExp(`<${escapedName}\\b([^>]*)\\/\\s*>`, 'i'));
|
|
1143
|
+
const match = closedMatch || selfClosingMatch;
|
|
1144
|
+
if (!match) continue;
|
|
1145
|
+
|
|
1146
|
+
const attrs = match[1] || '';
|
|
1147
|
+
const propsFromExample = {};
|
|
1148
|
+
const slots = {};
|
|
1149
|
+
const preview = {
|
|
1150
|
+
source: 'example',
|
|
1151
|
+
code: example.code,
|
|
1152
|
+
props: propsFromExample,
|
|
1153
|
+
slots,
|
|
1154
|
+
};
|
|
1155
|
+
const attrPattern = /([:@]?[\w:$-]+|v-model(?::[\w-]+)?)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
1156
|
+
let attrMatch;
|
|
1157
|
+
|
|
1158
|
+
while ((attrMatch = attrPattern.exec(attrs))) {
|
|
1159
|
+
const rawName = attrMatch[1];
|
|
1160
|
+
const rawValue = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4];
|
|
1161
|
+
if (!rawName || rawName.startsWith('@')) continue;
|
|
1162
|
+
|
|
1163
|
+
if (rawName.startsWith('v-model')) {
|
|
1164
|
+
const [, modelName] = rawName.split(':');
|
|
1165
|
+
const propName = modelName ? kebabToCamel(modelName) : 'modelValue';
|
|
1166
|
+
const prop = propMap.get(propName.toLowerCase());
|
|
1167
|
+
preview.vModel = {
|
|
1168
|
+
prop: propName,
|
|
1169
|
+
variable: rawValue || 'value',
|
|
1170
|
+
};
|
|
1171
|
+
if (prop?.default !== null && prop?.default !== undefined && prop.default !== '') {
|
|
1172
|
+
propsFromExample[propName] = prop.default;
|
|
1173
|
+
}
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const isDynamic = rawName.startsWith(':');
|
|
1178
|
+
const attrName = isDynamic ? rawName.slice(1) : rawName;
|
|
1179
|
+
const propName = kebabToCamel(attrName);
|
|
1180
|
+
const prop = propMap.get(propName.toLowerCase());
|
|
1181
|
+
if (!prop) continue;
|
|
1182
|
+
|
|
1183
|
+
let value = isDynamic ? parseExampleExpression(rawValue || '', bindings) : coerceStaticExampleValue(rawValue, prop);
|
|
1184
|
+
if (isDynamic && value === undefined) {
|
|
1185
|
+
value = createNamedPropMock(componentName, prop, kind, propsFromExample);
|
|
1186
|
+
}
|
|
1187
|
+
if (value !== undefined && value !== '') propsFromExample[prop.name] = value;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
if (closedMatch) {
|
|
1191
|
+
const defaultSlot = normalizeSlotContent(closedMatch[2] || '');
|
|
1192
|
+
if (defaultSlot) slots.default = defaultSlot;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
return preview;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
return null;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function normalizePreviewConfig(preview) {
|
|
1202
|
+
if (!preview || typeof preview !== 'object') return null;
|
|
1203
|
+
|
|
1204
|
+
return {
|
|
1205
|
+
source: preview.source || 'meta',
|
|
1206
|
+
kind: normalizePreviewKind(preview.kind),
|
|
1207
|
+
code: preview.code,
|
|
1208
|
+
props: preview.props && typeof preview.props === 'object' ? preview.props : {},
|
|
1209
|
+
slots: preview.slots && typeof preview.slots === 'object' ? preview.slots : preview.slot ? { default: preview.slot } : {},
|
|
1210
|
+
vModel: preview.vModel,
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function buildPreview(componentName, props, events, slots, examples, jsdoc, metaPreview) {
|
|
1215
|
+
const kind = inferPreviewKind(componentName, props, events, slots, jsdoc, metaPreview);
|
|
1216
|
+
const preview = normalizePreviewConfig(metaPreview) || inferPreviewFromExamples(componentName, props, examples, kind) || {
|
|
1217
|
+
source: 'inferred',
|
|
1218
|
+
props: {},
|
|
1219
|
+
slots: {},
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
return {
|
|
1223
|
+
...applyPreviewMocks(componentName, props, preview, kind),
|
|
1224
|
+
kind: preview.kind || kind,
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
|
|
690
1228
|
function findAllComponentDirs() {
|
|
691
1229
|
if (!fs.existsSync(componentRoot)) return [];
|
|
692
1230
|
|
|
@@ -758,69 +1296,10 @@ function buildUsageHints(componentName, title, props, events, slots) {
|
|
|
758
1296
|
return { useWhen, avoidWhen, notes };
|
|
759
1297
|
}
|
|
760
1298
|
|
|
761
|
-
function buildSamplePropValue(prop) {
|
|
762
|
-
if (prop.default !== null && prop.default !== undefined && prop.default !== '') return prop.default;
|
|
763
|
-
if (prop.values?.length) return prop.values[0];
|
|
764
|
-
|
|
765
|
-
const type = prop.type || '';
|
|
766
|
-
const name = prop.name.toLowerCase();
|
|
767
|
-
|
|
768
|
-
if (type.includes('boolean')) return false;
|
|
769
|
-
if (type.includes('number') || type.includes('Number')) {
|
|
770
|
-
if (name.includes('percent')) return 60;
|
|
771
|
-
if (name.includes('max')) return 10;
|
|
772
|
-
if (name.includes('min')) return 0;
|
|
773
|
-
if (name.includes('size')) return 32;
|
|
774
|
-
if (name === 'w' || name.includes('width')) return 240;
|
|
775
|
-
if (name === 'h' || name.includes('height')) return 80;
|
|
776
|
-
return 1;
|
|
777
|
-
}
|
|
778
|
-
if (type.includes('Array') || type.includes('array') || ['items', 'list', 'range', 'tabs', 'arr'].includes(prop.name)) {
|
|
779
|
-
return [
|
|
780
|
-
{ label: '选项一', value: 'one' },
|
|
781
|
-
{ label: '选项二', value: 'two' },
|
|
782
|
-
];
|
|
783
|
-
}
|
|
784
|
-
if (type.includes('Object') || type.includes('object')) return {};
|
|
785
|
-
if (name.includes('placeholder')) return '请输入';
|
|
786
|
-
if (name.includes('title')) return '标题';
|
|
787
|
-
if (name.includes('label')) return '标签';
|
|
788
|
-
if (name.includes('text') || name.includes('content')) return '示例内容';
|
|
789
|
-
if (name.includes('phone')) return '13800000000';
|
|
790
|
-
if (name === 'icon') return '/static/icon/avatar.svg';
|
|
791
|
-
if (name === 'src' || name.includes('poster')) return '';
|
|
792
|
-
|
|
793
|
-
return '';
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
function renderExampleFromProps(componentName, props) {
|
|
797
|
-
const importantProps = props
|
|
798
|
-
.filter((prop) => prop.required || ['modelValue', 'title', 'text', 'label', 'items', 'range', 'tabs', 'src', 'icon', 'type', 'size'].includes(prop.name))
|
|
799
|
-
.slice(0, 5);
|
|
800
|
-
const attrs = importantProps
|
|
801
|
-
.map((prop) => {
|
|
802
|
-
if (prop.name === 'modelValue') return 'v-model="value"';
|
|
803
|
-
const value = buildSamplePropValue(prop);
|
|
804
|
-
if (typeof value === 'boolean') return value ? prop.name : `:${prop.name}="false"`;
|
|
805
|
-
if (typeof value === 'number') return `:${prop.name}="${value}"`;
|
|
806
|
-
if (Array.isArray(value) || (value && typeof value === 'object')) return `:${prop.name}="${prop.name}"`;
|
|
807
|
-
if (!value) return '';
|
|
808
|
-
|
|
809
|
-
return `${prop.name}="${value}"`;
|
|
810
|
-
})
|
|
811
|
-
.filter(Boolean)
|
|
812
|
-
.join(' ');
|
|
813
|
-
const attrText = attrs ? ` ${attrs}` : '';
|
|
814
|
-
|
|
815
|
-
return `<${componentName}${attrText}>示例内容</${componentName}>`;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
1299
|
function inferComponentInfo(componentDir) {
|
|
819
1300
|
const componentName = path.basename(componentDir);
|
|
820
1301
|
const componentFile = findComponentFile(componentDir);
|
|
821
|
-
const
|
|
822
|
-
const script = extractSfcBlock(sfcSource, 'script', (attrs) => attrs.includes('setup'));
|
|
823
|
-
const template = extractSfcBlock(sfcSource, 'template');
|
|
1302
|
+
const { script, template } = readSfcBlocks(componentFile);
|
|
824
1303
|
const jsdoc = parseJsdoc(script);
|
|
825
1304
|
const aliases = collectTypeAliases(componentDir);
|
|
826
1305
|
const props = extractProps(script, componentFile, aliases, jsdoc);
|
|
@@ -842,7 +1321,6 @@ function inferInitialMeta(componentDir) {
|
|
|
842
1321
|
const examples = normalizeExamples([], jsdoc.examples);
|
|
843
1322
|
const title = jsdoc.description?.split('\n')[0]?.trim() || toTitleCase(componentName);
|
|
844
1323
|
const usageHints = buildUsageHints(componentName, title, props, events, slots);
|
|
845
|
-
const exampleCode = renderExampleFromProps(componentName, props);
|
|
846
1324
|
|
|
847
1325
|
return {
|
|
848
1326
|
title,
|
|
@@ -852,15 +1330,7 @@ function inferInitialMeta(componentDir) {
|
|
|
852
1330
|
avoidWhen: usageHints.avoidWhen,
|
|
853
1331
|
related: [],
|
|
854
1332
|
notes: usageHints.notes,
|
|
855
|
-
examples
|
|
856
|
-
? examples
|
|
857
|
-
: [
|
|
858
|
-
{
|
|
859
|
-
title: '基础用法',
|
|
860
|
-
description: `自动根据 ${componentName} 的 props 生成,可按业务语义继续调整。`,
|
|
861
|
-
code: exampleCode,
|
|
862
|
-
},
|
|
863
|
-
],
|
|
1333
|
+
examples,
|
|
864
1334
|
};
|
|
865
1335
|
}
|
|
866
1336
|
|
|
@@ -881,6 +1351,10 @@ function mergeMeta(existingMeta, inferredMeta) {
|
|
|
881
1351
|
}
|
|
882
1352
|
}
|
|
883
1353
|
|
|
1354
|
+
if (Array.isArray(merged.examples)) {
|
|
1355
|
+
merged.examples = normalizeExamples(merged.examples, inferredMeta.examples || []);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
884
1358
|
return merged;
|
|
885
1359
|
}
|
|
886
1360
|
|
|
@@ -892,7 +1366,30 @@ function isAutoGeneratedDocPage(filePath) {
|
|
|
892
1366
|
}
|
|
893
1367
|
|
|
894
1368
|
function renderGeneratedControlStyles() {
|
|
895
|
-
return `.
|
|
1369
|
+
return `.preview-actions {
|
|
1370
|
+
display: flex;
|
|
1371
|
+
flex-direction: row;
|
|
1372
|
+
justify-content: flex-start;
|
|
1373
|
+
width: 100%;
|
|
1374
|
+
margin-bottom: 16rpx;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
.preview-open-btn {
|
|
1378
|
+
box-sizing: border-box;
|
|
1379
|
+
display: inline-flex;
|
|
1380
|
+
align-items: center;
|
|
1381
|
+
justify-content: center;
|
|
1382
|
+
min-height: 64rpx;
|
|
1383
|
+
padding: 0 24rpx;
|
|
1384
|
+
font-size: 26rpx;
|
|
1385
|
+
font-weight: 600;
|
|
1386
|
+
line-height: 1.35;
|
|
1387
|
+
color: #fff;
|
|
1388
|
+
background: #2563eb;
|
|
1389
|
+
border-radius: 8rpx;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
.control-panel,
|
|
896
1393
|
.control-list {
|
|
897
1394
|
box-sizing: border-box;
|
|
898
1395
|
display: flex;
|
|
@@ -1052,9 +1549,14 @@ import doc from './data.json';
|
|
|
1052
1549
|
|
|
1053
1550
|
const {
|
|
1054
1551
|
propControls,
|
|
1055
|
-
|
|
1552
|
+
renderedPreviewProps,
|
|
1553
|
+
previewKey,
|
|
1056
1554
|
slotText,
|
|
1057
1555
|
codeSnippet,
|
|
1556
|
+
isOverlayPreview,
|
|
1557
|
+
overlayTriggerText,
|
|
1558
|
+
openOverlayPreview,
|
|
1559
|
+
handlePreviewModelValueUpdate,
|
|
1058
1560
|
isBooleanProp,
|
|
1059
1561
|
isNumberProp,
|
|
1060
1562
|
isArrayProp,
|
|
@@ -1070,7 +1572,11 @@ const {
|
|
|
1070
1572
|
<${componentDocPageName} :doc="doc" :show-controls="doc.props.length > 0">
|
|
1071
1573
|
<template #preview>
|
|
1072
1574
|
<view class="preview-frame">
|
|
1073
|
-
|
|
1575
|
+
<view v-if="isOverlayPreview" class="preview-actions">
|
|
1576
|
+
<text class="preview-open-btn" @click="openOverlayPreview">{{ overlayTriggerText }}</text>
|
|
1577
|
+
</view>
|
|
1578
|
+
|
|
1579
|
+
<${importName} :key="previewKey" v-bind="renderedPreviewProps" @update:modelValue="handlePreviewModelValueUpdate('modelValue', $event)">
|
|
1074
1580
|
{{ slotText }}
|
|
1075
1581
|
</${importName}>
|
|
1076
1582
|
</view>
|
|
@@ -1271,15 +1777,15 @@ function collectComponentDoc(componentDir) {
|
|
|
1271
1777
|
if (!componentFile) return null;
|
|
1272
1778
|
|
|
1273
1779
|
const componentName = path.basename(componentDir);
|
|
1274
|
-
const
|
|
1275
|
-
const script = extractSfcBlock(sfcSource, 'script', (attrs) => attrs.includes('setup'));
|
|
1276
|
-
const template = extractSfcBlock(sfcSource, 'template');
|
|
1780
|
+
const { script, template } = readSfcBlocks(componentFile);
|
|
1277
1781
|
const meta = readJson(getMetaFile(componentDir));
|
|
1278
1782
|
const jsdoc = parseJsdoc(script);
|
|
1279
1783
|
const aliases = collectTypeAliases(componentDir);
|
|
1280
1784
|
const props = extractProps(script, componentFile, aliases, jsdoc);
|
|
1281
1785
|
const events = extractEvents(script, componentFile, jsdoc);
|
|
1282
1786
|
const slots = extractSlots(template);
|
|
1787
|
+
const examples = normalizeExamples(meta.examples, jsdoc.examples);
|
|
1788
|
+
const preview = buildPreview(componentName, props, events, slots, examples, jsdoc, meta.preview);
|
|
1283
1789
|
|
|
1284
1790
|
const component = {
|
|
1285
1791
|
name: componentName,
|
|
@@ -1291,7 +1797,8 @@ function collectComponentDoc(componentDir) {
|
|
|
1291
1797
|
props,
|
|
1292
1798
|
events,
|
|
1293
1799
|
slots,
|
|
1294
|
-
|
|
1800
|
+
preview,
|
|
1801
|
+
examples,
|
|
1295
1802
|
useWhen: meta.useWhen || [],
|
|
1296
1803
|
avoidWhen: meta.avoidWhen || [],
|
|
1297
1804
|
notes: meta.notes || [],
|