- Published on
如何编写你的第一个vscode插件
- Authors
- Name
- ttyS3
准备工作
安装好最新版本的vscode(这个自动不用说),但是我这里要强调一下的是,vscode插件用vscode自己来写,能省很多事情,虽然你也可以用WebStorm之类的来写
安装好基本的工具,Git和Node.js(包含我们需要的npm)
安装Yeoman和VS Code Extension Generator
npm install -g yo generator-code
Yeoman号称“现代webapps脚手架工具”,使用它可以非常方便地生成各种项目的代码,而不是每个字节都要你从0撸起。 你需要生成的那个项目的“模板”,在Yeoman里被称为“生成器"(generator), 这为是为什么我们安装了Yeoman之后, 还要安装VS Code Extension Generator (就是我们命令行中的generator-code
, 这里的code
不是代码的意思,则是指vscode
) 在Yeoman的官网你可以找到上千个生成器: https://yeoman.io/generators/
第一个vscode插件
代码生成
❯ yo code
_-----_ ╭──────────────────────────╮
| | │ Welcome to the Visual │
|--(o)--| │ Studio Code Extension │
`---------´ │ generator! │
( _´U`_ ) ╰──────────────────────────╯
/___A___\ /
| ~ |
__'.___.'__
´ ` |° ´ Y `
? What type of extension do you want to create? (Use arrow keys)
❯ New Extension (TypeScript)
New Extension (JavaScript)
New Color Theme
New Language Support
New Code Snippets
New Keymap
New Extension Pack
(Move up and down to reveal more choices)
不得不说这个生成器真是方便和强大。既然我们是要生成扩展,当然是选”New Extension“了,那么这个选项分两种语言可选:TypeScript 和 JavaScript.
虽然老灯更熟悉JavaScript, 但是,这里老灯要选择的是TypeScript,后面会为解释为什么。
然后这个yo还会问你一些问题:
? What's the name of your extension? wudeng
? What's the identifier of your extension? wudeng
? What's the description of your extension? this is a demo extension for fun
? Initialize a git repository? Yes
? Which package manager to use? npm
插件名称,这里由于我们只是创建一个演示插件,起名比较随意,就叫wudeng
(无灯)吧。 然后它会问你identifier(标识),默认和名字一样就OK,嗯,直接回车。 identifier
一般会用来组成插件在vscode插件市场的URL,也会用于插件安装时的快速安装命令。 因此,一般是使用纯小写英文。 然后description是简短地描述一下,你这个插件是干嘛的。 接着会问你是不是要初始化git仓库,当然是yes
. 最后问你是用哪个包管理器,npm 或 yarn, 老灯这里就直接使用npm了(2020年了,npm不再比yarn慢多少了).
运行调试
我们直接用vscode打开刚才生成的那个目录(wudeng
), 为了方便区, 我们把此时打开的vscode窗口叫做”代码编辑窗口“,然后,神奇的操作来了! 直接按F5
,没错,就是F5
, vscode会开始自动构建插件代码,然后自动启动了另一个vscode窗口。 这个窗口有点特别,它不是普通的窗口,而是加载了我们当前打开的插件的窗口。
窗口的标题有vscode自动加的前缀 Extension Development Host] -
, 为了方便区别,我们把此vscode窗口叫做”扩展开发宿主窗口“。
然后,我们在新开的”扩展开发宿主窗口“中,在命令面板(Ctrl+Shift+P)中输入Hello World命令。
按回车之后,vscode右下角弹出提示信息:
接下来,我们看下,即使是这么简单的东西,它是如何实现的。
在”代码编辑窗口“,我们双击打开src/extension.ts
这个源码文件。 可以看到它注册(registerCommand)了一个标识为wudeng.helloWorld
的命令(command), 这个命令的处理函数非常简单,就是一个箭头函数,功能就是显示一条提示信息(showInformationMessage
), 信息内容为 Hello World from wudeng!
() => {
vscode.window.showInformationMessage('Hello World from wudeng!');
}
我们把文字内容修改成 Hello World from 也无荒野也无灯!
会如何? 重新在”扩展开发宿主窗口“中执行Hello World命令,发现,执行结果并没有改变。 因为,插件并没有重新加载。我们更新了代码,vscode自动重新编译生成了新的插件代码,但是”扩展开发宿主窗口“ 没有重新加载新生成的代码。 要让插件代码重新加载也很简单,我们切到”代码编辑窗口“,点击顶部的绿色Restart
按钮(或者按快捷键Ctrl+Shift+F5
)
然后”扩展开发宿主窗口“就会自动重新加载新的插件代码。
tips:
除了在”代码编辑窗口“,点击顶部的绿色
Restart
按钮(或者按快捷键Ctrl+Shift+F5
), 还可以”扩展开发宿主窗口“中,按Ctrl+Shift+i
打开调试控制台,然后把光标定位到调试控制台, 再按F5
即可重载插件。
小试牛刀
当然,发个hello world确实显得这个文章没有太多意义了。你这个hello world插件又不能实际工作。 所以,发hello world当然不是老灯的本意了。hello world满大街都能找到。 文章的重点部分,当然是在这后面部分了。
缘由
其实在此之前老灯一直不会写vscode插件。也没有想过要写vscode插件,只到有一天,老灯开始用Hugo写博客,发现某个Hugo插件一直用起来不爽, 于是就想,能不能想办法改一改。我主要用它的New Post
功能来创建新的日志markdown文件。 为什么不直接新建markdown文件,然后再写日志?因为Hugo的markdown文件不是普通的markdown文件,它在文件最开始会有一些Front Matter, 而通过Hugofy的New Post
,它实际上是调用的hugo new post/xxx/xxx.md来创建新日志,因此会自动生成Front Matter
.
这个New Post
操作本身没有问题。 但是它对于“右击某个目录,然后在这个目录下创建日志”这种简单地需求都无法满足。 看上去它的命令行方式的New Post
操作是OK,但是,试想一下非常常见的需求: 在Hugo我们习惯用文件夹的方式来组织日志,默认情况下,目录就是分类,假设我有一个linux父分类, 然后其下有Fedora, ArchLinux, Debian等子分类,按照这个Hugofy的命令,我们要输入的文件名为 linux/fedora/日志标题.md
,而如果插件有这种支持,我们就可以直接在fedora
目录上右击,然后选择新建日志。
因此,我做的第一个修改,就是增加右键菜单的支持。 其次还有一些bug, 比如下载Hugo主题的功能不能正常工作, Hugo有错误这个插件完全没提示是什么错误,然后你会一头雾水不知道怎么解决。 错误提示信息不够友好和准确等等。下面会一一详细说明,并试图操刀解决。
第一刀: 优化新建日志和page bundle支持
我们先来做第一步吧,把源码fork到自己的仓库: https://github.com/ttys3/hugofy-vscode
然后clone到本地,用vscode打开,然后执行F5,发现并没有像预期的那样,打开一个新的Extension Development Host] -
窗口。 而是弹出了一个popup,让你选是C++还是Node.js等,然后我想,既然是Node.js的项目,就选Node.js吧,然而结果还是不能运行。 然后查看了下vscode console的日志,发现是提示找不到vscode
这个module, 明明在package.js
里已经加了这个module的,为什么运行的时候还提示找不到呢?
我用meld这个对比工具,把hugofy-vscode的目录跟我们的hello world扩展目录进行了对比,发现hugofy-vscode的目录下缺少一些vscode需要的文件。 总体来说,整个.vscode
这个目录是缺失的。直接把hello world下面的.vscode
目录复制过去,发现F5功能神奇地可以工作了。
然后大概浏览了一下代码,发现这些代码没有严格遵守typescript的写法。 于是编辑 tsconfig.json
启用严格类检测: "strict": true /* enable all strict type-checking options */
代码里充斥着各种分号;
, 自从用习惯了Golang之后,我再也不习惯写带分号的语句了。 那么,Typescript本身是不是支持和提倡无分号语句呢,于是我进行了一翻搜索。
TypeScript follows the same ASI rules as JavaScript. Semicolons are technically not required in either language, save for a few rare, specific cases.
https://medium.com/@eugenkiss/dont-use-semicolons-in-typescript-474ccfe4bdb3
既然ts像js一样遵循ASI规则,那我们就可以放心地省略分号了。
编辑 .eslintrc.json 禁用分号:
"@typescript-eslint/semi": "never",
"semi": "off"
然后,咱也没写过ts这玩意啊。倒底靠不靠谱呢?
咱也没太多时间看官方手册来学个ts, 像吃快餐一样的快速上手教程最适合,我们就要看两下就开搞的那种,嗯,找到一个不错的上手教程 https://ts.xcatliu.com/
老灯花了几分钟大概了解了一下ts这个语言。
然后,开始撸vscode插件了,先拿Hugofy这个插件开刀。
根据vscode文档 https://code.visualstudio.com/api/references/contribution-points ,我们要增加右键菜单,需要修改package.json
的contributes
字段. (你不是第一次搞vscode插件么,你怎么知道这些的?当然是通过搜索找到了文档)
我们在contributes
字段下增加keymenus
,其值为一对象:
{
"explorer/context": [
{
"when": "explorerResourceIsFolder",
"command": "hugofy.newPost",
"group": "hugo"
}
]
}
when
表示,只有在exploer, 也就是vscode左边的文件浏览器,才会触发这个右键菜单。并且,我们的需求是,只要求对目录右击时能有右键菜单。 因为这个when
的条件是explorerResourceIsFolder的值为true
的时候。 command 为要执行的命令,这个命令是我们在插件初始化时注册的。 对于一个第一次撸vscode插件的人来说,要知道这一点显然是不容易的,因此,我这个资源是从so上面搜索到的 https://stackoverflow.com/questions/38267360/vscode-extension-api-identify-file-or-folder-click-in-explorer-context-menu
这个答案中还提到了插件开发中一个非常重要的技能,调用Developer: Inspect Context Keys
来获取这些信息。
注意要打开控制台(Ctrl+Shift+i), 不然你是看不到结果的。但是我上面那个图还是不能太直观的描述这个inspect操作,因此老灯又做了个gif动图:
好了,现在我们测试一下右键菜单,是不是出来了?
右键菜单是出来了,但是执行的命令呢? 原代码是不能处理右键菜单的,因此我们要引入一个参数。右击了哪个对象,这个对象必须传入到我们的命令里。 通过一次调试,我们可以发现,我们需要的被右击的目录的路径,就在对象的 fsPath
这个属性里。但是如果是直接执行的命令(非右击的情况),命令处理函数 是收不到任何对象的,此时它是undefined
, 因此我们修改后的newPost()函数必须要同时支持这两种情况。
另外一点是,由于我们主要是采用Hugo的page bundle的方式来组织日志及其资源(图片,视频等), 因此,markdown文件的默认名称就是index.md
,我们需要快速的创建日志文件,而不需要用户再次手动输入index.md
这些字符串,这样太麻烦了。 查询vscode api文档,得知showInputBox
是可以有默认值的,这个值放在value
属性里,就像这样:
vscode.window.showInputBox({ placeHolder: 'Enter filename', value: 'index.md' })
好了,命令处理函数修改好了。如果我们输入中文的路径呢?中文路径在URL显示的时候非常不友好,会被转码为urlencode之后的形式。 因此,我们要么手动在Hugo markdown文件里的Front matter里加一个slug
字符,手写上英文版的slug
, 要么,还有更自动的方式? 没错,我们可以修改代码,让它自动完成中文到拼音的转换。当我们输入测试中文
时,新建时目录名自动转换为拼音的ce-shi-zhong-wen
.
我们再来看下修改后的效果:
这里,文件名相对于content/post
的路径是linux/archlinux/ce-shi-zhong-wen/index.md
, 如果我们使用原来的插件而不是我们修改过后的, 要输入这么长的路径,是不是很麻烦?而经过修改后的插件,我们只需要在archlinux
目录上右击,然后输入测试中文
,后面的index.md
插件已经预先写好了, 所以,在写page bundle或分类路径比较深的情况下,这次修改带来了非常大的方便。
第二刀: 修复主题下载
修复请求问题
原来的代码主题下载不工作,我们先分析下为什么。通过一些简单的console.log
日志打印,我们很快就能发现,原来的插件代码调用的是https.get
来请求github api. 但是github的api进行了一些重定向操作,而https.get
是不支持重定向的,怎么办?我们换一个更重量级的库来请求https? 好像没有必要。 https.get
非常轻量级且能完成工作,我们需要的只是让它能处理http重定向而已。又是一番搜索,很快,找到一个名为follow-redirects库,正是为解决重定向问题而生的。 我们只需要把原来的import代码换成import { https } from 'follow-redirects'
, 然后实际的代码都不用动,就解决这个问题了。 另外要记得安装一个这个库:npm i follow-redirects
修复逻辑问题
很多时候,一个主题对于它自己的名称(主要会用于URL路径加载资源文件)是有要求的。 所以我们不能直接github的仓库名称是什么,我们把主题git clone下来就存到什么目录,这会导致问题。
现在的问题是,Hugo的主题仓库里,很多仓库名称命名并不统一。有些是hugo-xxx-theme
的格式的名称,真正的主题名就是中间的xxx
, 有些是hugo-theme-xxx
,最后的xxx
才是真正的主题名称。还有各式各样的,比如xxx-hugo-theme
,hugo-xxx
,xxx-hugo
, theme-xxx
和xxx-theme
等。 还有一些更过分的, 比如xxx-hugo-yyy-theme
之类的。这种程序没法自动处理,因为真正的主题名,有可能是xxx, 也有可能是yyy.
总之,我们在处理主题名称的时候,要进行规范化处理,提取出真正的主题名。当然,这种处理是要尽可能是避免的,最好的解决办法是,Hugo官方出面,规范化主题repo的命名, 不规则的主题,要重新命名后再加入官方repo.对于一个已经存在300个以上主题的仓库来说,要这样办是非常困难的,因为重新命名主题,可能会破坏现存用户的主题更新操作。
还有一点是,当用户切换主题时,如果Hugo server已经启动了,则我们需要重启Hugo server以应用新的主题。
优化请求
原插件都是要进行二次请求,第一次请求用于从gohugoio/hugoThemes
这个仓库获取所有主题列表,第二次请求根据用户选择了哪个主题,再请求一次api获取这个主题的git clone URL. 但是我们仔细观察,会发现gohugoio/hugoThemes
这个仓库有些特殊,它里面的主题,都是采用git submodule的方式添加进来的,而不是添加的实际文件。 而我们观察第一次api请求获取主题列表时,里面有一个字段名为html_url
, 这个值就像这样:https://github.com/marketempower/axiom/tree/34156c0530092c3cef71bfeaa133c19673bb58b1"
眼尖的童鞋一眼就能看出,完全没有必要进行第二次请求,因为主题的仓库url就可以从这个html_url
里提取出来,比如上面这个提取出来就是https://github.com/marketempower/axiom.git
然后也有极个别主题html_url
字段值为null的,这是为什么呢?原来这几个主题的源git仓库不是github, 而是在其它git仓库托管服务商那里,比如在gitlab之类的。数了一下,发现这种类型的仓库数量只在个位数。因此我们可以优化代码,使这个代码在90%以上的情况,都不需要进行第二次api请求就能直接知道主题的git URL.
第二点优化就是,原代码是每次在执行下载主题的命令时才开始下载主题列表,而这个获取过程,在国外可能会很快,但是国内的情况大家也都明白,耗时是比较长的。这就导致了,你点击下载主题的命令,但是过了好长时间才会有结果出来。所以,我们可以在插件初始化时就在后台下载好主题列表,然后,我们引入一个缓存机制,后续任何时候要下载主题,都不需要进行第二次API请求来获取主题列表。 通过查询vscode api文档,我们知道,插件初始化时会调用 activate
, 那么我们就在activate
处理函数里加上这个主题获取的动作。
//文件顶部加上声明
let extCache: any
const themelistCacheKey = 'ttys3.hugoy.themeList'
// Extension activation method
const activate = (context: any) => {
// 插件加载,初始化缓存对象
extCache = new vscache(context)
// 注册插件命令
context.subscriptions.push(
vscode.commands.registerCommand('hugofy.getVersion', getVersion),
vscode.commands.registerCommand('hugofy.newSite', newSite),
vscode.commands.registerCommand('hugofy.newPost', newPost),
vscode.commands.registerCommand('hugofy.build', build),
vscode.commands.registerCommand('hugofy.downloadTheme', downloadTheme),
vscode.commands.registerCommand('hugofy.setTheme', setTheme),
vscode.commands.registerCommand('hugofy.startServer', startServer),
vscode.commands.registerCommand('hugofy.stopServer', stopServer)
)
//获取Hugo主题列表并缓存
themeUtils.getThemesList().then((themeList: any) => {
extCache.put(themelistCacheKey, themeList)
})
};
exports.activate = activate
优化主题选择列表
由于Hugo官方主题git仓库是使用的submodule, 虽然我们可以一个请求获取绝大多数主题的git URL, 但是对于这个主题的star数量,还是获取不到的。 如果要一次性给300个主题来一个star数据获取,通过REST API也会导致请求过于频繁。有没有办法解决呢? 办法当然是有的,GitHub GraphQL API可以一次请求和返回多个资源,但是不同于REST API可以有限地公开请求(不需要登录),GitHub GraphQL API是需要有密钥才能请求的。 直接使用,当然会给使用者造成一定的麻烦,毕竟人家只是需要安装个插件,然后还要提供一个github 密钥,有点麻烦了。因此,这种可以做成可选方案。 如果提供了Github API密钥,就可以显示更详细的信息(fork数量,star数量,最近更新代码时间等,然后可以根据star排序等等)。否则,就显示名称 + URL.
使用GitHub GraphQL API是可行的,我们可以随便一次请求3个主题的fork数量,star数量,创建时间,最后提交代码的时间:
当然,这里老灯时间有限,就不实现这个功能了,或许在看文章的你有时间,可以尝试实现,来提交PR.
第三刀: 其它改进
优化错误提示和处理
当Hugo server启动失败的时候,大部分是由于主题缺少必要的配置(有些主题会需要一些特别配置),或者,这个主题本身的代码存在问题。 原插件显示的是错误的Hugo命令不存在这种”错误的错误信息“。我们修改之后,让它显示详细的失败错误信息,以方便用户修正主题配置,顺利应用新主题。
提示信息优化方面也有很多工作可以做,比如,当用户点击下载一个主题时,很可能由于网络原因下载失败,此时,应该友好地显示一个“重新下载”的按钮,而不是重新返回主题列表, 再让用户肉眼找到之前他想下载的那个主题。 当一个主题下载成功的时候,用户很可能是想尝试一下使用这个主题的,插件不应该傻傻地只完成主题下载。而是应该提供一个“使用当前主题“的按钮,用户点击后即可应用当前主题, 并自动打开预览页面。
优化后的效果,选择下载主题时,主题列表可以瞬间显示, 另外,下载成功时会有”应用此主题“和”启动服务器“的按钮:
这里可以讨论的地方有很多,甚至老灯当前发布的这个版本可能也有很多地方还可以优化的。
增加默认快捷键
每次按Ctrl+Shift+p
,然后再通过搜索关键字hugo
来执行相应的命令,对于需要频繁执行的操作来说,会显得非常麻烦。如果有一个默认的快捷键映射,会更加方便。 这个插件有8个命令,所以在默认的快捷键映射上,也不可能占用8个快捷键,这样太浪费了。因此我这里使用第一组合键+second key of chord
的方式。 比如,新建Post, 快捷键设置为ctrl+shift+h p
,意思是,先按ctrl+shift+h
然后释放,再按一下p
然后释放,动作就触发了。设置主题(theme)的快捷键为 ctrl+shift+h t
,这里的h
取的是Hugo的意思,但是这里有个小问题,不过应该不会成为问题。vscode默认的快捷键里,ctrl+shift+h
是有映射成Search: Replace in Files
的。 但是由于vscode左侧快捷菜单就有搜索和替换的快捷按钮,因此,这个应该不会成为问题。如果实在是需要频繁的进行文件之间的替换操作,也可以自行设置快捷键。 插件只是提供默认地映射,用户可以自行修改。
有了快捷键,下载主题(d), 切换主题(t), 启动hugo服务器(s), 新建日志Post(p) 等等这些操作都非常顺畅了。而且键的字母默认是跟操作本身相关联的,基本上取的首字母,比较有意义, 从而记忆方面也不成问题。
根据vscode文档,我们需要向package.json
文件contributes
字段下增加如下快捷键配置:
"keybindings": [
{
"key": "ctrl+shift+h s",
"command": "hugofy.startServer"
},
{
"key": "ctrl+shift+h b",
"command": "hugofy.build"
},
{
"key": "ctrl+shift+h d",
"command": "hugofy.downloadTheme"
},
{
"key": "ctrl+shift+h p",
"command": "hugofy.newPost"
},
{
"key": "ctrl+shift+h n",
"command": "hugofy.newSite"
},
{
"key": "ctrl+shift+h t",
"command": "hugofy.setTheme"
},
{
"key": "ctrl+shift+h x",
"command": "hugofy.stopServer"
},
{
"key": "ctrl+shift+h v",
"command": "hugofy.getVersion"
}
]
使用快捷键操作一下主题下载和主题切换:
第四刀: webpack打包
webpack作为前端打包界的扛把子, 插件打包这种事自然少不了它。 事实上当你使用vsce
命令发布插件时,vsce就会提示你,你的插件包含的文件数太多了,应该采用打包器打包一下。
但是这里,我们面对的是一个现存的插件,它在创建之初就没有使用webpack. 因此我们要给它加上webpack打包。
打包这部分主要是参考 https://code.visualstudio.com/api/working-with-extensions/bundling-extension
以及这个文档中提到的参考PR https://github.com/Microsoft/vscode-references-view/pull/50
如果把代码贴出来就太长了,所以这里我说一下commit: https://github.com/ttys3/hugofy-vscode/commit/aa848221624ce13a52985568f2eeb19bd5d3ec28
注意:
由于
tasks.json
中的problemMatcher
换成了$ts-webpack
,而vscode默认并不支持这个, 因此我要要安装一个额外的插件TypeScript + Webpack Problem Matchers. 在vscode中,打开Quick Open (Ctrl+P),执行ext install eamodio.tsl-problem-matcher
即可快速安装。
如何发布插件
此部分基本上参考 https://code.visualstudio.com/api/working-with-extensions/publishing-extension
vscode采用vsce
(Visual Studio Code Extensions的缩写)命令进行插件的打包,发布和管理。 如果没有安装,首先我们要安装一下这个工具:
npm install -g vsce
发布插件时需要PAT(Personal Access Token的缩写), 这个PAT怎么获取呢?
首先,你要有一个Azure DevOps organization,如果没有Azure账号,你要注册一个。 登录Azure账号后,进入 https://dev.azure.com/ 会进入你的组织主页。
比如老灯的组织名是wudeng
, 那么进入的是https://dev.azure.com/wudeng/
,进入这里之后,啥项目也不用创建。 咱不用管其它的,只需要点击带齿轮的小人图标,进去之后点击+New Token
创建新的PAT。
这里要注意的是, Organization 一定要选 All accessible organizations
,不然你后面会遇到ERROR Failed request: (401)
错误。 权限范围那里,找到Marketplace
, 并勾选上 Acquire
和 Manage
:
创建好PAT后一定要记得复制保存好,因为后面不会再显示了。
OK, 有了PAT,我们可以开始创建 publisher 了:
vsce create-publisher ttys3
publisher 是对于插件开发者的标识, 这个标识是唯一的,老灯这里就用ttys3
吧。
根据 https://code.visualstudio.com/api/references/extension-manifest , 我们需要在package.json
中增加publisher
,这个值就是我们刚才创建的。
另外还有一些字段比较重要:
{
"name": "hugofy",
"publisher": "ttys3",
"repository": "https://github.com/ttys3/hugofy-vscode",
"version": "0.3.3"
}
插件的唯一标识是由publisher
.name
组成的,因此要选用好的方便记忆和搜索的名字。 repository
是源码仓库,这个方便大家点击和查看源码。 version
是版本号
发布插件:
cd 插件所在目录
vsce publish 要发布的版本号
比如 vsce publish 0.3.3
发布成功后,新的版本不会马上出现,要等待几分钟后才可以看到。
发布之后就会得到一个插件页面,比如我这个是 https://marketplace.visualstudio.com/items?itemName=ttys3.hugofy
后记
由于篇幅关系,文章中不可能涉及太多代码,如果希望参与一起改进的朋友,可以直接查看仓库源码 http://github.com/ttys3/hugofy-vscode 这也是老灯第一次使用ts写代码, 终究逃不过”真香定律“, Angular被ts拉拢了,Vue3直接采用ts编写了, 而React由于历史原因, 它比ts出来得早,因此它不可能是采用ts编写的,不过目前React也支持采用ts来编写代码。
所以,老灯觉得,以ts目前这个势头,学一下还是有必要的。
本文参考
https://code.visualstudio.com/api/get-started/your-first-extension
https://liiked.github.io/VS-Code-Extension-Doc-ZH/
https://code.visualstudio.com/api/working-with-extensions/bundling-extension
https://code.visualstudio.com/api/working-with-extensions/publishing-extension#.vscodeignore
https://code.visualstudio.com/api/references/vscode-api
https://code.visualstudio.com/api/references/contribution-points
https://medium.com/@eugenkiss/dont-use-semicolons-in-typescript-474ccfe4bdb3
https://ts.xcatliu.com/basics/type-assertion
https://github.com/microsoft/TypeScript/issues/27293
https://github.com/wmonk/create-react-app-typescript/issues/214
https://scripter.co/hugo-leaf-and-branch-bundles/
https://github.com/microsoft/vscode-extension-samples/blob/master/helloworld-sample/src/extension.ts