From d729cd55620eee58a16b5aed3e1f706f5f8f9b2a Mon Sep 17 00:00:00 2001
From: sakurayinfei <970412446@qq.com>
Date: Fri, 31 Oct 2025 10:26:36 +0800
Subject: [PATCH 1/3] =?UTF-8?q?fix(doc):=20=E4=BC=98=E5=8C=96api=E6=96=87?=
=?UTF-8?q?=E6=A1=A3=E7=94=9F=E6=88=90=E8=84=9A=E6=9C=AC=EF=BC=8C=E5=AE=8C?=
=?UTF-8?q?=E5=96=84=E5=AF=B9defineSlots=E5=AE=8F=E5=87=BD=E6=95=B0?=
=?UTF-8?q?=E7=9A=84=E8=A7=A3=E6=9E=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/docs/scripts/generateApi.ts | 64 +++++++------
packages/docs/scripts/parseDefineSlots.ts | 108 +++++++++++++---------
2 files changed, 95 insertions(+), 77 deletions(-)
diff --git a/packages/docs/scripts/generateApi.ts b/packages/docs/scripts/generateApi.ts
index bf10c44f8..da8617ab7 100644
--- a/packages/docs/scripts/generateApi.ts
+++ b/packages/docs/scripts/generateApi.ts
@@ -4,6 +4,7 @@ import fsp from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { type ComponentMeta, createChecker } from 'vue-component-meta';
import { parseMulti } from 'vue-docgen-api';
+import { parse } from '@vue/compiler-sfc';
import parseDefineSlots from './parseDefineSlots';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
@@ -36,6 +37,9 @@ function escapeTableValue(value?: string) {
const CELL_ESCAPE_REPLACE_RE = /[<>"'|\r\n]/g;
return value ? value.replace(CELL_ESCAPE_REPLACE_RE, replaceCellChar) : '';
}
+function escapeInlineCode(value: string) {
+ return value.replace(/`/g, '\\`');
+}
function cleanTableData(table: any[][]) {
// 清理表格数据
table.forEach((row) => {
@@ -82,7 +86,7 @@ async function applyTempFixForEventDescriptions(filename: string, componentMeta:
const hasEvents = componentMeta.events.length;
if (!hasEvents) {
- return componentMeta;
+ return;
}
try {
@@ -99,7 +103,6 @@ async function applyTempFixForEventDescriptions(filename: string, componentMeta:
} catch {
// noop
}
- return componentMeta;
}
/**
* 补充 vue-component-meta 未能解析 defineSlots 的描述和签名
@@ -108,44 +111,35 @@ async function applyTempFixForEventDescriptions(filename: string, componentMeta:
* @returns 新的组件元数据
*/
async function applyTempFixForSlot(filename: string, componentMeta: ComponentMeta) {
- const slotReg = /defineSlots<{[^}]+}>\(\)/;
- const slotMatch = slotReg.exec(await fsp.readFile(filename, 'utf-8'));
- if (!slotMatch) {
- return componentMeta;
+ const fileContent = await fsp.readFile(filename, 'utf-8');
+ const setupScript = parse(fileContent).descriptor.scriptSetup?.content;
+ if (!setupScript) {
+ return;
}
- const slotMeta = parseDefineSlots(slotMatch[0]);
- const slots = componentMeta.slots.map((slot) => {
- const parsedSlot = slotMeta.find((s) => s.name === slot.name);
- if (parsedSlot) {
- slot.description = parsedSlot.docs.description;
- slot.type = parsedSlot.type;
+ const slotMeta = parseDefineSlots(setupScript);
+
+ slotMeta.forEach((slot) => {
+ const meta = componentMeta.slots.find((item) => item.name === slot.name);
+ if (meta) {
+ meta.description = slot.description;
+ meta.type = slot.type;
+ } else {
+ componentMeta.slots.push(slot);
}
- return slot;
});
- return {
- ...componentMeta,
- slots,
- };
}
-const terminalWidth = process.stdout.columns || 80;
-const progressBarLength = Math.min(Math.floor(terminalWidth / 4), 30);
const pathReg = /\/(O.*)\.vue/;
const tagTypes = {
deprecated: '(warning)',
};
const exposeDesReg = /^\s*expose:([\s\S]+)/;
-glob('*/O*.vue', { cwd: srcDir, posix: true }).then((files) => {
- files.forEach(async (file, index) => {
+console.time('GenerateApi done');
+const promise = glob('*/O*.vue', { cwd: srcDir, posix: true }).then((files) => {
+ const promises = files.map(async (file) => {
const fullPath = join(srcDir, file);
// 解析Vue组件Api元数据
const meta = checker.getComponentMeta(fullPath);
- const completedCount = Math.floor(((index + 1) / files.length) * progressBarLength);
- const restCount = progressBarLength - completedCount;
- const progressBar = `${'█'.repeat(completedCount)}${' '.repeat(restCount)}`;
- const percent = (((index + 1) / files.length) * 100).toFixed(0);
- // 输出进度条
- process.stdout.write(`\r[${progressBar}] ${percent}%`);
await applyTempFixForEventDescriptions(fullPath, meta);
await applyTempFixForSlot(fullPath, meta);
const pathMath = file.match(pathReg);
@@ -162,8 +156,8 @@ glob('*/O*.vue', { cwd: srcDir, posix: true }).then((files) => {
const excludeTag = ['default', 'zh-CN', 'en-US'];
let propsData = selfProps.map((prop) => {
return [
- prop.name,
- prop.type,
+ escapeInlineCode(prop.name),
+ escapeInlineCode(prop.type),
prop.default || prop.tags.find((tag) => tag.name === 'default')?.text || '',
prop.required ? '🗸' : '',
prop.tags.find((tag) => tag.name === lang)?.text || prop.description || '',
@@ -186,8 +180,8 @@ glob('*/O*.vue', { cwd: srcDir, posix: true }).then((files) => {
const excludeTag = ['zh-CN', 'en-US'];
let eventsData = meta.events.map((event) => {
return [
- event.name,
- event.signature,
+ escapeInlineCode(event.name),
+ escapeInlineCode(event.signature),
event.tags.find((tag) => tag.name === lang)?.text || event.description || '',
event.tags
.filter((tag) => !excludeTag.includes(tag.name))
@@ -206,7 +200,7 @@ glob('*/O*.vue', { cwd: srcDir, posix: true }).then((files) => {
'en-US': ['Slot Name', 'Signature', 'Description'],
};
let slotsData = meta.slots.map((slot) => {
- return [slot.name, slot.type, slot.description];
+ return [escapeInlineCode(slot.name), escapeInlineCode(slot.type), slot.description];
});
slotsData.unshift(tableHeader[lang]);
slotsData = cleanTableData(slotsData);
@@ -220,7 +214,7 @@ glob('*/O*.vue', { cwd: srcDir, posix: true }).then((files) => {
'en-US': ['Name', 'Type', 'Description'],
};
let exposeData = selfExposed.map((expose) => {
- return [expose.name, expose.type, (expose.description.match(exposeDesReg)?.[1] || '').trim()];
+ return [escapeInlineCode(expose.name), escapeInlineCode(expose.type), (expose.description.match(exposeDesReg)?.[1] || '').trim()];
});
exposeData.unshift(tableHeader[lang]);
exposeData = cleanTableData(exposeData);
@@ -229,4 +223,8 @@ glob('*/O*.vue', { cwd: srcDir, posix: true }).then((files) => {
await fsp.mkdir(dirname(apiMdPath), { recursive: true }).then(() => fsp.writeFile(apiMdPath, mdContent, { encoding: 'utf-8' }));
}
});
+ return Promise.all(promises);
+});
+promise.finally(() => {
+ console.timeEnd('GenerateApi done');
});
diff --git a/packages/docs/scripts/parseDefineSlots.ts b/packages/docs/scripts/parseDefineSlots.ts
index 0698e4b95..e739bea46 100644
--- a/packages/docs/scripts/parseDefineSlots.ts
+++ b/packages/docs/scripts/parseDefineSlots.ts
@@ -3,13 +3,69 @@ import ts from 'typescript';
interface SlotDefinition {
name: string;
type: string;
- docs: {
- description: string;
- tags: {
- name: string;
- text: string;
- }[];
- };
+ description: string;
+ tags: {
+ name: string;
+ text: string;
+ }[];
+ schema: string;
+ declarations: Array<{ file: string; range: [number, number] }>;
+}
+
+function parseDoc(jsDoc: ts.Node[] | undefined, sourceFile: ts.SourceFile) {
+ const docs = { description: '', tags: [] as { name: string; text: string }[] };
+ if (!jsDoc) {
+ return docs;
+ }
+ docs.description = jsDoc
+ .map((doc) => {
+ let comment = doc.getText(sourceFile);
+ // 清理注释格式
+ comment = comment
+ .replace(/\/\*\*|\*\//g, '')
+ .replace(/^\s*\*\s?/gm, '')
+ .replace(/@([a-zA-Z]+)\s+(.*)/gm, (_: string, paramName: string, paramValue: string) => {
+ docs.tags.push({
+ name: paramName,
+ text: paramValue || '',
+ });
+ return '';
+ })
+ .trim();
+ return comment;
+ })
+ .join('\n');
+ return docs;
+}
+function parseTypeArg(typeArg: ts.TypeNode, sourceFile: ts.SourceFile, slots: SlotDefinition[]) {
+ if (ts.isTypeLiteralNode(typeArg)) {
+ for (const member of typeArg.members) {
+ let name = '';
+ let type = '';
+ let isParsed = false;
+ if (ts.isPropertySignature(member)) {
+ // 属性签名 propertyName: type
+ name = member.name.getText(sourceFile);
+ type = member.type?.getText(sourceFile) || 'any';
+ isParsed = true;
+ } else if (ts.isMethodSignature(member)) {
+ // 方法签名 methodName(paramName: type): type
+ name = member.name.getText(sourceFile);
+ type = member.getText(sourceFile) || 'any';
+ isParsed = true;
+ } else if (ts.isIndexSignatureDeclaration(member)) {
+ // 索引签名 [key: keyType]: type
+ const param = member.parameters[0];
+ name = param.type?.getText(sourceFile) || param.getText(sourceFile);
+ type = member.getText(sourceFile) || 'any';
+ isParsed = true;
+ }
+ if (isParsed) {
+ const docs = parseDoc((member as any).jsDoc, sourceFile);
+ slots.push({ name, type, description: docs.description, tags: docs.tags, schema: '', declarations: [] });
+ }
+ }
+ }
}
export default function parseDefineSlots(code: string): SlotDefinition[] {
@@ -21,43 +77,7 @@ export default function parseDefineSlots(code: string): SlotDefinition[] {
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'defineSlots' && node.typeArguments?.length) {
const typeArg = node.typeArguments[0];
- // 处理内联对象类型
- if (ts.isTypeLiteralNode(typeArg)) {
- for (const member of typeArg.members) {
- if (ts.isPropertySignature(member)) {
- const name = member.name.getText(sourceFile);
- const type = member.type?.getText(sourceFile) || 'any';
- const docs = {
- description: '',
- tags: [],
- };
-
- // 提取文档注释
- if ((member as any).jsDoc?.length) {
- docs.description = (member as any).jsDoc
- .map((doc) => {
- let comment = doc.getText(sourceFile);
- // 清理注释格式
- comment = comment
- .replace(/\/\*\*|\*\//g, '')
- .replace(/^\s*\*\s?/gm, '')
- .replace(/@([a-zA-Z]+)\s+(.*)/gm, (_: string, paramName: string, paramValue: string) => {
- docs.tags.push({
- name: paramName,
- value: paramValue || '',
- });
- return '';
- })
- .trim();
- return comment;
- })
- .join('\n');
- }
-
- slots.push({ name, type, docs });
- }
- }
- }
+ parseTypeArg(typeArg, sourceFile, slots);
}
ts.forEachChild(node, visit);
}
--
Gitee
From 72af615a6ed68c556e4bbba10991432f6fe196f6 Mon Sep 17 00:00:00 2001
From: sakurayinfei <970412446@qq.com>
Date: Fri, 31 Oct 2025 10:27:03 +0800
Subject: [PATCH 2/3] =?UTF-8?q?doc(table):=20=E5=AE=8C=E5=96=84=E6=96=87?=
=?UTF-8?q?=E6=A1=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../{tableUsage.ts => TableUsage.vue} | 44 +++++++++----------
.../src/table/__docs__/index.en-US.md | 8 ++--
.../src/table/__docs__/index.zh-CN.md | 6 +--
packages/opendesign/src/table/types.ts | 25 +++++++----
4 files changed, 43 insertions(+), 40 deletions(-)
rename packages/opendesign/src/table/__docs__/__case__/{tableUsage.ts => TableUsage.vue} (52%)
diff --git a/packages/opendesign/src/table/__docs__/__case__/tableUsage.ts b/packages/opendesign/src/table/__docs__/__case__/TableUsage.vue
similarity index 52%
rename from packages/opendesign/src/table/__docs__/__case__/tableUsage.ts
rename to packages/opendesign/src/table/__docs__/__case__/TableUsage.vue
index ab5df0b11..7ff7b0c61 100644
--- a/packages/opendesign/src/table/__docs__/__case__/tableUsage.ts
+++ b/packages/opendesign/src/table/__docs__/__case__/TableUsage.vue
@@ -1,55 +1,53 @@
+
+
+
+### 使用
+
+
+
+### Usage
+
+
diff --git a/packages/opendesign/src/table/__docs__/index.en-US.md b/packages/opendesign/src/table/__docs__/index.en-US.md
index 52dbba7fd..d2da006ae 100644
--- a/packages/opendesign/src/table/__docs__/index.en-US.md
+++ b/packages/opendesign/src/table/__docs__/index.en-US.md
@@ -5,18 +5,16 @@ kind: container
# Data Table
-## Usage
-
-
-## Cases
+## Demo
+
-## Api
+## API
\ No newline at end of file
diff --git a/packages/opendesign/src/table/__docs__/index.zh-CN.md b/packages/opendesign/src/table/__docs__/index.zh-CN.md
index 604268395..c46bb7bb5 100644
--- a/packages/opendesign/src/table/__docs__/index.zh-CN.md
+++ b/packages/opendesign/src/table/__docs__/index.zh-CN.md
@@ -5,18 +5,16 @@ kind: container
# 数据表格
-## 使用
-
-
## 示例
+
-## Api
+## API
\ No newline at end of file
diff --git a/packages/opendesign/src/table/types.ts b/packages/opendesign/src/table/types.ts
index e762d0311..3bb5fa73b 100644
--- a/packages/opendesign/src/table/types.ts
+++ b/packages/opendesign/src/table/types.ts
@@ -31,50 +31,59 @@ export type TableBorderT = (typeof TableBorderTypes)[number];
export const tableProps = {
/**
- * 表头内容 TableColumnT[] | string[]
+ * @zh-CN 表头数据
+ * @en-US Table header data
*/
columns: {
type: Array as PropType,
},
/**
- * 表格数据 TableRowT[]
+ * @zh-CN 表格数据
+ * @en-US Table data
*/
data: {
type: Array as PropType,
},
/**
- * 是否显示边框 TableBorderT
+ * @zh-CN 表格边框
+ * @en-US Table border
+ * @default 'row'
*/
border: {
type: String as PropType,
default: 'row',
},
/**
- * 是否小表格
+ * @zh-CN 是否使用小尺寸
+ * @en-US Use small size
*/
small: {
type: Boolean,
},
/**
- * 处理单元格合并(表体部分,不包含表头) CellSpanT
+ * @zh-CN 单元格合并(不含表头)
+ * @en-US Cell merge (excluding header)
*/
cellSpan: {
type: Function as PropType,
},
/**
- * 空数据提示文本
+ * @zh-CN 空数据提示文本
+ * @en-US Empty data prompt text
*/
emptyLabel: {
type: String,
},
/**
- * 是否正在加载
+ * @zh-CN 是否显示加载中状态
+ * @en-US Whether to show loading state
*/
loading: {
type: Boolean,
},
/**
- * 加载提示文本
+ * @zh-CN 加载中提示文本
+ * @en-US Loading prompt text
*/
loadingLabel: {
type: String,
--
Gitee
From c7b185ad2374f7c4ebaeebbd582937f6104b9b75 Mon Sep 17 00:00:00 2001
From: sakurayinfei <970412446@qq.com>
Date: Wed, 5 Nov 2025 11:24:26 +0800
Subject: [PATCH 3/3] =?UTF-8?q?fix(doc):=20=E4=BC=98=E5=8C=96=E5=AF=B9defi?=
=?UTF-8?q?neExpose=E7=9A=84=E8=A7=A3=E6=9E=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/docs/package.json | 1 +
packages/docs/scripts/generateApi.ts | 26 ++---
packages/docs/scripts/parseDefineSlots.ts | 87 ---------------
packages/docs/scripts/parseSlotsAndExpose.ts | 105 ++++++++++++++++++
packages/opendesign/src/dialog/ODialog.vue | 2 +-
packages/opendesign/src/form/OForm.vue | 6 +-
packages/opendesign/src/loading/OLoading.vue | 2 +-
.../opendesign/src/pagination/OPagination.vue | 2 +-
.../opendesign/src/textarea/OTextarea.vue | 8 +-
.../src/virtual-list/OVirtualList.vue | 2 +-
pnpm-lock.yaml | 84 +++++++++-----
pnpm-workspace.yaml | 1 +
12 files changed, 188 insertions(+), 138 deletions(-)
delete mode 100644 packages/docs/scripts/parseDefineSlots.ts
create mode 100644 packages/docs/scripts/parseSlotsAndExpose.ts
diff --git a/packages/docs/package.json b/packages/docs/package.json
index 1009931fd..0d2135950 100644
--- a/packages/docs/package.json
+++ b/packages/docs/package.json
@@ -21,6 +21,7 @@
"markdown-it": "catalog:markdown",
"pinia": "catalog:vue",
"prettier": "catalog:lint",
+ "ts-morph": "catalog:typescript",
"vue": "catalog:vue"
},
"devDependencies": {
diff --git a/packages/docs/scripts/generateApi.ts b/packages/docs/scripts/generateApi.ts
index da8617ab7..9876b6ac8 100644
--- a/packages/docs/scripts/generateApi.ts
+++ b/packages/docs/scripts/generateApi.ts
@@ -4,8 +4,7 @@ import fsp from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { type ComponentMeta, createChecker } from 'vue-component-meta';
import { parseMulti } from 'vue-docgen-api';
-import { parse } from '@vue/compiler-sfc';
-import parseDefineSlots from './parseDefineSlots';
+import parseSlotsAndExpose from './parseSlotsAndExpose';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const base = join(__dirname, '../../opendesign/');
@@ -106,17 +105,12 @@ async function applyTempFixForEventDescriptions(filename: string, componentMeta:
}
/**
* 补充 vue-component-meta 未能解析 defineSlots 的描述和签名
- * @param filename 待解析的vue文件
+ * @param filePath 待解析的vue文件
* @param componentMeta vue组件元数据
* @returns 新的组件元数据
*/
-async function applyTempFixForSlot(filename: string, componentMeta: ComponentMeta) {
- const fileContent = await fsp.readFile(filename, 'utf-8');
- const setupScript = parse(fileContent).descriptor.scriptSetup?.content;
- if (!setupScript) {
- return;
- }
- const slotMeta = parseDefineSlots(setupScript);
+async function applyTempFixForSlotAndExpose(filePath: string, componentMeta: ComponentMeta) {
+ const { slots: slotMeta, exposes } = await parseSlotsAndExpose(filePath);
slotMeta.forEach((slot) => {
const meta = componentMeta.slots.find((item) => item.name === slot.name);
@@ -127,6 +121,9 @@ async function applyTempFixForSlot(filename: string, componentMeta: ComponentMet
componentMeta.slots.push(slot);
}
});
+ const exposed = exposes.map((expose) => componentMeta.exposed.find((item) => item.name === expose));
+ componentMeta.exposed.length = 0;
+ componentMeta.exposed.push(...exposed);
}
const pathReg = /\/(O.*)\.vue/;
const tagTypes = {
@@ -141,7 +138,7 @@ const promise = glob('*/O*.vue', { cwd: srcDir, posix: true }).then((files) => {
const meta = checker.getComponentMeta(fullPath);
await applyTempFixForEventDescriptions(fullPath, meta);
- await applyTempFixForSlot(fullPath, meta);
+ await applyTempFixForSlotAndExpose(fullPath, meta);
const pathMath = file.match(pathReg);
for (const lang of ['zh-CN', 'en-US']) {
const apiMdPath = join(fullPath, `../__docs__/${pathMath[1]}-api.${lang}.md`);
@@ -207,14 +204,13 @@ const promise = glob('*/O*.vue', { cwd: srcDir, posix: true }).then((files) => {
mdContent = `${mdContent}\n\n#### slots\n\n${markdownTable(slotsData)}`;
}
// expose
- const selfExposed = meta.exposed.filter((expose) => expose.description && exposeDesReg.test(expose.description));
- if (selfExposed.length) {
+ if (meta.exposed.length) {
const tableHeader = {
'zh-CN': ['名称', '类型', '说明'],
'en-US': ['Name', 'Type', 'Description'],
};
- let exposeData = selfExposed.map((expose) => {
- return [escapeInlineCode(expose.name), escapeInlineCode(expose.type), (expose.description.match(exposeDesReg)?.[1] || '').trim()];
+ let exposeData = meta.exposed.map((expose) => {
+ return [escapeInlineCode(expose.name), escapeInlineCode(expose.type), expose.description];
});
exposeData.unshift(tableHeader[lang]);
exposeData = cleanTableData(exposeData);
diff --git a/packages/docs/scripts/parseDefineSlots.ts b/packages/docs/scripts/parseDefineSlots.ts
deleted file mode 100644
index e739bea46..000000000
--- a/packages/docs/scripts/parseDefineSlots.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import ts from 'typescript';
-
-interface SlotDefinition {
- name: string;
- type: string;
- description: string;
- tags: {
- name: string;
- text: string;
- }[];
- schema: string;
- declarations: Array<{ file: string; range: [number, number] }>;
-}
-
-function parseDoc(jsDoc: ts.Node[] | undefined, sourceFile: ts.SourceFile) {
- const docs = { description: '', tags: [] as { name: string; text: string }[] };
- if (!jsDoc) {
- return docs;
- }
- docs.description = jsDoc
- .map((doc) => {
- let comment = doc.getText(sourceFile);
- // 清理注释格式
- comment = comment
- .replace(/\/\*\*|\*\//g, '')
- .replace(/^\s*\*\s?/gm, '')
- .replace(/@([a-zA-Z]+)\s+(.*)/gm, (_: string, paramName: string, paramValue: string) => {
- docs.tags.push({
- name: paramName,
- text: paramValue || '',
- });
- return '';
- })
- .trim();
- return comment;
- })
- .join('\n');
- return docs;
-}
-function parseTypeArg(typeArg: ts.TypeNode, sourceFile: ts.SourceFile, slots: SlotDefinition[]) {
- if (ts.isTypeLiteralNode(typeArg)) {
- for (const member of typeArg.members) {
- let name = '';
- let type = '';
- let isParsed = false;
- if (ts.isPropertySignature(member)) {
- // 属性签名 propertyName: type
- name = member.name.getText(sourceFile);
- type = member.type?.getText(sourceFile) || 'any';
- isParsed = true;
- } else if (ts.isMethodSignature(member)) {
- // 方法签名 methodName(paramName: type): type
- name = member.name.getText(sourceFile);
- type = member.getText(sourceFile) || 'any';
- isParsed = true;
- } else if (ts.isIndexSignatureDeclaration(member)) {
- // 索引签名 [key: keyType]: type
- const param = member.parameters[0];
- name = param.type?.getText(sourceFile) || param.getText(sourceFile);
- type = member.getText(sourceFile) || 'any';
- isParsed = true;
- }
- if (isParsed) {
- const docs = parseDoc((member as any).jsDoc, sourceFile);
- slots.push({ name, type, description: docs.description, tags: docs.tags, schema: '', declarations: [] });
- }
- }
- }
-}
-
-export default function parseDefineSlots(code: string): SlotDefinition[] {
- const sourceFile = ts.createSourceFile('temp.ts', code, ts.ScriptTarget.Latest, true);
- const slots: SlotDefinition[] = [];
-
- function visit(node: ts.Node) {
- // 检测 defineSlots 调用表达式
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'defineSlots' && node.typeArguments?.length) {
- const typeArg = node.typeArguments[0];
-
- parseTypeArg(typeArg, sourceFile, slots);
- }
- ts.forEachChild(node, visit);
- }
-
- visit(sourceFile);
- return slots;
-}
diff --git a/packages/docs/scripts/parseSlotsAndExpose.ts b/packages/docs/scripts/parseSlotsAndExpose.ts
new file mode 100644
index 000000000..505474e84
--- /dev/null
+++ b/packages/docs/scripts/parseSlotsAndExpose.ts
@@ -0,0 +1,105 @@
+import { Project, SyntaxKind, ScriptTarget, type TypeNode, type JSDoc, type Node} from 'ts-morph';
+import { promises as fsp } from 'node:fs';
+import { parse } from '@vue/compiler-sfc';
+import { join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const tsConfigFilePath = join(fileURLToPath(import.meta.url), '../../../opendesign', 'tsconfig.app.json');
+interface Definition {
+ name: string;
+ type: string;
+ description: string;
+ tags: {
+ name: string;
+ text: string;
+ }[];
+ schema: string;
+ declarations: Array<{ file: string; range: [number, number] }>;
+}
+const project = new Project({
+ compilerOptions: {
+ strict: true,
+ target: ScriptTarget.Latest,
+ allowJs: true,
+ lib: ['lib.esnext.d.ts', 'lib.dom.d.ts'],
+ },
+ tsConfigFilePath,
+});
+function parseDoc(jsDocs: JSDoc[]) {
+ const description = jsDocs
+ .map((doc) => doc.getDescription())
+ .join('\n')
+ .trim();
+ const tags: { name: string; text: string }[] = [];
+ jsDocs.forEach((doc) => {
+ doc.getTags().forEach((tag) => {
+ tags.push({ name: tag.getTagName(), text: tag.getText() });
+ });
+ });
+ return { description, tags };
+}
+const importReg = /import\([^)]+\)\./g;
+function getTypeText(node: Node) {
+ return node.getType().getApparentType().getText().replace(importReg, '');
+}
+function parseTypeArg(typeArg: TypeNode, slots: Definition[]) {
+ if (typeArg.isKind(SyntaxKind.TypeLiteral)) {
+ for (const member of typeArg.getMembers()) {
+ let name = '';
+ let type = '';
+ let isParsed = false;
+ if (member.isKind(SyntaxKind.PropertySignature)) {
+ // 属性签名 propertyName: type
+ name = member.getName();
+ type = getTypeText(member);
+ isParsed = true;
+ } else if (member.isKind(SyntaxKind.MethodSignature)) {
+ // 方法签名 methodName(paramName: type): type
+ name = member.getName();
+ type = getTypeText(member);
+ isParsed = true;
+ } else if (member.isKind(SyntaxKind.IndexSignature)) {
+ // 索引签名 [key: keyType]: type
+ name = member.getKeyType().getText();
+ type = member.getSignature().getDeclaration().getText();
+ isParsed = true;
+ }
+ if (isParsed) {
+ const { description, tags } = parseDoc(member.getJsDocs());
+ slots.push({ name, type, description, tags, schema: '', declarations: [] });
+ }
+ }
+ }
+}
+function parseParams(node: Node, exposes: string[]) {
+ node
+ .getType()
+ .getProperties()
+ .forEach((prop) => {
+ exposes.push(prop.getName());
+ });
+}
+export default async function parseSlotsAndExpose(filePath: string) {
+ const slots: Definition[] = [];
+ const exposes: string[] = [];
+ const content = await fsp.readFile(filePath, 'utf-8');
+ const { descriptor } = parse(content);
+ if (!descriptor.scriptSetup && !descriptor.script) {
+ return { slots, exposes };
+ }
+ const code = `${descriptor.script?.content || ''}\n;${descriptor.scriptSetup?.content || ''}`;
+ const s = project.createSourceFile(`${filePath}.script.ts`, code);
+
+ const callExpressions = s.getDescendantsOfKind(SyntaxKind.CallExpression);
+ for (const callExpression of callExpressions) {
+ const funName = callExpression.getExpression()?.getText();
+ if (funName === 'defineSlots') {
+ const typeArg = callExpression.getTypeArguments()[0];
+ parseTypeArg(typeArg, slots);
+ } else if (funName === 'defineExpose') {
+ const parma = callExpression.getArguments()[0];
+ parseParams(parma, exposes);
+ }
+ }
+ return { slots, exposes };
+}
diff --git a/packages/opendesign/src/dialog/ODialog.vue b/packages/opendesign/src/dialog/ODialog.vue
index 42d4a4419..626e048ad 100644
--- a/packages/opendesign/src/dialog/ODialog.vue
+++ b/packages/opendesign/src/dialog/ODialog.vue
@@ -46,7 +46,7 @@ const scrollbarProps = computed(() => {
});
defineExpose({
- /** expose: Toggle the ODialog */
+ /** Toggle the ODialog */
toggle(show?: boolean) {
layerRef.value?.toggle(show);
},
diff --git a/packages/opendesign/src/form/OForm.vue b/packages/opendesign/src/form/OForm.vue
index df2d429c2..3d813502e 100644
--- a/packages/opendesign/src/form/OForm.vue
+++ b/packages/opendesign/src/form/OForm.vue
@@ -77,11 +77,11 @@ provide(formInjectKey, {
});
defineExpose({
- /** expose: validate form */
+ /** validate form */
validate: doValidate,
- /** expose: reset form */
+ /** reset form */
resetFields,
- /** expose: clear validate state */
+ /** clear validate state */
clearValidate,
});
diff --git a/packages/opendesign/src/loading/OLoading.vue b/packages/opendesign/src/loading/OLoading.vue
index de0fbb969..6ea880ab4 100644
--- a/packages/opendesign/src/loading/OLoading.vue
+++ b/packages/opendesign/src/loading/OLoading.vue
@@ -15,7 +15,7 @@ const emits = defineEmits<{
const layerRef = ref | null>(null);
defineExpose({
- /** expose: Toggle loading */
+ /** Toggle loading */
toggle(show?: boolean) {
layerRef.value?.toggle(show);
},
diff --git a/packages/opendesign/src/pagination/OPagination.vue b/packages/opendesign/src/pagination/OPagination.vue
index 6534913d9..16758bdfa 100644
--- a/packages/opendesign/src/pagination/OPagination.vue
+++ b/packages/opendesign/src/pagination/OPagination.vue
@@ -153,7 +153,7 @@ const validateInput = (value: number) => {
};
defineExpose({
- /** expose: Total number of pages */
+ /** Total number of pages */
pageCount: totalPage,
});
diff --git a/packages/opendesign/src/textarea/OTextarea.vue b/packages/opendesign/src/textarea/OTextarea.vue
index 08df185d0..2d8a1c144 100644
--- a/packages/opendesign/src/textarea/OTextarea.vue
+++ b/packages/opendesign/src/textarea/OTextarea.vue
@@ -79,13 +79,13 @@ onMounted(() => {
});
defineExpose({
- /** expose: Focus method */
+ /** Focus method */
focus: () => inTextareaRef.value?.focus(),
- /** expose: Blur method */
+ /** Blur method */
blur: () => inTextareaRef.value?.blur(),
- /** expose: Clear method */
+ /** Clear method */
clear: () => inTextareaRef.value?.clear(),
- /** expose: Textarea element */
+ /** Textarea element */
inputEl: () => inTextareaRef.value?.inputEl as HTMLTextAreaElement | undefined,
});
diff --git a/packages/opendesign/src/virtual-list/OVirtualList.vue b/packages/opendesign/src/virtual-list/OVirtualList.vue
index e48a9e857..c33e39a69 100644
--- a/packages/opendesign/src/virtual-list/OVirtualList.vue
+++ b/packages/opendesign/src/virtual-list/OVirtualList.vue
@@ -398,7 +398,7 @@ onMounted(() => {
});
defineExpose({
- /** expose: scroll into view */
+ /** scroll into view */
scrollToView,
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 12deaee68..8db362100 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -104,6 +104,9 @@ catalogs:
'@types/node':
specifier: ^22.13.1
version: 22.13.1
+ ts-morph:
+ specifier: ^27.0.2
+ version: 27.0.2
typescript:
specifier: ~5.8.2
version: 5.8.2
@@ -214,6 +217,9 @@ importers:
shiki:
specifier: catalog:markdown
version: 3.7.0
+ ts-morph:
+ specifier: catalog:typescript
+ version: 27.0.2
vue:
specifier: catalog:vue
version: 3.5.13(typescript@5.8.2)
@@ -311,28 +317,6 @@ importers:
specifier: catalog:build
version: 2.2.8(typescript@5.8.2)
- packages/portal-ak:
- dependencies:
- '@opensig/open-scripts':
- specifier: workspace:^
- version: link:../scripts
- '@opensig/opendesign':
- specifier: workspace:^
- version: link:../opendesign
- devDependencies:
- sass:
- specifier: catalog:css
- version: 1.84.0
- typescript:
- specifier: catalog:typescript
- version: 5.8.2
- vite:
- specifier: catalog:build
- version: 6.3.0(@types/node@22.13.1)(sass-embedded@1.83.4)(sass@1.84.0)(terser@5.38.1)
- vue-tsc:
- specifier: catalog:build
- version: 2.2.8(typescript@5.8.2)
-
packages/scripts:
dependencies:
'@rollup/plugin-terser':
@@ -914,6 +898,9 @@ packages:
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
+ '@ts-morph/common@0.28.1':
+ resolution: {integrity: sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==}
+
'@types/argparse@1.0.38':
resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==}
@@ -1289,6 +1276,9 @@ packages:
resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==}
engines: {node: '>= 10.0'}
+ code-block-writer@13.0.3:
+ resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -1564,6 +1554,15 @@ packages:
picomatch:
optional: true
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
@@ -2034,6 +2033,10 @@ packages:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
+ picomatch@4.0.3:
+ resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+ engines: {node: '>=12'}
+
pinia@3.0.3:
resolution: {integrity: sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==}
peerDependencies:
@@ -2454,6 +2457,10 @@ packages:
resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==}
engines: {node: '>=12.0.0'}
+ tinyglobby@0.2.15:
+ resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
+ engines: {node: '>=12.0.0'}
+
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -2477,6 +2484,9 @@ packages:
ts-map@1.0.3:
resolution: {integrity: sha512-vDWbsl26LIcPGmDpoVzjEP6+hvHZkBkLW7JpvwbCv/5IYPJlsbzCVXY3wsCeAxAUeTclNOUZxnLdGh3VBD/J6w==}
+ ts-morph@27.0.2:
+ resolution: {integrity: sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==}
+
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -3029,7 +3039,7 @@ snapshots:
dependencies:
'@types/estree': 1.0.7
estree-walker: 2.0.2
- picomatch: 4.0.2
+ picomatch: 4.0.3
optionalDependencies:
rollup: 4.40.0
@@ -3164,6 +3174,12 @@ snapshots:
'@trysound/sax@0.2.0': {}
+ '@ts-morph/common@0.28.1':
+ dependencies:
+ minimatch: 10.0.1
+ path-browserify: 1.0.1
+ tinyglobby: 0.2.15
+
'@types/argparse@1.0.38': {}
'@types/clean-css@4.2.11':
@@ -3615,6 +3631,8 @@ snapshots:
dependencies:
source-map: 0.6.1
+ code-block-writer@13.0.3: {}
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -3921,6 +3939,10 @@ snapshots:
optionalDependencies:
picomatch: 4.0.2
+ fdir@6.5.0(picomatch@4.0.3):
+ optionalDependencies:
+ picomatch: 4.0.3
+
file-entry-cache@6.0.1:
dependencies:
flat-cache: 3.2.0
@@ -4394,6 +4416,8 @@ snapshots:
picomatch@4.0.2: {}
+ picomatch@4.0.3: {}
+
pinia@3.0.3(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2)):
dependencies:
'@vue/devtools-api': 7.7.7
@@ -4825,6 +4849,11 @@ snapshots:
fdir: 6.4.3(picomatch@4.0.2)
picomatch: 4.0.2
+ tinyglobby@0.2.15:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -4841,6 +4870,11 @@ snapshots:
ts-map@1.0.3: {}
+ ts-morph@27.0.2:
+ dependencies:
+ '@ts-morph/common': 0.28.1
+ code-block-writer: 13.0.3
+
tslib@2.8.1: {}
type-check@0.4.0:
@@ -4887,7 +4921,7 @@ snapshots:
unplugin-utils@0.2.4:
dependencies:
pathe: 2.0.3
- picomatch: 4.0.2
+ picomatch: 4.0.3
unplugin-vue-markdown@28.3.1(vite@6.3.0(@types/node@22.13.1)(sass-embedded@1.83.4)(sass@1.84.0)(terser@5.38.1)):
dependencies:
@@ -4904,7 +4938,7 @@ snapshots:
unplugin@2.3.5:
dependencies:
acorn: 8.15.0
- picomatch: 4.0.2
+ picomatch: 4.0.3
webpack-virtual-modules: 0.6.2
uri-js@4.4.1:
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index dd143c44c..c40e104bf 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -47,6 +47,7 @@ catalogs:
"@types/fs-extra": "^9.0.13"
"@types/glob": "^8.1.0"
"@types/markdown-it": "^14.1.0"
+ "ts-morph": "^27.0.2"
# 代码质量工具
lint:
--
Gitee