Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugin_kit): add vite plugin for developing Artalk plugins #904

Merged
merged 1 commit into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
771 changes: 763 additions & 8 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions ui/artalk/src/artalk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,7 @@ export default class Artalk {

/** Use plugin, the plugin will be used when Artalk.init */
public static use(plugin: ArtalkPlugin) {
if (GlobalPlugins.includes(plugin)) return
GlobalPlugins.push(plugin)
GlobalPlugins.add(plugin)
}

/** Load count widget */
Expand Down
22 changes: 14 additions & 8 deletions ui/artalk/src/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import { DefaultPlugins } from './plugins'
/**
* Global Plugins for all Artalk instances
*/
export const GlobalPlugins: ArtalkPlugin[] = [...DefaultPlugins]
export const GlobalPlugins: Set<ArtalkPlugin> = new Set([...DefaultPlugins])

export async function load(ctx: ContextApi) {
const loadedPlugins: ArtalkPlugin[] = []
const loadPlugins = (plugins: ArtalkPlugin[]) => {
const loadedPlugins = new Set<ArtalkPlugin>()
const loadPlugins = (plugins: Set<ArtalkPlugin>) => {
plugins.forEach((plugin) => {
if (typeof plugin === 'function' && !loadedPlugins.includes(plugin)) {
if (typeof plugin === 'function' && !loadedPlugins.has(plugin)) {
plugin(ctx)
loadedPlugins.push(plugin)
loadedPlugins.add(plugin)
}
})
}
Expand Down Expand Up @@ -77,8 +77,9 @@ export async function load(ctx: ContextApi) {
/**
* Dynamically load plugins from Network
*/
async function loadNetworkPlugins(scripts: string[], apiBase: string): Promise<ArtalkPlugin[]> {
if (!scripts || !Array.isArray(scripts)) return []
async function loadNetworkPlugins(scripts: string[], apiBase: string): Promise<Set<ArtalkPlugin>> {
const networkPlugins = new Set<ArtalkPlugin>()
if (!scripts || !Array.isArray(scripts)) return networkPlugins

const tasks: Promise<void>[] = []

Expand Down Expand Up @@ -107,7 +108,12 @@ async function loadNetworkPlugins(scripts: string[], apiBase: string): Promise<A

await Promise.all(tasks)

return Object.values(window.ArtalkPlugins || {})
// Read ArtalkPlugins object from window
Object.values(window.ArtalkPlugins || {}).forEach((plugin) => {
if (typeof plugin === 'function') networkPlugins.add(plugin)
})

return networkPlugins
}

export function onLoadErr(ctx: ContextApi, err: any) {
Expand Down
71 changes: 71 additions & 0 deletions ui/plugin-kit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Artalk Plugin Kit

Artalk Plugin Kit 是 Artalk 的插件开发工具包,提供了一系列的工具函数和组件,帮助开发者更方便的开发插件。

## 安装

```bash
pnpm add artalk
pnpm add @artalk/plugin-kit -D
```

工具包中提供了 Vite 的集成插件,使用 Vite 开发能够开箱即用地构建 Artalk 插件,简化 Vite 的配置。

```js
// vite.config.js
import { ViteArtalkPluginKit } from '@artalk/plugin-kit'

export default {
plugins: [ViteArtalkPluginKit()],
}
```

## 开发

在开发插件前,你需要在 `package.json` 设置插件名字,修改 `name` 字段,以 `artalk-plugin-` 开头。

插件入口文件默认为 `src/main.ts`,你需要在入口文件导出名为 `ArtalkDemoPlugin` 的 `ArtalkPlugin` 类型的对象。

```ts
// src/main.ts
import type { ArtalkPlugin } from 'artalk'

export const ArtalkDemoPlugin: ArtalkPlugin = (ctx) => {
ctx.on('mounted', () => {
// Your plugin code here
})
}
```

执行 `pnpm dev` 开发插件,Vite 将会启动开发服务器。浏览器访问 ViteArtalkPluginKit 插件提供的内置 Artalk 调试页面,该页面已自动注入并启用了你当前正在开发的插件。

## 构建

执行 `pnpm build` 构建插件,构建产物为 `dist/artalk-plugin-demo.js`,以及 `.mjs`、`.cjs`、`.d.ts` 等文件。

ViteArtalkPluginKit 将帮助你自动生成合并的 `.d.ts` 文件,并自动在 `package.json` 中填充 `exports`、`types` 等字段。

ViteArtalkPluginKit 会进行一系列代码检查,以确保 Artalk 的插件符合开发规范。

## 发布

执行 `pnpm publish` 发布插件,你可以在 `package.json` 中配置 `"files": ["dist"]` 只推送需要发布的构建产物。

## 配置

可以通过配置 `artalkInitOptions` 控制 Artalk 的初始化参数。

```ts
// vite.config.js
import { ViteArtalkPluginKit } from '@artalk/plugin-kit'

export default {
plugins: [
ViteArtalkPluginKit({
artalkInitOptions: {
// Your Artalk init options here
},
}),
],
}
```
10 changes: 10 additions & 0 deletions ui/plugin-kit/client.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Artalk, { ArtalkPlugin } from 'artalk'

export {}

declare global {
interface Window {
Artalk?: typeof Artalk
ArtalkPlugins?: { [name: string]: ArtalkPlugin }
}
}
37 changes: 37 additions & 0 deletions ui/plugin-kit/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@artalk/plugin-kit",
"version": "1.0.0",
"description": "The plugin kit for Artalk",
"type": "module",
"main": "dist/main.js",
"types": "dist/main.d.ts",
"exports": {
".": {
"require": {
"types": "./dist/main.d.cjs",
"default": "./dist/main.cjs"
},
"default": {
"types": "./dist/main.d.ts",
"default": "./dist/main.js"
}
},
"./client": "./client.d.ts"
},
"scripts": {
"dev": "tsup --watch",
"build": "tsup"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"artalk": "workspace:^",
"picocolors": "^1.0.1"
},
"devDependencies": {
"@microsoft/api-extractor": "^7.46.1",
"esbuild-plugin-raw": "^0.1.8",
"tsup": "^8.0.2"
}
}
71 changes: 71 additions & 0 deletions ui/plugin-kit/src/plugin/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { ViteDevServer } from 'vite'
import type ts from 'typescript'

export class ViteArtalkPluginKitCtx {
/**
* The artalk plugin name (kabab-case)
*/
pluginName = ''

/**
* Whether the current environment is development
*/
isDev = true

/**
* The root directory of the working project
*/
rootDir = './'

/**
* The entry path of the plugin (main.ts)
*/
entryPath = './src/main.ts'

/**
* The export name of entry code (PascalCase)
*/
entryExportName = ''

/**
* The output directory of the plugin (dist)
*/
outDir = './dist/'

/**
* Dts temporary directory
*
* (for generate dts, first store in this directory, then copy to outDir)
*/
dtsTempDir = '.atk-vite-dts-temp' // TODO: node_modules/.atk-vite-dts-temp not working for rollup dts

/**
* The package.json path
*/
packageJsonPath = './package.json'

/**
* The vite server instance
*/
viteServer?: ViteDevServer

/**
* The TypeScript config path
*/
tsConfigPath = 'tsconfig.json'

/**
* The TypeScript compiler options (for generate dts)
*/
tsCompilerOptions?: ts.CompilerOptions

/**
* The TypeScript compiler options (raw)
*/
tsCompilerOptionsRaw?: ts.CompilerOptions

/**
* Builded flag (for watchChange)
*/
isBundled = false
}
76 changes: 76 additions & 0 deletions ui/plugin-kit/src/plugin/dev-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { Connect, HtmlTagDescriptor } from 'vite'
// eslint-disable-next-line import/no-relative-packages
import DevHTML from '../../../artalk/index.html?raw'
import { RUNTIME_PATH } from './runtime-helper'

function patchArtalkInitOptionsInHTML(html: string, options: Record<string, any>) {
const jsonStr = JSON.stringify(options, null, 8).slice(2, -2)
const optionsStr = jsonStr ? `${jsonStr},` : ''
html = html.replace(/(Artalk\.init\({[\s\S]*?)(\n\s*\}\))/, (match, p1, p2) => {
const cleanedP1 = p1.replace(/,\s*$/, '') // remove last comma
return `${cleanedP1},\n${optionsStr}${p2}`
})
return html
}

export function hijackIndexPage(
middlewares: Connect.Server,
transformIndexHtml: (url: string, html: string) => Promise<string>,
artalkInitOptions: Record<string, any>,
) {
middlewares.use(async (req, res, next) => {
// only handle the index page
let [url] = (req.originalUrl || '').split('?')
if (url.endsWith('/')) url += 'index.html'
if (url !== '/index.html') return next()

try {
let html = DevHTML.replace(/import.*?from.*/, '') // remove imports
html = patchArtalkInitOptionsInHTML(html, artalkInitOptions)
html = await transformIndexHtml('/', html)
res.end(html)
} catch (e) {
console.log(e)
res.statusCode = 500
res.end(e)
}

return undefined
})
}

export function getInjectHTMLTags(entrySrc: string): HtmlTagDescriptor[] {
return [
// artalk code
{
tag: 'script',
attrs: { type: 'module' },
children: `
import Artalk from '/node_modules/artalk'
import '/node_modules/artalk/dist/artalk.css'
window.Artalk = Artalk
`,
injectTo: 'head',
},

// plugin-kit-runtime code
{
tag: 'script',
attrs: { type: 'module' },
children: `
import { inject } from "${RUNTIME_PATH}";
inject({
config: ${JSON.stringify({ test: 'Hello World' })},
});
`,
},

// entry code
{
tag: 'script',
attrs: { type: 'module', src: entrySrc },
children: '',
injectTo: 'head',
},
]
}
Loading