5f4915222eff2ed15b1b679bb4e4b5b24913e20b
修复:插件面板实例级 i18n / undo-redo 滚动保持 / shortcutHelp 卸载残留 / CSS.escape 环境崩溃 / 大纲光标按源码行号定位 / setLocale 即时刷新 / 浮动工具栏 CJK 宽度 / ko 语言包括号 新增:setOutline(isOutline) 运行时大纲 API、getRenderCacheSize 观测接口 测试:841 → 871(补 7 个事件断言、5 个配置项、修 3 处恒真断言) 工程:CI lint 阻断、build.sh npm ci、rollup 死代码清理、覆盖率阈值门禁
MetonaEditor
TypeScript 重构 · 零运行时依赖 · 轻量级桌面端 Markdown Editor 库
特性
- TypeScript 源码 — 全模块 TypeScript 严格模式,完整类型导出,IDE 智能提示
- 零运行时依赖 — 打包后单文件,无任何第三方库,UMD / ESM / CJS 三种格式
- 自研解析器 — CommonMark + GFM 扩展,99% 行覆盖率,parseTokens / renderTokens 分离 API,registerBlockHandler 自定义块语法
- 插件系统 v2 — 拓扑排序依赖管理,6 个预设插件开箱即用,安装 / 卸载生命周期
- 三模式视图 — edit / split / preview,拖拽分隔条调整比例,双向滚动同步
- 主题系统 — light / dark / warm / auto,CSS 变量可定制,实例级隔离,主题继承,外部跟随
- 国际化 — zh-CN / en-US / ja / ko / fr / de 六种语言完整翻译,实例级语言隔离,远程加载翻译包
- 编辑体验 — 行号装订线、智能 Enter、括号自动闭合、拖放文件、大纲面板、Zen 专注模式(宽度可配置,默认 960px)、右键上下文菜单
- 内置语法高亮 — 零依赖轻量高亮器,js/ts/python/bash/css/html 等 16 个语言标识,规则缓存零重建开销,
MeEditor.highlight即插即用 - 性能 — 字数 / 词数 / 行数差异增量统计,击键零全量扫描;大纲 O(n) 线性构建,长文档即时响应
- 安全 — HTML 转义,XSS 协议过滤(javascript / vbscript / file / data),属性级注入防护(含脚注 id),白名单裸标签透传,sanitize 钩子
- 工程化 — 871 个单元测试 + Playwright 真实浏览器冒烟测试,CI 并行 5 job(含 e2e),覆盖率阈值门禁,
sideEffects: false便于 tree-shaking - 桌面端优先 — 纯电脑端设计,无移动端冗余代码
- 引用链接 — 支持
[text][ref]/![alt][ref]引用式链接和图片,含 title 属性 - RTL 支持 — 完整的从右到左布局适配(阿拉伯语、希伯来语等)
- 正则搜索 — 查找替换面板支持正则表达式模式
- 分隔条记忆 — 分屏比例自动保存到 localStorage
目录
快速开始
方式一:npm 安装(推荐)
包托管于 MetonaTeam 自建 Gitea 私有 npm 源(
git.metona.cn),不在 npm 官方源发布,安装时需指定 registry。
# 直接指定私有源安装
npm install @metona-team/metona-editor --registry=https://git.metona.cn/api/packages/MetonaTeam/npm/
# 或配置项目 .npmrc(推荐,一次配置长期生效)
# .npmrc
@metona-team:registry=https://git.metona.cn/api/packages/MetonaTeam/npm/
npm install @metona-team/metona-editor
// ES Module / TypeScript
import MeEditor from '@metona-team/metona-editor';
const editor = MeEditor.create('#editor', {
value: '# Hello World',
mode: 'split',
theme: 'auto',
plugins: ['autoSave', 'searchReplace'],
});
// CommonJS
const MeEditor = require('@metona-team/metona-editor');
const editor = MeEditor.create('#editor', { mode: 'split' });
方式二:CDN 引入
<!-- Gitea 源(私有仓库需登录,公开仓库直接可访问) -->
<script src="https://git.metona.cn/MetonaTeam/MetonaEditor/raw/branch/master/dist/metona-editor.js"></script>
<!-- 本地构建产物(下载 dist 后引入) -->
<script src="./dist/metona-editor.min.js"></script>
<div id="editor"></div>
<script>
const editor = MeEditor.create('#editor', { mode: 'split' });
</script>
方式三:本地文件
# 下载产物文件到项目中
# dist/metona-editor.js → UMD (浏览器)
# dist/metona-editor.min.js → UMD 压缩
# dist/metona-editor.mjs → ES Module
# dist/metona-editor.cjs → CommonJS
<!-- 浏览器直接引入本地文件 -->
<script src="./dist/metona-editor.min.js"></script>
// Node.js ESM
import MeEditor from './dist/metona-editor.mjs';
// Node.js CJS
const MeEditor = require('./dist/metona-editor.cjs');
配置项
MeEditor.create(container, {
// 内容
value: '', // 初始 Markdown 文本
placeholder: '', // 占位符
id: '', // 实例 id(默认自动生成,用于分隔条/草稿等存储键隔离)
// 视图
mode: 'split', // 'edit' | 'split' | 'preview'
height: 400, // 数字为 px,字符串原样使用
toolbar: DEFAULT_TOOLBAR, // 工具栏按钮数组,false 隐藏
wordCount: true, // 状态栏字数统计
lineNumbers: true, // 行号装订线
outline: false, // 大纲面板
// 行为
autofocus: false, // 自动聚焦
spellcheck: false, // 拼写检查
readOnly: false, // 只读模式
tabSize: 2, // Tab 空格数(0 = \t)
historyLimit: 100, // 历史栈上限
historyDebounce: 400, // 历史栈防抖(ms)
syncScroll: true, // 分屏同步滚动
autoBrackets: true, // 括号自动闭合
zenMode: false, // 启动即进入 Zen 专注模式
zenMaxWidth: 960, // Zen 内容区最大宽度:数字为 px / 字符串原样作为 CSS 值 / false 不限制
wordWrap: true, // 自动换行
maxLength: 0, // 最大字符数(0=不限)
floatingToolbar: true, // 选中文本浮动格式栏
// 主题与语言
theme: 'auto', // 'light' | 'dark' | 'warm' | 'auto'
locale: 'zh-CN', // 'zh-CN' | 'en-US' | 'ja' | 'ko' | 'fr' | 'de'
// 渲染钩子
render: null, // (md: string, env: RenderEnv) => string
highlight: null, // (code: string, lang: string) => string
sanitize: null, // (html: string) => string
// 外观
className: '', // 容器额外 CSS class
style: {}, // 容器内联样式
// 插件
plugins: [], // 实例级插件列表
// 回调
onChange: null, // (value: string, editor: MarkdownEditor) => void
onInput: null, // (value: string, editor: MarkdownEditor) => void
onFocus: null, // (editor: MarkdownEditor) => void
onBlur: null, // (editor: MarkdownEditor) => void
onSave: null, // (value: string, editor: MarkdownEditor) => void
onModeChange: null, // (mode: EditMode, editor: MarkdownEditor) => void
onFullscreen: null, // (fullscreen: boolean, editor: MarkdownEditor) => void
onCreate: null, // (editor: MarkdownEditor) => void
onDestroy: null, // (editor: MarkdownEditor) => void
onLinkClick: null, // (href: string, text: string, editor: MarkdownEditor) => void
});
工具栏
默认工具栏
['bold','italic','strikethrough','code','|',
'h1','h2','h3','|',
'quote','ul','ol','|',
'link','image','table','hr','|',
'undo','redo','|',
'edit','split','preview','fullscreen']
自定义工具栏
// 精简版
MeEditor.create('#editor', {
toolbar: ['bold', 'italic', '|', 'h1', 'h2', '|', 'undo', 'redo'],
});
// 添加自定义按钮
editor.addToolbarButton({
action: 'timestamp',
title: '插入时间戳',
icon: '<svg>...</svg>',
onClick: (editor) => editor.insert(new Date().toISOString()),
});
实例 API
内容操作
editor.getValue(): string
editor.setValue(md: string, opts?: { silent?: boolean }): this
editor.getHTML(): string
editor.refresh(): this // 强制重渲染(内容未变时)
editor.insert(text: string, opts?: { replace?: boolean }): this
editor.wrap(before: string, after?: string): this
editor.replaceAll(search: string, replace: string, caseSensitive?: boolean): number
editor.replaceAllRegex(pattern: RegExp, replace: string): number
editor.lineCount(): number
editor.getLine(line: number): string
editor.copyAsMarkdown(): this // 复制源码到剪贴板
editor.copyAsHTML(): this // 复制渲染 HTML 到剪贴板
光标与选区
editor.getSelectedText(): string
editor.getCursorPosition(): { line: number; column: number }
editor.setCursorPosition(line: number, column?: number): this
editor.scrollToLine(line: number): this
editor.selectLine(line: number): this
editor.selectAll(): this
浮动工具栏
editor.toggleFloatingToolbar(): this // 开关选中文本格式栏
editor.isFloatingToolbar(): boolean
命令执行
editor.exec(action: string): this
// 支持 action:
// bold italic strikethrough underline code
// h1 h2 h3 quote ul ol hr
// link image table
// indent outdent undo redo
// formatTable(光标所在表格按列宽对齐)
// edit split preview fullscreen
// zen wordwrap
历史栈
editor.undo(): this
editor.redo(): this
editor.canUndo(): boolean
editor.canRedo(): boolean
模式与全屏
editor.setMode(mode: 'edit' | 'split' | 'preview'): this
editor.getMode(): EditMode
editor.toggleFullscreen(): this
editor.isFullscreen(): boolean
editor.exitFullscreen(): this
主题与语言
editor.setTheme(theme: string): this
editor.getTheme(): string
editor.setLocale(locale: string): this
editor.getLocale(): string
editor.t(key: string, params?: object): string
统计与状态
editor.getStats(): { characters, words, chineseChars, englishWords, lines, readingTime }
editor.getStatus(): { id, mode, theme, locale, fullscreen, readOnly, disabled, destroyed, plugins }
启用 / 禁用 / 只读
editor.enable(): this
editor.disable(): this
editor.isDisabled(): boolean
editor.setReadOnly(readOnly: boolean): this
editor.isReadOnly(): boolean
事件
editor.on(name: string, fn: (...args: any[]) => void): () => void
editor.off(name: string, fn: (...args: any[]) => void): this
插件
editor.use(plugin: string | PluginObject, options?: object): this
editor.unuse(name: string): this
editor.getPlugins(): PluginObject[]
editor.addToolbarButton(config: ToolbarButtonConfig): this
快捷键
editor.registerShortcut(combo: string, handler: Function | string, description?: string): this
editor.unregisterShortcut(combo: string): this
editor.getShortcuts(): Shortcut[]
工具栏管理
editor.configureToolbar(tools: string[]): this
editor.removeToolbarButton(action: string): this
Zen / Word Wrap / Outline
editor.toggleZen(): this
editor.isZen(): boolean
editor.setZenMaxWidth(width: number | string | false): this // 运行时调整专注模式宽度
editor.getZenMaxWidth(): number | string | false
editor.toggleWordWrap(): this
editor.setWordWrap(on: boolean): this
editor.isWordWrap(): boolean
editor.setOutline(on: boolean): this // 运行时开关大纲面板
editor.isOutline(): boolean
Toast
editor.toast(message: string, opts?: {
type?: 'success' | 'error' | 'warning' | 'info';
duration?: number; // ms,0 = 不自动消失
animation?: 'fade' | 'slide' | 'scale' | 'bounce' | 'flip' | 'rotate' | 'zoom';
}): this
Copy API
editor.copyAsMarkdown(): this // 复制 Markdown 源码到剪贴板
editor.copyAsHTML(): this // 复制渲染后的 HTML 到剪贴板
销毁
editor.destroy(): void
editor.isDestroyed(): boolean
静态 API
import MeEditor from '@metona-team/metona-editor';
// 工厂函数
MeEditor.create(container, options)
// 全局默认插件
MeEditor.use(plugin, options)
MeEditor.use('autoSave', { delay: 2000 })
// 全局事件钩子
MeEditor.on('beforeCreate', (editor) => {})
MeEditor.off('beforeCreate', handler)
// 全局主题 / 语言
MeEditor.setTheme('dark')
MeEditor.setLocale('en-US')
// 状态查询
MeEditor.getStatus()
// => { version, theme, locale, globalPlugins, presetPlugins }
// 销毁全局资源(主题/语言监听、全局插件、全局钩子全面复位)
MeEditor.destroy()
// 解析器独立使用
import { parseMarkdown, parseTokens, renderTokens, safeUrl, slugify, clearRenderCache, getRenderCacheSize, registerBlockHandler } from '@metona-team/metona-editor';
// 内置语法高亮
import { highlight, registerLanguage, getSupportedLanguages } from '@metona-team/metona-editor';
事件系统
实例事件
| 事件 | 触发时机 | 参数 |
|---|---|---|
input |
textarea 原生 input | (value, editor) |
change |
内容变化 | (value, editor) |
focus |
聚焦 | (editor) |
blur |
失焦 | (editor) |
save |
Ctrl+S | (editor) |
modeChange |
模式切换 | (mode, editor) |
fullscreen |
全屏切换 | (fullscreen, editor) |
themeChange |
实例主题变更 | ({ theme, resolved, config }) |
localeChange |
实例语言变更 | ({ locale, direction }) |
zenChange |
Zen 模式切换 | (zen, editor) |
destroy |
销毁 | () |
autosave |
autoSave 触发 | ({ key, value }) |
beforeRender |
渲染前 | (editor) |
afterRender |
渲染后 | (editor) |
linkClick |
预览区链接点击 | ({ href, text }) |
fileOpened |
fileSystem 打开文件 | ({ name, handle }) |
fileSaved |
fileSystem 保存文件 | ({ handle }) |
beforeChange |
内容变更前 | (oldValue, newValue, editor) |
afterChange |
内容变更后 | (newValue, editor) |
copy |
复制到剪贴板 | ({ type }) |
selectionChange |
选区变化 | ({ start, end, text }) |
cursorMove |
光标移动 | ({ line, column }) |
全局钩子
| 钩子 | 触发时机 |
|---|---|
beforeCreate |
实例构造初始化前 |
afterCreate |
实例构造初始化完成 |
beforeRender |
每次渲染前(全局) |
afterRender |
每次渲染后(全局) |
beforeDestroy |
destroy 前 |
afterDestroy |
destroy 后 |
beforeChange |
内容变更前(全局) |
afterChange |
内容变更后(全局) |
Markdown 解析器
支持语法
| 语法 | 示例 | 输出 |
|---|---|---|
| ATX 标题 | # H1 ~ ###### H6 |
<h1> ~ <h6> |
| Setext 标题 | H1\n=== |
<h1> / <h2> |
| 粗体 | **bold** __bold__ |
<strong> |
| 粗斜体 | ***text*** ___text___ |
<em><strong> |
| 斜体 | *italic* _italic_ |
<em> |
| 删除线 | ~~text~~ |
<del> |
| 下划线 | <u>text</u>(裸标签透传) |
<u> |
| 高亮 | ==text== |
<mark> |
| 上标 | x^2^ |
<sup> |
| 下标 | H~2~O |
<sub> |
| 行内代码 | `code` |
<code> |
| 代码块 | ```lang |
<pre><code class="language-lang"> |
| 缩进代码块 | code / \tcode |
<pre><code> |
| 引用 | > quote |
<blockquote> |
| 无序列表 | - / * / + item |
<ul><li> |
| 有序列表 | 1. item |
<ol><li> |
| 嵌套列表 | 多级缩进 | 多级 <ul>/<ol> |
| 任务列表 | - [x] done |
<li class="me-task-item"> |
| 水平线 | --- *** ___ |
<hr> |
| 表格 | | a | b | |
<table> + 列对齐 |
| 链接 | [text](url 'title') |
<a> |
| 图片 |  |
<img> |
| 引用链接 | [text][ref] + [ref]: url |
<a> |
| 引用图片 | ![alt][ref] + [ref]: url |
<img> |
| 表格格式化 | exec('formatTable') |
光标所在表格按列宽对齐 |
| 自动链接 | <https://...> |
<a> |
| 数学公式 | $E=mc^2$ $$\int$$ |
<span> / <div> |
| 脚注 | text[^1] |
<sup> + 底部定义 |
| 定义列表 | Term\n: def |
<dl><dt><dd> |
| Emoji | :smile: :rocket: |
😊 🚀(150+) |
| 反斜杠转义 | \* \_ \\ |
取消标点特殊含义 |
| HTML 注释 | <!-- note --> |
透传 |
| 实体引用保护 | & © |
不被二次转义 |
链接内格式
parseMarkdown('[**粗体链接**](https://example.com)')
// => <a href="..."><strong>粗体链接</strong></a>
parseTokens / renderTokens
import { parseTokens, renderTokens } from '@metona-team/metona-editor';
const { tokens, footnotes } = parseTokens('# Hello\n\n**world**');
// tokens: [{ type: 'heading', level: 1, text: 'Hello' }, ...]
// 在渲染前变换 token
tokens.unshift({ type: 'hr' });
const html = renderTokens(tokens, {}, footnotes);
registerBlockHandler
import { registerBlockHandler } from '@metona-team/metona-editor';
registerBlockHandler({
name: 'customAlert',
priority: 8.5, // 越小越先尝试
test: (line) => line.match(/^:::(\\w+)/),
parse: (lines, i, match) => ({
token: { type: 'alert', alertType: match[1] },
newIndex: i + 2,
}),
});
XSS 安全
- 所有文本经
escapeHTML转义 - 链接 URL 过滤
javascript:/vbscript:/file:/ 非图片data: - 属性级注入防护:
href/src/alt/title/ 脚注 id /language-*引号与换行均被转义 - 白名单裸标签透传:仅
<u>/</u>(无属性),带属性的标签仍被转义 data:image限制最大 500KBsanitize钩子供外部净化(如 DOMPurify)highlight钩子异常自动回退为纯文本
插件系统
自定义插件
const myPlugin = {
name: 'myPlugin',
version: '1.0.0',
description: 'My custom plugin',
install(editor) {
// 安装逻辑
editor.on('change', this._onChange);
},
destroy(editor) {
// 清理逻辑
editor.off('change', this._onChange);
},
};
editor.use(myPlugin, { optionA: 'value' });
editor.unuse('myPlugin');
预设插件
| 插件 | 说明 | 用法 |
|---|---|---|
autoSave |
localStorage 自动保存草稿 | editor.use('autoSave', { delay: 1000 }) |
exportTool |
导出 .md / .html 文件,支持 PDF(打印) | editor.use('exportTool'); editor.exportMarkdown() |
searchReplace |
Ctrl+F 查找 / Ctrl+H 替换,支持正则、大小写、全字匹配 | editor.use('searchReplace') |
imagePaste |
粘贴剪贴板图片转 base64,可限尺寸 | editor.use('imagePaste', { maxSizeKB: 500 }) |
shortcutHelp |
按 ? 弹出快捷键面板 | editor.use('shortcutHelp') |
fileSystem |
File System Access API 读写磁盘 | editor.use('fileSystem'); editor.openFile() |
PluginManager & pluginUtils
import { PluginManager, pluginUtils } from '@metona-team/metona-editor';
const pm = new PluginManager();
pm.register('my', myPlugin);
pm.has('my'); // true
pm.destroy();
pluginUtils.validatePlugin(plugin) // { valid, errors }
pluginUtils.createPlugin({ ... }) // 工厂函数
pluginUtils.getPreset('autoSave') // 预设副本
语法高亮
v0.2.5 起内置零依赖轻量高亮器,支持 js / ts / tsx / jsx / python / bash / css / html / json / yaml / markdown / java / go / rust(含 shell / md 别名共 16 个语言标识)。
import MeEditor from '@metona-team/metona-editor';
// 作为 highlight 钩子(代码块自动高亮)
MeEditor.create('#editor', { highlight: MeEditor.highlight });
// 独立调用
const html = MeEditor.highlight(code, 'typescript');
// 注册自定义语言
MeEditor.registerLanguage('myLang', { keywords: ['kw'], builtins: [] });
高亮类名:me-hl-keyword / me-hl-string / me-hl-comment / me-hl-number / me-hl-builtin / me-hl-function,内置浅色 / 深色自适应配色,也可用 CSS 覆盖定制。
主题系统
内置主题
editor.setTheme('light') // 亮色
editor.setTheme('dark') // 暗色
editor.setTheme('warm') // 暖色
editor.setTheme('auto') // 跟随系统(默认)
CSS 变量定制
:root {
--md-bg: #ffffff;
--md-text: #1f2937;
--md-border: rgba(0, 0, 0, 0.08);
--md-shadow: 0 10px 36px -10px rgba(0,0,0,0.18);
--md-toolbar-bg: rgba(248, 249, 250, 0.92);
--md-textarea-bg: #ffffff;
--md-preview-bg: #ffffff;
--md-code-bg: rgba(243, 244, 246, 1);
--md-code-text: #1f2937;
--md-accent: #3b82f6;
--md-muted: #6b7280;
--md-radius: 10px;
--md-font: -apple-system, "Segoe UI", "PingFang SC", sans-serif;
--md-mono: "SF Mono", "Consolas", monospace;
}
自定义主题
import { themeUtils } from '@metona-team/metona-editor';
themeUtils.registerTheme('ocean', {
extends: 'dark', // 继承 dark 主题
bg: '#0a1628',
accent: '#38bdf8',
});
themeUtils.switchTheme('ocean');
外部主题跟随
// 从父容器继承
editor.getThemeContext().adopt();
// 跟随外部元素
editor.getThemeContext().syncWithElement(document.body);
// 手动断开所有外部跟随订阅(实例 destroy 时也会自动断开)
editor.getThemeContext().dispose();
国际化
切换语言
editor.setLocale('en-US') // 实例级
MeEditor.setLocale('zh-CN') // 全局
添加语言
import { i18nUtils } from '@metona-team/metona-editor';
i18nUtils.addTranslations('ja', {
bold: '太字',
italic: '斜体',
});
远程加载翻译包
await i18nUtils.loadRemote('/locales/ja.json', 'ja');
格式化工具
i18nUtils.formatNumber(1234567) // '1,234,567'
i18nUtils.formatCurrency(99.99, 'USD') // '$99.99'
i18nUtils.formatDate('2024-01-15') // 本地化日期
快捷键
| 快捷键 | 功能 |
|---|---|
Ctrl+B |
粗体 |
Ctrl+I |
斜体 |
Ctrl+U |
下划线 |
Ctrl+E |
行内代码 |
Ctrl+K |
插入链接 |
Ctrl+1/2/3 |
标题 H1/H2/H3 |
Ctrl+Q |
引用 |
Ctrl+Z |
撤销 |
Ctrl+Y / Ctrl+Shift+Z |
重做 |
Ctrl+S |
保存 |
Ctrl+F |
查找 |
Ctrl+H |
查找替换 |
Tab / Shift+Tab |
缩进 / 反缩进 |
? |
快捷键帮助 |
Enter |
智能 Enter(列表/引用延续) |
构建与测试
# 安装依赖
npm install
# 开发模式(热更新 + 示例站点)
npm run dev
# 构建产物(dist/)
npm run build
# 类型检查
npm run typecheck
# 运行测试
npm test
# 测试覆盖率
npm run test -- --coverage
# 浏览器冒烟测试(Playwright + Chromium,需先构建)
npm run build && npm run test:e2e
# 性能基准(先构建再运行)
npm run build && npm run bench
# 代码格式化
npm run format
构建产物
dist/
├── metona-editor.js UMD(浏览器直接引入)
├── metona-editor.min.js UMD 压缩版(CDN)
├── metona-editor.mjs ES Module
├── metona-editor.cjs CommonJS
└── metona-editor.d.ts TypeScript 类型声明
源码结构
src/
├── index.ts 入口 / 全局API
├── core.ts MarkdownEditor 类(构造 / 事件 / 历史 / 模式)
├── commands.ts 编辑命令实现(包裹 / 前缀 / 插入 / 表格格式化)
├── floating-toolbar.ts 选中文本浮动格式栏
├── context-menu.ts 右键上下文菜单
├── outline.ts 大纲面板
├── parser.ts 自研 Markdown 解析器
├── highlight.ts 内置轻量语法高亮器
├── plugins.ts 插件系统 & 6 个预设
├── themes.ts 主题系统
├── i18n.ts 国际化
├── styles.ts CSS-in-JS
├── constants.ts 常量 / 类型定义
├── utils.ts 工具函数
├── animations.ts 动画元数据
├── icons.ts 工具栏 SVG 图标
└── locales.ts 六语言翻译数据
技术栈
| 项目 | 技术 |
|---|---|
| 语言 | TypeScript 5 (strict) |
| 构建 | Rollup 3 |
| 测试 | Jest 29 + jsdom(871 用例,覆盖率阈值门禁) |
| 浏览器冒烟 | Playwright + Chromium(22 项断言) |
| 类型生成 | rollup-plugin-dts |
| 零运行时依赖 | ✅ |
License
MIT © MetonaTeam
Languages
TypeScript
82.9%
HTML
14.6%
JavaScript
2.1%
Shell
0.4%