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