From 5098ae666fded1ab8e993c8b036810cc063672f6 Mon Sep 17 00:00:00 2001 From: sakurayinfei <970412446@qq.com> Date: Thu, 24 Jul 2025 15:03:42 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=E6=96=B0=E5=A2=9E=E5=8F=8A=E8=B0=83?= =?UTF-8?q?=E6=95=B4no-shadow=EF=BC=8Cno-use-before-define=EF=BC=8Cno-unus?= =?UTF-8?q?ed-vars,=20vue/no-mutating-props=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 13 +++++++++++-- packages/opendesign/tsconfig.json | 2 -- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 776484b9..335e5727 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -54,6 +54,8 @@ module.exports = { }], 'default-param-last': 'off', 'no-param-reassign': ['error', { props: false }], + 'no-shadow': ['error', { hoist: 'all' }], + 'no-use-before-define': ['error', { functions: false }], 'vue/max-attributes-per-line': 'off', 'vue/html-self-closing': ['warn', { @@ -66,8 +68,15 @@ module.exports = { 'vue/singleline-html-element-content-newline': 'off', 'vue/html-closing-bracket-newline': 'off', 'vue/multiline-html-element-content-newline': 'warn', - + 'vue/no-mutating-props': ['error', { shallowOnly: true }], 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + varsIgnorePattern: '^_', + argsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_' + } + ], }, }; \ No newline at end of file diff --git a/packages/opendesign/tsconfig.json b/packages/opendesign/tsconfig.json index 1e52bd19..abe09c4b 100644 --- a/packages/opendesign/tsconfig.json +++ b/packages/opendesign/tsconfig.json @@ -18,8 +18,6 @@ "jsx": "preserve", /* Linting */ "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "baseUrl": "./", "paths": { -- Gitee From 647979a9e99e43c5cc2c8a9577288c8a189e6ad2 Mon Sep 17 00:00:00 2001 From: sakurayinfei <970412446@qq.com> Date: Thu, 24 Jul 2025 15:05:45 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=E5=88=A0=E9=99=A4=20@opensig/opende?= =?UTF-8?q?sign=E7=9A=84workspace=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 738dba12..d9565ce9 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "docs:build": "pnpm -C packages/docs build" }, "dependencies": { - "@opensig/opendesign": "workspace:^", "normalize.css": "catalog:css", "vue": "catalog:vue", "vue-router": "catalog:vue" -- Gitee From 1820c0dca1c9f3c907fd01decd070221f3a700a6 Mon Sep 17 00:00:00 2001 From: sakurayinfei <970412446@qq.com> Date: Thu, 24 Jul 2025 15:13:28 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix[doc]:=20=E4=BC=98=E5=8C=96=E5=B9=B6?= =?UTF-8?q?=E9=87=8D=E6=9E=84DemoUsage=E7=BB=84=E4=BB=B6=E5=8F=8A=E5=85=B6?= =?UTF-8?q?vite=E6=8F=92=E4=BB=B6=EF=BC=9B=E6=8C=89eslint=E8=A7=84?= =?UTF-8?q?=E5=88=99=E8=A7=84=E8=8C=83=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/docs/helper/utils.ts | 59 +++++ .../docs/plugins/generateComponentRouter.ts | 2 +- packages/docs/plugins/injectDemoAndApi.ts | 194 +++++++++++----- packages/docs/plugins/injectDemoDocs.ts | 22 +- packages/docs/plugins/markdown/highlight.ts | 2 +- .../docs/src/components/DemoContainer.vue | 14 +- packages/docs/src/components/DemoUsage.vue | 208 ++---------------- packages/docs/src/components/OperatorView.ts | 175 +++++++++++++++ packages/docs/vite.config.ts | 2 +- packages/opendesign/src/_demo/types.ts | 2 +- 10 files changed, 407 insertions(+), 273 deletions(-) create mode 100644 packages/docs/src/components/OperatorView.ts diff --git a/packages/docs/helper/utils.ts b/packages/docs/helper/utils.ts index 3dd6e315..cb728f93 100644 --- a/packages/docs/helper/utils.ts +++ b/packages/docs/helper/utils.ts @@ -19,3 +19,62 @@ export function generateCode(block: SFCBlock) { }) .join('')}>${block.content}\n`; } + +/** + * 解析vue文件的自定义块docs + * @param code 带解析的md代码 + * @returns 解析产物 + */ +export function parseDocsCode(code: string) { + const langSeparator = //gm; + const langMatchList = Array.from(code.matchAll(langSeparator)); + return langMatchList.map((langMatch, matchIndex) => { + const lang = langMatch[1]; + const langCode = code.slice( + langMatch.index + langMatch[0].length, + matchIndex === langMatchList.length - 1 ? code.length : langMatchList[matchIndex + 1].index, + ); + return { + lang, + code: langCode, + }; + }); +} + +export function escapeRegExp(str: string) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** + * 异步替换字符串中的匹配项 + * @param str - 原始字符串 + * @param regex - 正则表达式或字符串形式的正则,若想全局替换,请设置正则表达式的g标志 + * @param asyncReplacer - 异步替换函数 + * @returns 处理后的字符串 + */ +export async function asyncReplace( + str: string, + regex: RegExp | string, + asyncReplacer: (matched: RegExpExecArray) => Promise | string, +): Promise { + const finalRegex = typeof regex === 'string' ? new RegExp(escapeRegExp(regex)) : regex; + + const matches = finalRegex.global ? Array.from(str.matchAll(finalRegex)) : [finalRegex.exec(str)].filter(Boolean); + const replacements = await Promise.all(matches.map((matched) => asyncReplacer(matched))); + + let lastIndex = 0; + let result = ''; + + matches.forEach((matched, i) => { + // 添加非匹配内容 + result += str.slice(lastIndex, matched.index); + // 添加替换内容 + result += replacements[i]; + // 正确更新最后索引位置 + lastIndex = matched.index + matched[0].length; + }); + + // 添加剩余内容 + result += str.slice(lastIndex); + return result; +} diff --git a/packages/docs/plugins/generateComponentRouter.ts b/packages/docs/plugins/generateComponentRouter.ts index 561d79df..af75b4e4 100644 --- a/packages/docs/plugins/generateComponentRouter.ts +++ b/packages/docs/plugins/generateComponentRouter.ts @@ -38,7 +38,7 @@ const emit = debounce(() => { return files.map((file) => { const fullPath = resolve(searchBase, file); const content = fse.readFileSync(fullPath).toString(); - return { content, file, fullPath, name: file.match(/([^\/]+)\/__docs__\/?/)?.[1], lang: getLangByFileName(file).lang }; + return { content, file, fullPath, name: file.match(/([^/]+)\/__docs__\/?/)?.[1], lang: getLangByFileName(file).lang }; }); }) .then((fileContents) => { diff --git a/packages/docs/plugins/injectDemoAndApi.ts b/packages/docs/plugins/injectDemoAndApi.ts index c5e2b1ba..a34acb0f 100644 --- a/packages/docs/plugins/injectDemoAndApi.ts +++ b/packages/docs/plugins/injectDemoAndApi.ts @@ -1,83 +1,159 @@ -import { type Plugin } from 'vite'; +import { type Plugin, type ViteDevServer } from 'vite'; import { join, dirname } from 'node:path'; -import { existsSync, readFileSync, promises as fsp } from 'node:fs'; +import { promises as fsp } from 'node:fs'; import { getLangByFileName } from '../helper/utils'; +import { parse } from '@vue/compiler-sfc'; +import { parseDocsCode, generateCode, asyncReplace } from '../helper/utils'; const entryFileRegex = /index\.([\w-]+)\.md$/; const apiFileRegex = /api\.([\w-]+)\.md$/; +const virtualModules = new Map(); + +/** 生成 import 语句 */ +const genImportedExpression = (imported: Array<{ path: string; default?: string; all?: string }>) => { + return `${imported + .map((item) => { + let importStr = 'import '; + if (item.default) { + importStr += `${item.default} `; + } + if (item.all) { + importStr += `* as ${item.all} `; + } + importStr += `from ${JSON.stringify(item.path)};`; + return importStr; + }) + .join('\n')}`; +}; +const transformVueDemo = (code: string, id: string, mode: 'dev' | 'build' | 'unknown', viteDevServer?: ViteDevServer) => { + const imported: { + default?: string; + all?: string; + lang?: string; + path: string; + }[] = []; + const { customBlocks: _customBlocks, scriptSetup: _scriptSetup, styles } = parse(code).descriptor; + // 因为要修改 customBlocks 和 scriptSetup,所以复制一份 + const customBlocks = [..._customBlocks]; + const scriptSetup = _scriptSetup ? { ..._scriptSetup } : null; + // 处理 docs 模块 + const docsBlockIdx = customBlocks.findIndex((block) => { + if (block.type === 'docs' && block.lang === 'md') { + return true; + } + }); + if (docsBlockIdx >= 0) { + parseDocsCode(customBlocks[docsBlockIdx].content).forEach(({ lang: docsLang, code: docCode }, index) => { + const virtualId = join(dirname(id), `virtual-${docsLang}.md`).replace(/\\/g, '/'); + imported.push({ path: virtualId, default: `AutoInjectDocs${index}`, lang: docsLang }); + if (virtualModules.has(virtualId) && mode === 'dev' && viteDevServer) { + viteDevServer.watcher.emit('change', virtualId); + } + virtualModules.set(virtualId, docCode); + }); + customBlocks.splice(docsBlockIdx, 1); + if (scriptSetup) { + scriptSetup.content = `${scriptSetup.content}\n;${genImportedExpression(imported)}`; + } + } + if (scriptSetup) { + let styleCode = ''; + styles.forEach((styleItem) => { + styleCode += `${generateCode(styleItem)}`; + }); + scriptSetup.content = `${scriptSetup.content}\n;const _style = ${JSON.stringify(styleCode)};\n`; + } + const template = ``; + return `${[...customBlocks, scriptSetup, ...styles] + .filter(Boolean) + .map((block) => { + return generateCode(block); + }) + .join('\n')}\n${template}`; +}; +const transformMdEntry = async (code: string, id: string, usageFiles: Set) => { + const imported: { + default?: string; + all?: string; + lang?: string; + path: string; + }[] = []; + const lang = getLangByFileName(id); + // 将 注释替换成 + // 将 注释替换成 + let newCode = await asyncReplace(code, //g, async (match) => { + const [, directive, fileName] = match; + if (directive === 'api') { + // 拼接 api 文件 + const apiFile = join(dirname(id), `${fileName}-api.${lang.lang}.md`); + if (await fsp.stat(apiFile).catch(() => false)) { + return fsp.readFile(apiFile, 'utf-8'); + } + } + const demoFile = join(dirname(id), `./__case__/${fileName}.vue`); + if (await fsp.stat(demoFile).catch(() => false)) { + if (directive === 'case') { + imported.push({ + default: `AutoInject${fileName}`, + path: demoFile, + }); + return ``; + } else { + imported.push({ + path: demoFile, + default: `AutoInject${fileName}`, + }); + usageFiles.add(demoFile.replace(/\\/g, '/')); + return ``; + } + } + return match[0]; + }); + // 插入需要导入的模块 + if (imported.length) { + newCode += `\n`; + } + return newCode; +}; /** * vite 插件,在 index..md 添加 /__docs__/__case__ 组件;拼接 api.zh-CN.md 文件 * @returns Plugin */ export function injectDemoAndApi(): Plugin { + const usageFiles = new Set(); + let viteDevServer: ViteDevServer | null = null; return { name: 'portal:inject-demo-and-api', enforce: 'pre', - async transform(code, id) { - if (!entryFileRegex.test(id) || id.startsWith('virtual:')) { - return; + configureServer(server) { + viteDevServer = server; + }, + resolveId(id) { + if (virtualModules.has(id)) { + return id; } - const lang = getLangByFileName(id); - const imported: { - default?: string; - all?: string; - path: string; - }[] = []; - // 将 注释替换成 - // 将 注释替换成 - let newCode = code.replace(//g, (match, directive, fileName) => { - if (directive === 'api') { - // 拼接 api 文件 - const apiFile = join(dirname(id), `${fileName}-api.${lang.lang}.md`); - if (existsSync(apiFile)) { - return readFileSync(apiFile, 'utf-8'); - } - } - const fileNameWidthExt = directive === 'case' ? `./__case__/${fileName}.vue` : `./__case__/${fileName}.ts`; - const demoFile = join(dirname(id), fileNameWidthExt); - if (existsSync(demoFile)) { - if (directive === 'case') { - imported.push({ - default: `AutoInject${fileName}`, - path: demoFile, - }); - return ``; - } else { - - imported.push({ - path: demoFile, - all: 'autoInjectUsage' - }); - return ``; - } - } - return match; - }); - // 插入需要导入的模块 - if (imported.length) { - newCode += `\n`; + }, + load(id) { + return virtualModules.get(id); + }, + transform(code, id) { + // 处理 vue 文件 + if (usageFiles.has(id)) { + return transformVueDemo(code, id, this.environment.mode, viteDevServer); + } + + // 处理 md 入口文件 + if (entryFileRegex.test(id) && !id.startsWith('virtual:')) { + return transformMdEntry(code, id, usageFiles); } - return newCode; }, // 处理 api.*.md 文件的热更新 - handleHotUpdate(ctx) { + async handleHotUpdate(ctx) { const { file } = ctx; const match = file.match(apiFileRegex); if (match) { const entryFile = join(dirname(file), `index.${match[1]}.md`); - if (existsSync(entryFile)) { + if (await fsp.stat(entryFile).catch(() => false)) { const module = ctx.server.moduleGraph.getModuleById(entryFile.replace(/\\/g, '/')); if (module) { ctx.server.moduleGraph.invalidateModule(module); diff --git a/packages/docs/plugins/injectDemoDocs.ts b/packages/docs/plugins/injectDemoDocs.ts index d196b25c..4014cf9d 100644 --- a/packages/docs/plugins/injectDemoDocs.ts +++ b/packages/docs/plugins/injectDemoDocs.ts @@ -1,14 +1,15 @@ import { type Plugin, type ViteDevServer } from 'vite'; import { MarkdownItAsync } from 'markdown-it-async'; import { markdownItPlugins, markdownItOptions } from './markdown/common'; +import { parseDocsCode } from '../helper/utils'; const parseVueQuery = (id: string) => { - let [file, query] = id.split('?', 2); - if (!query) { + const [file, _query] = id.split('?', 2); + if (!_query) { return { file, query: {}, queryExtension: '' }; } - const queryExtension = query.match(/\.([a-zA-Z0-9]+)$/)?.[1]; - query = queryExtension ? query.slice(0, query.length - queryExtension.length - 1) : query; + const queryExtension = _query.match(/\.([a-zA-Z0-9]+)$/)?.[1]; + const query = queryExtension ? _query.slice(0, _query.length - queryExtension.length - 1) : _query; const queryObj = query ? query.split('&').reduce( (prev, curr) => { @@ -24,7 +25,7 @@ const parseVueQuery = (id: string) => { const virtualModules = new Map(); const genVirtualId = (id: string, lang: string) => { - return `${id.split('?')[0]}-${lang}.md`; + return `${id.split('?')[0]}-virtual-${lang}.md`; }; /** * vite插件,将vue文件中的自定义块 docs 中的 markdown 保存到_sfc_main.__docs中, @@ -53,18 +54,11 @@ export function injectDemoDocs(): Plugin { if (!query.vue || query.type !== 'docs' || !id.endsWith('.md')) { return; } - // 通过 分割不同语言的文案块 - const langSeparator = //gm; - const langMatchList = Array.from(code.matchAll(langSeparator)); const virtualIds: string[] = []; - langMatchList.forEach((langMatch, matchIndex) => { - const lang = langMatch[1]; - const langCode = code.slice( - langMatch.index + langMatch[0].length, - matchIndex === langMatchList.length - 1 ? code.length : langMatchList[matchIndex + 1].index, - ); + parseDocsCode(code).forEach(({ lang, code: langCode }) => { const virtualId = genVirtualId(id, lang); if (virtualModules.has(virtualId) && this.environment.mode === 'dev' && viteDevServer) { + // 刷新虚拟模块,通知浏览器更新 viteDevServer.watcher.emit('change', virtualId); } virtualModules.set(virtualId, { langCode, lang }); diff --git a/packages/docs/plugins/markdown/highlight.ts b/packages/docs/plugins/markdown/highlight.ts index ba59024f..2b4273bb 100644 --- a/packages/docs/plugins/markdown/highlight.ts +++ b/packages/docs/plugins/markdown/highlight.ts @@ -42,7 +42,7 @@ export const createHighlighter = () => { const stripPreCodeReg = /([\s\S]*?)<\/code><\/pre>/; /** 去除高亮代码的首尾
...
*/ - const stripPreCodeTag = (html: string) => html.replace(stripPreCodeReg, '$1'); + const stripPreCodeTag = (htmlCode: string) => htmlCode.replace(stripPreCodeReg, '$1'); // 支持语言集合 const supportLangs = new Set([ diff --git a/packages/docs/src/components/DemoContainer.vue b/packages/docs/src/components/DemoContainer.vue index caa17c59..c4e858bb 100644 --- a/packages/docs/src/components/DemoContainer.vue +++ b/packages/docs/src/components/DemoContainer.vue @@ -3,7 +3,7 @@ import { ref, h, type Component, type PropType } from 'vue'; import { OScroller, OButton, useI18n, OIconChevronUp } from '@opensig/opendesign'; type DocT = Record; -type DemoComponent = Component & { +export type DemoComponent = Component & { /** __docs__/__case__组件源的源码组件 */ DemoSource?: Component; /** __docs__/__case__组件文案 */ @@ -20,17 +20,17 @@ const switchShowCode = () => { isShowCode.value = !isShowCode.value; }; const { locale } = useI18n(); -const Docs = ({ docs, locale }: { docs?: DocT; locale: string }) => { - if (!docs || !docs[locale]) { +const Docs = ({ docs, locale: _locale }: { docs?: DocT; locale: string }) => { + if (!docs || !docs[_locale]) { return; } - if (typeof docs[locale] === 'string') { + if (typeof docs[_locale] === 'string') { return h('div', { class: 'docs', - innerHTML: docs[locale], + innerHTML: docs[_locale], }); } - return h('div', { class: 'docs' }, h(docs[locale])); + return h('div', { class: 'docs' }, h(docs[_locale])); }; @@ -44,7 +44,7 @@ const Docs = ({ docs, locale }: { docs?: DocT; locale: string }) => { + /> diff --git a/packages/docs/src/components/DemoUsage.vue b/packages/docs/src/components/DemoUsage.vue index 5f591a09..3aaa8936 100644 --- a/packages/docs/src/components/DemoUsage.vue +++ b/packages/docs/src/components/DemoUsage.vue @@ -1,62 +1,27 @@