From f1467021161332f6d33d798a8a48caf25be3c96e Mon Sep 17 00:00:00 2001 From: jeffwcx Date: Fri, 22 Mar 2024 06:48:40 +0800 Subject: [PATCH 1/2] docs: vue related --- docs/config/index.md | 2 + docs/guide/vue-api-table.md | 121 ++++++++++++++++---- docs/guide/vue.md | 9 +- docs/plugin/api.md | 31 +++++- docs/plugin/techstack.md | 212 +++++++++++++++++------------------- 5 files changed, 237 insertions(+), 138 deletions(-) diff --git a/docs/config/index.md b/docs/config/index.md index 71b3ebde70..a4ab45f5c5 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -98,6 +98,8 @@ export default () => '我会被编译,展示为组件'; `resolveFilter` 配置项用于跳过指定原子资产的解析以提升性能。部分组件属性或函数签名存在多层嵌套,甚至是循环引用时,会导致解析结果巨大,此时可以通过该配置项跳过解析。 +上述配置是默认 React 解析器的配置, dumi 也提供方法覆盖原有解析器,具体可查看[API Table 支持](../plugin/techstack.md#api-table-支持)。 + ### autoAlias - 类型:`boolean` diff --git a/docs/guide/vue-api-table.md b/docs/guide/vue-api-table.md index 780c914b22..da1f8eeee1 100644 --- a/docs/guide/vue-api-table.md +++ b/docs/guide/vue-api-table.md @@ -6,7 +6,7 @@ group: order: 2 --- -# Vue 的自动 API 表格 +# Vue 的自动 API 表格 实验性 dumi 支持 Vue 组件的自动 API 表格生成,用户只需配置`entryFile`即可开始 API 表格的使用: @@ -46,6 +46,51 @@ export default { }; ``` +## checkerOptions + +我们还可以通过 checkerOptions 选项来配置 Type Checker: + +其中`exclude`选项默认会排除从 node_modules 中引用的所有类型,你还可以配置排除更多的目录: + +```ts +export default { + plugins: ['@dumijs/preset-vue'], + vue: { + checkerOptions: { + schema: { + exclude: /src\/runtime\//, + }, + }, + }, +}; +``` + +这样,`src/runtime/`目录下引用的所有接口都不会被检查。 + +还有一个比较有用的选项则是`externalSymbolLinkMappings`,可以帮助我们配置外部接口的外链,例如: + +```ts +export default { + plugins: ['@dumijs/preset-vue'], + vue: { + checkerOptions: { + schema: { + externalSymbolLinkMappings: { + typescript: { + Promise: + 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise', + }, + }, + }, + }, + }, +}; +``` + +上述配置可以将 Promise 接口链接到 MDN 的参考文档。 + +更多关于 checkerOptions 的选项请查看: [`MetaCheckerOptions`](https://github.com/umijs/dumi/tree/master/suites/dumi-vue-meta/README.md#metacheckeroptions) + ## JSDoc 编写 :::warning @@ -91,38 +136,70 @@ defineComponent({ }); ``` -### @exposed/@expose +### @component + +用以区分普通函数和函数组件的。目前无法自动识别为组件的情况有两种: + +```ts +/** + * @component + */ +function InternalComponent(props: { a: string }) { + return h('div', props.a); +} +``` + +```tsx | pure +/** + * @component + */ +export const GenericComponent = defineComponent( + (props: { item: T }) => { + return () => (
{item}
); + }, +); +``` + +都需要用`@component`注解,否则会被识别为函数 + +### API 发行相关 + +#### @public + +#### @deprecated + +#### @experimental/@beta + +#### @alpha :::warning -组件实例的方法或是属性的暴露,必须使用@exposed/@expose 标识,单文件组件也不例外 +这些 release 标签在`defineEmits`中是无法生效 ::: +对于组件实例本身暴露的方法,可以使用像`@public`这样的标签来公开 + ```ts defineExpose({ /** - * @exposed + * @public */ focus() {}, }); ``` -JSX/TSX 的组件方法暴露比较麻烦,需要用户另外声明 +如果将 MetaCheckerOptions 中的`filterExposed`设置为 false,这些发布标签将全部无效。 -```ts -export default Button as typeof Button & { - new (): { - /** - * The signature of the expose api should be obtained from here - * @exposed - */ - focus: () => void; - }; -}; -``` +> vue 的组件实例不仅会可以通过`expose`暴露属性和方法,还会暴露从外部传入的 props。 + +### @ignore/@internal -### @ignore +标有`@ignore`或`@internal`的属性不会被检查。 -被`@ignore`标记的属性就会被忽略,不会被解析 +### 版本控制相关 + +#### @version + +#### @since ## Markdown 编写 @@ -149,7 +226,11 @@ export default Button as typeof Button & { -### Methods +### Instance Methods - + ``` + +:::info +imperative 类别是通过 release 标签暴露的组件实例方法 +:::: diff --git a/docs/guide/vue.md b/docs/guide/vue.md index 1fb3b41eed..169250fb9e 100644 --- a/docs/guide/vue.md +++ b/docs/guide/vue.md @@ -6,7 +6,7 @@ group: order: 1 --- -# 安装 Vue 支持插件 +# 安装 Vue 支持插件 实验性 dumi 中对 Vue 的支持主要通过`@dumijs/preset-vue`插件集实现, 目前只支持 Vue3 @@ -17,8 +17,9 @@ pnpm i vue pnpm i -D @dumijs/preset-vue ``` -> [!NOTE] -> 如果您的 Vue 版本低于 3.3.6,请安装`@vue/compiler-sfc` +:::warning +如果您的 Vue 版本低于 3.3.6,请安装`@vue/compiler-sfc` +::: ## 配置 @@ -63,6 +64,8 @@ export default { }; ``` +关于 checkerOptions 更多的选项请查看: [`MetaCheckerOptions`](https://github.com/umijs/dumi/tree/master/suites/dumi-vue-meta/README.md#metacheckeroptions) + ### tsconfigPath TypeChecker 使用的 tsconfig,默认值为 `/tsconfig.json` diff --git a/docs/plugin/api.md b/docs/plugin/api.md index 6b3525f5b7..6b7513d3f4 100644 --- a/docs/plugin/api.md +++ b/docs/plugin/api.md @@ -93,7 +93,36 @@ api.modifyTheme((memo) => { ### registerTechStack -注册其他技术栈,用于扩展 Vue.js、小程序等技术栈的 demo 编译能力,可参考内置的 [React 技术栈](https://github.com/umijs/dumi/tree/master/src/techStacks/react.ts) 实现。dumi 官方的 Vue.js 编译方案正在研发中,敬请期待。 +注册其他技术栈,用于扩展 Vue.js、小程序等技术栈的 demo 编译能力,可参考内置的 [React 技术栈](https://github.com/umijs/dumi/tree/master/src/techStacks/react.ts) 或是 通过 `@dumijs/preset-vue`提供的 [Vue 技术栈](https://github.com/umijs/dumi/tree/master/suites/preset-vue/src/vue/techStack/sfc.ts) 实现。 + +目前提供两种 API 实现技术栈: + +1. `defineTechStack` 推荐 + +```ts +import { defineTechStack } from 'dumi/tech-stack-utils'; +const CustomTechStack = defineTechStack({ + name: 'custom', + runtimeOpts: { + compilePath: '...', + rendererPath: '...', + pluginPath: '...', + }, + isSupported(lang: string) { + return ['vue'].includes(lang); + }, + onBlockLoad(args) { + // ... + }, + transformCode(raw, opts) { + // ... + }, +}); + +api.registerTechStack(() => CustomTechStack); +``` + +2. 直接实现 `IDumiTechStack`抽象类 ```ts // CustomTechStack.ts diff --git a/docs/plugin/techstack.md b/docs/plugin/techstack.md index 16a7243280..fd47678a85 100644 --- a/docs/plugin/techstack.md +++ b/docs/plugin/techstack.md @@ -8,166 +8,149 @@ order: 1 ## IDumiTechStack 接口的实现 -为 dumi 开发添加一个技术栈插件,其核心是实现`IDumiTechStack`接口,我们以实现 Vue SFC 支持为例: +为 dumi 开发添加一个技术栈插件,其核心是实现`IDumiTechStack`接口。我们可以通过`defineTechStack`方法实现, +以 Vue SFC 支持为例,下面是一段伪代码: ```ts -import type { - IDumiTechStack, - IDumiTechStackOnBlockLoadArgs, - IDumiTechStackOnBlockLoadResult, - IDumiTechStackRenderType, -} from 'dumi/tech-stack-utils'; -import { extractScript, transformDemoCode } from 'dumi/tech-stack-utils'; -import hashId from 'hash-sum'; -import type { Element } from 'hast'; -import { dirname, resolve } from 'path'; -import { logger } from 'umi/plugin-utils'; -import { VUE_RENDERER_KEY } from '../../constants'; -import { COMP_IDENTIFIER, compileSFC } from './compile'; - -export default class VueSfcTechStack implements IDumiTechStack { - name = 'vue3-sfc'; - - isSupported(_: Element, lang: string) { - return ['vue'].includes(lang); - } - - onBlockLoad( - args: IDumiTechStackOnBlockLoadArgs, - ): IDumiTechStackOnBlockLoadResult { - return { - loader: 'tsx', - contents: extractScript(args.entryPointCode), - }; - } +import { defineTechStack, wrapDemoWithFn } from 'dumi/tech-stack-utils'; +import { logger } from 'dumi/plugin-utils'; - render: IDumiTechStackRenderType = { - type: 'CANCELABLE', - plugin: VUE_RENDERER_KEY, - }; - - transformCode(...[raw, opts]: Parameters) { +export const VueSfcTechStack = defineTechStack({ + name: 'vue3-sfc', + runtimeOpts: {}, + isSupported(lang: string) { + return ['vue'].includes(lang); + }, + onBlockLoad(args) { + // ... + }, + transformCode(raw, opts) { if (opts.type === 'code-block') { - const filename = !!opts.id - ? resolve(dirname(opts.fileAbsPath), opts.id, '.vue') - : opts.fileAbsPath; - const id = hashId(filename); - - const compiled = compileSFC({ id, filename, code: raw }); - if (Array.isArray(compiled)) { - logger.error(compiled); - return ''; - } - let { js, css } = compiled; - if (css) { - js += `\n${COMP_IDENTIFIER}.__css__ = ${JSON.stringify(css)};`; - } - js += `\n${COMP_IDENTIFIER}.__id__ = "${id}"; - export default ${COMP_IDENTIFIER};`; - - // 将代码和样式整合为一段 JS 代码 - - const { code } = transformDemoCode(js, { + const js = '...'; + const code = wrapDemoWithFn(js, { filename, - parserConfig: { - syntax: 'ecmascript', - }, + parserConfig: { syntax: 'ecmascript' }, }); - return `(async function() { - ${code} - })()`; + return `(${code})()`; } return raw; - } -} + }, +}); ``` +完整实现请查看[vue/techStack/sfc.ts](https://github.com/umijs/dumi/tree/master/suites/preset-vue/src/vue/techStack/sfc.ts) + 其实现分成三个部分: ### transformCode: 编译转换 Internal Demo -主要采用官方的`@vue/compiler-sfc`进行编译,`.vue`文件会被转换为 JS 和 CSS 代码,我们将两者封装为一个完整的 ES module。最后利用 `dumi/tech-stack-utils` 提供的`transformDemo`函数,将 ES module 转换成一个 IIFE 表达式(**代码在 dumi 编译中必须以一个 JS 表达式的方式存在**)。 +官方的`@vue/compiler-sfc`可以将`.vue`文件会被转换为 JS 和 CSS 代码。 + +我们须将两者封装为一个完整的 ES module,然后利用 `dumi/tech-stack-utils` 提供的`wrapDemoWithFn`函数,将 ES module 转换为 Block Statements([示例](https://github.com/umijs/dumi/blob/master/crates/swc_plugin_react_demo/src/lib.rs#L131))。 + +代码最后只须返回一个 IIFE 表达式即可(**代码在 dumi 编译中必须以一个 JS 表达式的方式存在**)。 -### render: 确定组件在 React 中的渲染方式 +### runtimeOpts: 运行时配置 + +有三个选项可供选择: ```ts -render: IDumiTechStackRenderType = { - type: 'CANCELABLE', - plugin: VUE_RENDERER_KEY, -}; +{ + runtimeOpts: { + compilePath: '...', + rendererPath: '...', + pluginPath: '...', + }, +} + ``` -这里指定了 Vue 组件需要实现`cancelable`函数,而该函数则需要通过 Dumi RuntimePlugin 将其注入到 React 框架中。 +`rendererPath` 指定了 挂载/卸载 Vue 组件的 cancelable 函数所在路径 一个典型的`cancelable`函数如下: ```ts -// render.tpl +import type { IDemoCancelableFn } from 'dumi/dist/client/theme-api'; import { createApp } from 'vue'; -export async function {{{pluginKey}}} ({ canvas, component }) { - if (component.__css__) { - setTimeout(() => { - document - .querySelectorAll(`style[css-${component.__id__}]`) - .forEach((el) => el.remove()); - document.head.insertAdjacentHTML( - 'beforeend', - ``, - ); - }, 1); - } +const renderer: IDemoCancelableFn = function (canvas, component) { const app = createApp(component); - app.config.errorHandler = (e) => console.error(e); app.mount(canvas); return () => { app.unmount(); }; -} +}; + +export default renderer; ``` -其主要实现 Vue 应用的创建及挂载,并返回 Vue 应用的销毁方法。 +主要实现了 Vue 应用的创建及挂载,并返回了 Vue 应用的销毁方法。 -之后将该代码注入运行时: +之后还需将该代码以文件路径的形式提供给 dumi,具体方法分两步: + +1. 将上述`cancelable`函数写入临时文件 (一般在.dumi/tmp/{插件名称}目录下,这可以保证能引用到用户安装的库): ```ts -// generate vue render code api.onGenerateFiles(() => { api.writeTmpFile({ - path: 'index.ts', - tplPath: join(tplPath, 'render.tpl'), - context: { - pluginKey: VUE_RENDERER_KEY, - }, + path: 'renderer.mjs', + content: `...`, // cancaelable函数 }); }); +``` -api.addRuntimePluginKey(() => [VUE_RENDERER_KEY]); -api.addRuntimePlugin(() => - winPath(join(api.paths.absTmpPath, `plugin-${api.plugin.key}`, 'index.ts')), -); +2. 获取临时文件地址 + +```ts +function getPluginPath(api: IApi, filename: string) { + return winPath( + join(api.paths.absTmpPath, `plugin-${api.plugin.key}`, filename), + ); +} +const rendererPath = getPluginPath(api, 'renderer.mjs'); ``` -dumi 运行时就会通过`VUE_RENDERER_KEY`执行相应的`cancelable`函数。 +得到的`rendererPath`我们就可以提供给 dumi 了。 + +`compilePath`则是浏览器端 Vue 实时编译库所在地址,dumi 会在用户进行实时代码编辑时,通过 + +```ts +const { compile } = await import(compilePath); +``` + +进行实时代码编译。 + +`compilePath`的提供方式和`rendererPath`如出一辙,这里就不赘述了。 + +在实际实现过程中,主要难度还是在于提供轻量的,浏览器端运行的编译器。 + +常用的浏览器端编译器有[@babel/standalone](https://babeljs.io/docs/babel-standalone),但其体积过大,使用时请谨慎。 + +最后的`pluginPath` 主要用于覆盖运行时配置,例如 `modifyCodeSandboxData`,`modifyStackBlitzData`,只要把这些函数所在文件地址提供给 Dumi 即可。 ### onBlockLoad: 模块加载 -dumi 默认只能对`js`,`jsx`,`ts`,`tsx`文件进行依赖分析并生成相关 asset,对于`.vue`文件则束手无策。我们可以通过`onBlockLoad`来接管默认的加载方式,主要目标是将`.vue`文件中的 script 代码提取出来方便 dumi 进行依赖分析: +dumi 默认只能对`js`,`jsx`,`ts`,`tsx`文件进行依赖分析并生成相关 asset,对于`.vue`文件则束手无策。我们可以通过`onBlockLoad`来接管默认的加载方式,主要目标是将`.vue`文件编译后进行依赖分析: ```ts onBlockLoad( args: IDumiTechStackOnBlockLoadArgs, ): IDumiTechStackOnBlockLoadResult { - return { - loader: 'tsx', // 将提取出的内容视为tsx模块 - contents: extractScript(args.entryPointCode), - }; - } + const result = compileSFC({ + id: args.path, + code: args.entryPointCode, + filename: args.filename, + }); + return { + type: 'tsx', + content: Array.isArray(result) ? '' : result.js, + }; +} ``` -其中`extractScript`是`dumi/tech-stack-utils`提供的函数用以提取类 html 文件中的所有 script 代码。 +(Vue 比较特殊,会在编译之后,引入额外的依赖,所以必须全量编译,不能只是简单地将 script 代码抽取出来) `IDumiTechStack`接口实现之后,我们还需要通过 registerTechStack 注册 Vue SFC @@ -175,26 +158,28 @@ onBlockLoad( api.register({ key: 'registerTechStack', stage: 1, - fn: () => new VueSfcTechStack(), + fn: VueSfcTechStack, }); ``` -之后就要考虑 External Demo 的编译及 API Table 的支持了: +接下来就得考虑 External Demo 的编译及 API Table 的支持了: ## External Demo 编译支持 添加对 External Demo 的编译及打包支持,这需要我们对 Webpack 进行配置,由于 dumi 本身是 react 框架,所以不能粗暴地移除对 react 的支持,而是需要将 react 相关配置限定在`.dumi`中。 +具体配置可参考 [vue/webpack/config.ts](https://github.com/umijs/dumi/tree/master/suites/preset-vue/src/vue/webpack/config.ts) + ## API Table 支持 -API Table 的支持主要在于对框架元信息信息的提取,例如针对 Vue 组件,dumi 就提供了`@dumijs/vue-meta`包来提取元数据。其他框架也要实现相关的元数据提取,主流框架基本都有相应的元数据提取包,但需要注意的是,开发者需要适配到 dumi 的元数据 schema。 +API Table 的支持主要在于对框架元信息信息的提取,例如针对 Vue 组件,dumi 就提供了 [@dumijs/vue-meta](https://github.com/umijs/dumi/tree/master/suites/dumi-vue-meta) 包来提取元数据。其他框架也要实现相关的元数据提取,主流框架基本都有相应的元数据提取包,但需要注意的是,开发者需要适配到 dumi 的元数据 schema([dumi-assets-types](https://github.com/umijs/dumi/blob/master/assets-types/typings/atom/index.d.ts)) 。 在实现元数据提取之后,还需要实现 dumi 的元数据解析架构,即将数据的提取放在子线程中。dumi 也提供了相关的 API 简化实现: **子线程侧**,我们需要实现一个元数据 Parser,这里需要实现`LanguageMetaParser`接口 ```ts -import { LanguageMetaParser, PatchFile } from 'dumi'; +import { ILanguageMetaParser, IPatchFile } from 'dumi/tech-stack-utils'; class VueMetaParser implements LanguageMetaParser { async patch(file: PatchFile) { @@ -216,11 +201,10 @@ class VueMetaParser implements LanguageMetaParser { ```ts import { - BaseApiParserOptions, - LanguageMetaParser, - PatchFile, + IBaseApiParserOptions, + ILanguageMetaParser, createApiParser, -} from 'dumi'; +} from 'dumi/tech-stack-utils'; export const VueApiParser = createApiParser({ filename: __filename, From 8f63c104f876052cae4bcb076596a446d953410c Mon Sep 17 00:00:00 2001 From: jeffwcx Date: Sun, 24 Mar 2024 15:29:47 +0800 Subject: [PATCH 2/2] docs: update defineTechStack --- docs/plugin/api.md | 4 ++-- docs/plugin/techstack.md | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/plugin/api.md b/docs/plugin/api.md index 6b7513d3f4..07899d57cb 100644 --- a/docs/plugin/api.md +++ b/docs/plugin/api.md @@ -93,7 +93,7 @@ api.modifyTheme((memo) => { ### registerTechStack -注册其他技术栈,用于扩展 Vue.js、小程序等技术栈的 demo 编译能力,可参考内置的 [React 技术栈](https://github.com/umijs/dumi/tree/master/src/techStacks/react.ts) 或是 通过 `@dumijs/preset-vue`提供的 [Vue 技术栈](https://github.com/umijs/dumi/tree/master/suites/preset-vue/src/vue/techStack/sfc.ts) 实现。 +注册其他技术栈,用于扩展 Vue.js、小程序等技术栈的 demo 编译能力。如何添加一个完整的技术栈支持,可查看[添加技术栈](../plugin/techstack.md)。 目前提供两种 API 实现技术栈: @@ -108,7 +108,7 @@ const CustomTechStack = defineTechStack({ rendererPath: '...', pluginPath: '...', }, - isSupported(lang: string) { + isSupported(node, lang) { return ['vue'].includes(lang); }, onBlockLoad(args) { diff --git a/docs/plugin/techstack.md b/docs/plugin/techstack.md index fd47678a85..767b1ee9f7 100644 --- a/docs/plugin/techstack.md +++ b/docs/plugin/techstack.md @@ -18,7 +18,7 @@ import { logger } from 'dumi/plugin-utils'; export const VueSfcTechStack = defineTechStack({ name: 'vue3-sfc', runtimeOpts: {}, - isSupported(lang: string) { + isSupported(_, lang: string) { return ['vue'].includes(lang); }, onBlockLoad(args) { @@ -36,6 +36,8 @@ export const VueSfcTechStack = defineTechStack({ return raw; }, }); + +api.registerTechStack(() => VueSfcTechStack); ``` 完整实现请查看[vue/techStack/sfc.ts](https://github.com/umijs/dumi/tree/master/suites/preset-vue/src/vue/techStack/sfc.ts)