refactor!: move vuepress-plugin-netlify-functions to another repo

This commit is contained in:
pengzhanbo 2024-06-27 00:58:32 +08:00
parent 10cb625401
commit 4132167597
35 changed files with 16 additions and 7943 deletions

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (C) 2021 - PRESENT by pengzhanbo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,108 +0,0 @@
# `vuepress-plugin-netlify-functions`
English | [简体中文](./README.md)
If your vuepress site is deployed on `netlify` and you want to be able to use `netlify functions` for `serverless`.
You may need this plugin to provide support.
## Features
- On the Vuepress local server, start a `Netlify Functions` local service to help you debug `functions` locally.
- Generate a usable `netlify.toml` and add the appropriate 'functions' configurations if you have already created`netlify.toml`
- This plugin does not provide specific `function` scripts, only `netlify functions` support;
It can be used directly in themes, directly in vuepress projects, or based on this plugin,
new plugins can be developed to provide more detailed functionality support.
- Use `dotenv` to provide support similar to `Netlify Environment variables` in the local service
environment. Create `.env` files in the project root directory to hold development-time environment variables
## Install
```sh
npm install vuepress-plugin-netlify-functions
# or
pnpm add vuepress-plugin-netlify-functions
# or
yarn add vuepress-plugin-netlify-functions
```
## Usage
1. In a Vuepress project, or in a Vuepress theme
``` js
// .vuepress/config.[jt]s
import { netlifyFunctionsPlugin } from 'vuepress-plugin-netlify-functions'
export default {
// ...
plugins: [
netlifyFunctionsPlugin()
]
// ...
}
```
2. In a vuepress plugin:
``` ts
import { useNetlifyFunctionsPlugin } from 'vuepress-plugin-netlify-functions'
function myPlugin(): Plugin {
return (app: App) => {
const {
// proxy prefix, default: /api
proxyPrefix,
preparePluginFunctions,
generatePluginFunctions
} = useNetlifyFunctionsPlugin(app, {
// Specifies the functions directory for the plugin where the relevant scripts are developed
directory: path.resolve(__dirname, 'functions')
})
return {
name: 'vuepress-plugin-myPlugin',
onPrepared: () => preparePluginFunctions(),
onGenerated: () => generatePluginFunctions(),
}
}
}
```
## Methods
### `netlifyFunctionsPlugin(options)`
plugin function
In the Vuepress configuration, or in the Vuepress topic configuration.
#### options
- `options.sourceDirectory` functions source directory。
@default `app.dir.source('.vuepress/functions')。
- `options.destDirectory` functions output directory
@default `app.dir.dest('function')
- `options.proxyPrefix` server proxy prefix
@default `/api`
functions request to proxy `^/api/*`
### `useNetlifyFunctionsPlugin(app, options)`
Used in the plugin when developing the VuePress plugin
- `app`: **App**
- `options.directory`
Functions development directory in plugin
## Example
- [vuepress-plugin-page-collection](https://github.com/pengzhanbo/vuepress-theme-plume/tree/main/packages/plugin-page-collection)
Log and display the number of page views/visits of blog articles by connecting `leanCloud` in functions based on `netlify-functions`

View File

@ -1,113 +0,0 @@
# `vuepress-plugin-netlify-functions`
[English](./README.en-US.md) | 简体中文
如果你的 vuepress 站点是部署在 `netlify` 的,而且希望能够使用`netlify functions` 来做 `serverless`
那么你可能需要本插件提供支持。
## 功能
- 在 vuepress 本地服务器上,启动一个 `netlify functions` 本地服务,帮助你在本地对 `functions` 进行调试。
- 帮助生成一个 可用的 `netlify.toml` ,如果你已经创建了 `netlify.toml`,会添加合适的 `functions` 配置
- 本插件不提供具体的 `function` 脚本,仅提供 `netlify functions` 支持;可以在主题中直接只用,也可以在 vuepress 项目中直接使用,也可以基于本插件,开发新的插件提供更详细的功能支持。
- 使用 `dotenv` 在本地服务环境提供 类似于 `netlify environment variables` 支持。 在项目根目录下 创建 `.env` 文件用于保存开发时环境变量
## 安装
```sh
npm install vuepress-plugin-netlify-functions
# or
pnpm add vuepress-plugin-netlify-functions
# or
yarn add vuepress-plugin-netlify-functions
```
## 使用
1. 在 vuepress 项目中,或者在一个 vuepress 主题中
``` js
// .vuepress/config.[jt]s
import { netlifyFunctionsPlugin } from 'vuepress-plugin-netlify-functions'
export default {
// ...
plugins: [
netlifyFunctionsPlugin()
]
// ...
}
```
2. 在 vuepress plugin 中:
``` ts
import { useNetlifyFunctionsPlugin } from 'vuepress-plugin-netlify-functions'
function myPlugin(): Plugin {
return (app: App) => {
const {
// 请求前缀, 默认 /api
proxyPrefix,
preparePluginFunctions,
generatePluginFunctions
} = useNetlifyFunctionsPlugin(app, {
// 指定插件的functions目录相关脚本在此目录中开发
directory: path.resolve(__dirname, 'functions')
})
return {
name: 'vuepress-plugin-myPlugin',
onPrepared: () => preparePluginFunctions(),
onGenerated: () => generatePluginFunctions(),
}
}
}
```
## Methods
### `netlifyFunctionsPlugin(options)`
插件函数。
在 vuepress 配置中,或者在 vuepress 主题配置中使用。
#### options
- `options.sourceDirectory` functions 源文件夹。
默认 `app.dir.source('.vuepress/functions') 目录。
- `options.destDirectory` functions 输出文件夹。
默认 `app.dir.dest('function') 目录
- `options.proxyPrefix` proxy代理前缀。
默认 `/api`
functions下的请求通过 `/api` 转发
### `useNetlifyFunctionsPlugin(app, options)`
在 开发 vuepress 插件时, 在插件中使用
- `app`: **App**
- `options.directory`
插件中的 functions 开发目录
## 查看详细说明文档
待补充
## Example
## 示例
### 插件开发示例
- [vuepress-plugin-page-collection](https://github.com/pengzhanbo/vuepress-theme-plume/tree/main/packages/plugin-page-collection)
基于 netlify-functions 的,通过在 functions中连接 `leancloud` 实现 博客文章页 阅读数/访问次数 的记录与展示。

View File

@ -1,61 +0,0 @@
{
"name": "vuepress-plugin-netlify-functions",
"type": "module",
"version": "1.0.0-rc.71",
"description": "The Plugin for VuePress 2, Support Netlify Functions",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"license": "MIT",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/pengzhanbo/vuepress-theme-plume.git",
"directory": "plugins/plugin-netlify-functions"
},
"bugs": {
"url": "https://github.com/pengzhanbo/vuepress-theme-plume/issues"
},
"keywords": [
"VuePress",
"vuepress plugin",
"netlify",
"netlify functions",
"netlifyFunctions",
"vuepress-plugin-netlify-functions"
],
"exports": {
".": {
"types": "./lib/node/index.d.ts",
"import": "./lib/node/index.js"
},
"./package.json": "./package.json"
},
"main": "lib/node/index.js",
"types": "lib/node/index.d.ts",
"files": [
"lib"
],
"scripts": {
"build": "pnpm run copy && pnpm run ts",
"clean": "rimraf --glob ./lib ./*.tsbuildinfo",
"copy": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png}\" lib",
"ts": "tsc -b tsconfig.build.json"
},
"peerDependencies": {
"vuepress": "2.0.0-rc.14"
},
"dependencies": {
"@iarna/toml": "^2.2.5",
"@netlify/functions": "^2.8.0",
"chalk": "^5.3.0",
"chokidar": "^3.6.0",
"cpx2": "^7.0.1",
"dotenv": "^16.4.5",
"esbuild": "^0.21.5",
"execa": "^9.3.0",
"netlify-cli": "^17.30.0",
"portfinder": "^1.0.32"
},
"publishConfig": {
"access": "public"
}
}

View File

@ -1,39 +0,0 @@
import type { App } from 'vuepress/core'
import type { NetlifyFunctionsPluginOptions } from '../shared/index.js'
export function extendsBundlerOptions(bundlerOption: any, app: App, options: NetlifyFunctionsPluginOptions, server: string): void {
// 在 netlify-cli 的 function:serve 中,
// 默认就是 指向 /.netlify/functions
// 而配置的 --functions 仅作为源文件入口
const targetPath = '/.netlify/functions'
if (app.options.bundler.name === '@vuepress/bundler-vite') {
const rewriteRE = new RegExp(`^${options.proxyPrefix}`)
bundlerOption.viteOptions.server = bundlerOption.viteOptions.server || {}
const viteServer = bundlerOption.viteOptions.server
// 将 netlify functions server 代理到 当前的 vuepress 开发 服务器上
viteServer.proxy = Object.assign(viteServer.proxy || {}, {
[options.proxyPrefix as string]: {
target: server,
changeOrigin: true,
rewrite: (url: string) => url.replace(rewriteRE, targetPath),
},
})
}
if (app.options.bundler.name === '@vuepress/bundler-webpack') {
const rewritePath = `^${options.proxyPrefix}`
bundlerOption.configureWebpack(
(config: any, isServer: boolean, isBuild: boolean) => {
if (isBuild)
return
config.devServer = config.devServer || {}
config.devServer.proxy = Object.assign(config.devServer.proxy || {}, {
[options.proxyPrefix as string]: {
target: server,
changeOrigin: true,
pathRewrite: { [rewritePath]: targetPath },
},
})
},
)
}
}

View File

@ -1,10 +0,0 @@
import type { NetlifyFunctionsOptions } from '../shared/index.js'
import { netlifyFunctionsPlugin } from './plugin.js'
export * from './useNetlifyFunctionsPlugins.js'
export { type NetlifyFunctionsOptions }
export { netlifyFunctionsPlugin }
export default netlifyFunctionsPlugin

View File

@ -1,3 +0,0 @@
export * from './initFunctions.js'
export * from './netlifyConfig.js'
export * from './netlifyServer.js'

View File

@ -1,72 +0,0 @@
import type { App } from 'vuepress/core'
import { path } from 'vuepress/utils'
import * as chokidar from 'chokidar'
import esbuild from 'esbuild'
import type { NetlifyFunctionsPluginOptions } from '../../shared/index.js'
import { readFileList } from '../utils/index.js'
export async function generateFunctions(app: App, options: NetlifyFunctionsPluginOptions): Promise<void> {
const { directory } = options
const { source, dest } = directory
const userSource = source[0]
const files = readFileList(userSource)
if (files.length > 0) {
await esbuild.build({
entryPoints: files,
outbase: userSource,
outdir: dest,
platform: 'node',
format: 'cjs',
})
}
}
export async function initialFunctions(app: App, options: NetlifyFunctionsPluginOptions): Promise<void> {
if (!app.env.isDev)
return
const { directory } = options
const { source, temp } = directory
const userSource = source[0]
const files = readFileList(userSource)
if (files.length > 0) {
await esbuild.build({
entryPoints: files,
outbase: userSource,
outdir: temp,
platform: 'node',
format: 'cjs',
})
}
watchFunctions(app, options)
}
export function watchFunctions(app: App, { directory }: NetlifyFunctionsPluginOptions): void {
const { source, temp } = directory
const userSource = source[0]
const watcher = chokidar.watch('**/*.ts', {
cwd: userSource,
ignoreInitial: true,
})
watcher.on('add', async (file: string) => {
await esbuild.build({
entryPoints: [path.join(userSource, file)],
outbase: userSource,
outdir: temp,
platform: 'node',
format: 'cjs',
})
})
watcher.on('change', async (file: string) => {
await esbuild.build({
entryPoints: [path.join(userSource, file)],
outbase: userSource,
outdir: temp,
platform: 'node',
format: 'cjs',
})
})
}

View File

@ -1,62 +0,0 @@
import process from 'node:process'
import type { JsonMap } from '@iarna/toml'
import { parse, stringify } from '@iarna/toml'
import type { App } from 'vuepress/core'
import { fs, path } from 'vuepress/utils'
import type { NetlifyFunctionsPluginOptions } from '../../shared/index.js'
export interface NetlifyConfig {
functions: Record<string, any>
redirects: Record<string, any>[]
}
const configName = 'netlify.toml'
function readConfig(filepath: string): NetlifyConfig {
let netlifyConfig = ''
if (fs.existsSync(filepath))
netlifyConfig = fs.readFileSync(filepath, 'utf-8') || ''
return (parse(netlifyConfig) as unknown as NetlifyConfig) || {}
}
function writeConfig(filepath: string, netlifyConfig: NetlifyConfig): void {
fs.writeFileSync(
filepath,
stringify(netlifyConfig as unknown as JsonMap),
'utf-8',
)
}
function resolveFunctions(config: NetlifyConfig, { directory }: NetlifyFunctionsPluginOptions, app: App): void {
const functions = (config.functions = config.functions || {})
functions.directory
= functions.directory || path.relative(app.dir.dest('../'), directory.dest)
}
function resolveRedirects(config: NetlifyConfig, { proxyPrefix }: NetlifyFunctionsPluginOptions): void {
const funcDir = `/${(config.functions.directory || '').replace(/^\//, '')}`
const redirects = (config.redirects = config.redirects || [])
if (!redirects.some(redirect => redirect?.to?.startsWith(funcDir))) {
redirects.push({
from: path.join('/', proxyPrefix, '*'),
to: path.join(funcDir, ':splat'),
status: 200,
force: true,
Headers: {
'X-From': 'Netlify',
},
})
}
}
export function generateNetlifyConfig(app: App, options: NetlifyFunctionsPluginOptions): NetlifyConfig {
const configPath = path.join(process.cwd(), configName)
const config = readConfig(configPath)
resolveFunctions(config, options, app)
resolveRedirects(config, options)
writeConfig(configPath, config)
return config
}

View File

@ -1,59 +0,0 @@
import { Buffer } from 'node:buffer'
import process from 'node:process'
import { fs, getDirname, path } from 'vuepress/utils'
import dotenv from 'dotenv'
import { execa } from 'execa'
import * as portFinder from 'portfinder'
import type { NetlifyFunctionsPluginOptions } from '../../shared/index.js'
const __dirname = getDirname(import.meta.url)
function loadEnvConfig(): Record<string, string | undefined> {
const configPath = path.resolve(process.cwd(), '.env')
if (!fs.existsSync(configPath))
return {}
try {
const content = fs.readFileSync(configPath, 'utf-8')
return dotenv.parse(Buffer.from(content))
}
catch {
return {}
}
}
export interface NetlifyServe {
host: string
close: () => void
}
export async function netlifyServe({
directory,
}: NetlifyFunctionsPluginOptions): Promise<NetlifyServe> {
const port = await portFinder.getPortPromise({ port: 9000 })
const argv = [
'functions:serve',
'--port',
`${port}`,
'--functions',
path.join('./', path.relative(process.cwd(), directory.temp)),
// '--debug',
]
const { stdout, kill } = execa(
path.resolve(__dirname, '../../../node_modules/.bin/netlify'),
argv,
{
cwd: process.cwd(),
env: {
...loadEnvConfig(),
},
},
)
stdout?.pipe(process.stdout)
return {
host: `http://localhost:${port}`,
close: () => kill(),
}
}

View File

@ -1,103 +0,0 @@
/**
* vuepress netlify上 netlify functions
* serverless
*
* functions firebase
* 访访
*
* 使
* netlify functions server
* vuepress
* functions functions
*
* .vuepress/.temp/functions
* netlify functions server
* functions的地址为
* http://localhost:{port}/.netlify/functions/{function-name}
*
*
*
* 使 useNetlifyFunctionsPlugin()
*
*
* - 使 proxyPrefix netlify.toml
* redirect
* - functions
*
*/
import type { App, Plugin } from 'vuepress/core'
import type {
NetlifyFunctionsOptions,
NetlifyFunctionsPluginOptions,
} from '../shared/index.js'
import { extendsBundlerOptions } from './extendsBundlerOptions.js'
import type { NetlifyServe } from './netlify/index.js'
import {
generateFunctions,
generateNetlifyConfig,
initialFunctions,
netlifyServe,
} from './netlify/index.js'
function initOptions(app: App, {
sourceDirectory,
destDirectory,
proxyPrefix = '/api',
}: NetlifyFunctionsOptions): NetlifyFunctionsPluginOptions {
return {
directory: {
source: [sourceDirectory || app.dir.source('.vuepress/functions')],
dest: destDirectory || app.dir.dest('functions'),
temp: app.dir.temp('functions'),
},
proxyPrefix,
}
}
const cache = {
options: {},
}
export function getOptions(): NetlifyFunctionsPluginOptions {
return cache.options as NetlifyFunctionsPluginOptions
}
/**
*
* netlify function netlify functions
*
* @param options
*/
export function netlifyFunctionsPlugin(options: NetlifyFunctionsOptions = {}): Plugin {
return (app: App) => {
const opts = initOptions(app, options)
let server: NetlifyServe
cache.options = opts
return {
name: 'vuepress-plugin-netlify-functions',
onInitialized: async (app) => {
// 启动netlify functions server
if (!app.env.isBuild) {
// 初始化用户侧的 functions
await initialFunctions(app, opts)
server = await netlifyServe(opts)
}
},
onWatched: (app, watchers) => {
watchers.push(server)
},
extendsBundlerOptions: (bundlerOption, app: App) => {
extendsBundlerOptions(bundlerOption, app, opts, server.host)
},
onGenerated: async (app: App) => {
// 生成配置文件
generateNetlifyConfig(app, opts)
await generateFunctions(app, opts)
},
}
}
}

View File

@ -1,5 +0,0 @@
declare module 'cpx2' {
const watch: any
const copy: any
export { watch, copy }
}

View File

@ -1,57 +0,0 @@
import type { App, PluginObject } from 'vuepress/core'
import { path } from 'vuepress/utils'
import * as cpx2 from 'cpx2'
import type { UseNetlifyFunctionPluginsOptions } from '../shared/index.js'
import { getOptions, netlifyFunctionsPlugin } from './plugin.js'
interface UseNetlifyFunctionResult {
/**
* functions
*/
proxyPrefix: string
/**
* onPrepare functions
*/
preparePluginFunctions: () => void
/**
* onGenerate functions dest中
*/
generatePluginFunctions: () => void
}
export function useNetlifyFunctionsPlugin(app: App, options: UseNetlifyFunctionPluginsOptions): UseNetlifyFunctionResult {
if (typeof options === 'undefined')
throw new Error('useNetlifyFunctionsPlugin [options] argument not found.')
if (typeof options.directory !== 'string' || !options.directory) {
throw new Error(
`useNetlifyFunctionsPlugin [options.directory] must be a string\n exp: path.join(__dirname, 'functions')`,
)
}
const plugins = app.pluginApi.plugins
if (
!plugins.some(
(plugin: PluginObject) =>
plugin.name === 'vuepress-plugin-netlify-functions',
)
) {
app.use(netlifyFunctionsPlugin())
}
const { proxyPrefix, directory } = getOptions()
const source = path.join(options.directory, '**/*.js')
function preparePluginFunctions(): void {
if (!app.env.isBuild) {
cpx2.watch(source, directory.temp, {
ignore: ['!**/*.d.js'],
})
}
}
function generatePluginFunctions(): void {
cpx2.copy(source, directory.dest)
}
return { proxyPrefix, preparePluginFunctions, generatePluginFunctions }
}

View File

@ -1 +0,0 @@
export * from './readFileList.js'

View File

@ -1,19 +0,0 @@
import { fs, path } from 'vuepress/utils'
export function readFileList(source: string, fileList: string[] = []): string[] {
if (!fs.existsSync(source))
return []
const files = fs.readdirSync(source)
files.forEach((file: string) => {
const filepath = path.join(source, file)
const stat = fs.statSync(filepath)
if (stat.isDirectory()) {
if (file !== 'node_modules')
readFileList(filepath, fileList)
}
else {
fileList.push(filepath)
}
})
return fileList
}

View File

@ -1,39 +0,0 @@
export interface NetlifyFunctionsOptions {
/**
* netlify functions source directory
*
* netlify functions
*
* @default `app.dir.source('.vuepress/functions')`
*/
sourceDirectory?: string
/**
* netlify functions output directory
*
* netlify functions
*
* @default `app.dir.dest('functions')`
*/
destDirectory?: string
/**
* functions directory
*
* @default `/api`
*/
proxyPrefix?: string
}
export interface NetlifyFunctionsPluginOptions {
directory: {
dest: string
source: string[]
temp: string
}
proxyPrefix: string
}
export interface UseNetlifyFunctionPluginsOptions {
directory: string
}

View File

@ -1,8 +0,0 @@
{
"extends": "../tsconfig.build.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib"
},
"include": ["./src"]
}

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (C) 2021 - PRESENT by pengzhanbo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,77 +0,0 @@
# `@vuepress-plume/plugin-page-collection`
这是一个使用了 `netlify functions` + `leancloud``VuePress2` 插件示例。
- 功能: 进入页面时,记录访问次数,并提供一个 组件展示页面访问次数。
## Why
- 为什么是在 `netlify functions` 中 连接 `leancloud` ,而不是直接在 web客户端中请求 `leancloud` ?
这是出于数据安全的角度考虑,避免直接在浏览器可见的代码中暴露私密的鉴权信息。
## 使用方式
> (仅示例,本插件未发布到 npm)
- 在 vuepress 配置插件
``` ts
export default defineUserConfig({
plugins: [
pageCollectionPlugin()
]
})
```
- 在需要做页面记录的路由级组件中引入`<PageCollection>`组件
``` html
<Page>
<PageCollection />
</Page>
```
- 在项目根目录 新建 `.env` 文件
> 开发时使用,发布到生产时,需要在 netlify 中配置 环境变量。
> 同时,需要将 `.env` 文件添加到 `.ignore`中,避免暴露
```sh
LEAN_CLOUD_APP_ID='your leancloud appId'
LEAN_CLOUD_APP_KEY='your leancloud appKey'
LEAN_CLOUD_MASTER_KEY='your leancloud masterKey'
```
效果:
``` html
阅读数99
```
## 结构
``` SH
.
├── database # leancloud-storage
│ └── Page.json # Object Page 在 leancloud控制台中导入即可使用
├── src # 源码
│ ├── client
│ │ ├── clientAppEnhance.ts # 注入全局组件
│ │ ├── components
| | | └── PageCollection.ts # 组件
│ │ └── composables
| | ├── index.ts
| | └── usePageCollection.ts # 请求 functions
│ ├── node
│ │ ├── functions # netlify functions 文件夹
| | | └── page_collection.ts # 连接 leancloud 获取 page数据
│ │ ├── index.ts
│ │ └── plugin.ts # 插件配置
│ └── shared
│ └── index.ts
└── package.json
```
## 主要代码说明
请查看源码注释

View File

@ -1,46 +0,0 @@
{
"schema": {
"updatedAt": { "type": "Date" },
"ACL": {
"type": "ACL",
"default": { "_owner": { "write": true }, "*": { "read": true } },
"hidden": true
},
"objectId": { "type": "String" },
"createdAt": { "type": "Date" },
"url": {
"type": "String",
"v": 2,
"required": false,
"comment": "页面路径",
"hidden": false,
"read_only": false
},
"visitCount": {
"type": "Number",
"v": 2,
"required": false,
"auto_increment": false,
"default": 0,
"comment": "访问次数",
"hidden": false,
"read_only": false
}
},
"permissions": {
"create": { "*": true },
"find": { "onlySignInUsers": true },
"get": { "onlySignInUsers": true },
"update": { "onlySignInUsers": true },
"delete": { "onlySignInUsers": true },
"add_fields": { "onlySignInUsers": true }
},
"indexes": [
{
"v": 2,
"key": { "url": 1 },
"name": "-user-url_1",
"background": true
}
]
}

View File

@ -1,52 +0,0 @@
{
"name": "plugin-page-collection",
"type": "module",
"version": "1.0.0-rc.71",
"private": true,
"description": "The Plugin for VuePress 2",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"license": "MIT",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/pengzhanbo/vuepress-theme-plume.git"
},
"bugs": {
"url": "https://github.com/pengzhanbo/vuepress-theme-plume/issues"
},
"exports": {
".": "./lib/node/index.js",
"./client": "./lib/client/index.js",
"./package.json": "./package.json"
},
"main": "lib/node/index.js",
"types": "lib/node/index.d.ts",
"files": [
"lib"
],
"scripts": {
"build": "pnpm run copy && pnpm run ts",
"clean": "rimraf --glob ./lib ./*.tsbuildinfo",
"copy": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png}\" lib",
"ts": "tsc -b tsconfig.build.json"
},
"peerDependencies": {
"vuepress": "2.0.0-rc.14"
},
"dependencies": {
"@netlify/functions": "^2.8.0",
"leancloud-storage": "^4.15.2",
"vue": "^3.4.30",
"vue-router": "4.3.2",
"vuepress-plugin-netlify-functions": "workspace:~"
},
"publishConfig": {
"access": "public"
},
"keyword": [
"VuePress",
"vuepress plugin",
"page collection",
"vuepress-plugin-page-collection"
]
}

View File

@ -1,9 +0,0 @@
import { defineClientConfig } from 'vuepress/client'
import type { ClientConfig } from 'vuepress/client'
import Collection from './components/PageCollection.js'
export default defineClientConfig({
enhance({ app }) {
app.component('PageCollection', Collection)
},
}) as ClientConfig

View File

@ -1,17 +0,0 @@
import { defineComponent, h } from 'vue'
import { usePageCollection } from '../composables/index.js'
export default defineComponent({
name: 'PageCollection',
setup() {
const collection = usePageCollection()
return () =>
h(
'span',
{
class: 'page-collection',
},
`阅读数:${collection.visitCount}`,
)
},
})

View File

@ -1 +0,0 @@
export * from './usePageCollection.js'

View File

@ -1,51 +0,0 @@
import { onMounted, reactive } from 'vue'
import { useRoute } from 'vue-router'
declare const __COLLECTION_PROXY_PREFIX__: string
const prefix = __COLLECTION_PROXY_PREFIX__
interface PageCollection {
visitCount: number
}
interface ResponseData {
code: number
result: Record<string, any>
}
async function fetchCollection(url: string): Promise<PageCollection> {
// 发起 netlify functions 请求
// 你已经注意到,接口名就是在 node/functions 目录下的 文件名
const response = await fetch(`${prefix}/page_collection`, {
method: 'POST',
body: JSON.stringify({ url }),
})
const result = (await response.json()) as unknown as ResponseData
if (result.code === 200) {
return (result.result || {}) as PageCollection
}
else {
return {
visitCount: 0,
}
}
}
export function usePageCollection(): PageCollection {
const collection = reactive({
visitCount: 0,
})
const route = useRoute()
const getPageCollection = async (url: string): Promise<void> => {
const { visitCount } = await fetchCollection(url)
collection.visitCount = visitCount
}
onMounted(() => {
setTimeout(async () => {
await getPageCollection(route.path)
}, 0)
})
return collection
}

View File

@ -1,5 +0,0 @@
import Collection from './components/PageCollection.js'
export * from '../shared/index.js'
export { Collection }

View File

@ -1,89 +0,0 @@
/**
* functions
*
* page_collection,
* {host}:{port}/{proxyPrefix}/page_collection
*/
// 引入这个包来提供类型支持
import process from 'node:process'
import type { Handler } from '@netlify/functions'
import * as lean from 'leancloud-storage'
// 通过环境变量来获取 鉴权信息
// 具体的配置,在开发环境中应该写在项目根目录的 `.env` 文件中
// 在生产环境中应该在 netlify 控制台中 配置 环境变量
// 同时,需要在插件的说明文档中 说明需要配置哪些 变量
lean.init({
appId: process.env.LEAN_CLOUD_APP_ID || '',
appKey: process.env.LEAN_CLOUD_APP_KEY || '',
masterKey: process.env.LEAN_CLOUD_MASTER_KEY || '',
})
interface ResponseRes {
statusCode: number
body?: string
message?: string
}
function response(code: number, message: string, data?: Record<string, any>): ResponseRes {
return {
statusCode: 200,
body: JSON.stringify({
code,
result: data || '',
message,
}),
}
}
function successRes(data: Record<string, any>): ResponseRes {
return response(200, 'success', data)
}
function errorRes(message: string, code = 500): ResponseRes {
return response(code, message)
}
// 主要是为了 在 leancloud 中获取 所有权限,便于操作
// 我在 leancloud 控制台中配置的安全策略,默认是所有用户不可读写
// 所以需要在 functions 中用这种方式来获取权限。
const useMasterKey = Boolean(process.env.LEAN_CLOUD_MASTER_KEY)
// netlify functions 的 functions 格式规范
// 通过导出一个 handler 函数作为 钩子
// event 中包含了 请求相关的信息,你可以通过 console.log(event) 查看具体信息
export const handler: Handler = async (event) => {
// body 即为 请求体
const { url } = JSON.parse(event.body || '') || {}
if (!url)
return errorRes('params [url] not found')
// 出于安全考虑,你可能还需要在这里做 域名请求白名单的校验
// 可以在 event.headers 中拿到相关 域名信息
// leancloud 相关这里就不解释了
const query = new lean.Query('Page')
const Page = lean.Object.extend('Page')
try {
query.equalTo('url', url)
const current = await query.first({ useMasterKey })
let page: lean.Object
if (current)
page = lean.Object.createWithoutData('Page', current.get('objectId'))
else
page = new Page()
page.increment('visitCount', 1)
page = await page.save(null, {
useMasterKey,
fetchWhenSave: true,
})
return successRes({
visitCount: page.get('visitCount'),
})
}
catch (e: any) {
return errorRes(e.message, e.code || e.status || e.statusCode)
}
}

View File

@ -1,6 +0,0 @@
import { pageCollectionPlugin } from './plugin.js'
export * from './plugin.js'
export * from '../shared/index.js'
export default pageCollectionPlugin

View File

@ -1,47 +0,0 @@
/**
* functions
*
*
*/
// 通过引入 'vuepress-plugin-netlify-functions' 插件,来为本插件提供
// netlify functions 开发时支持
import type { App, Plugin } from 'vuepress/core'
import { getDirname, path } from 'vuepress/utils'
import { useNetlifyFunctionsPlugin } from 'vuepress-plugin-netlify-functions'
import type { PageCollectionOptions } from '../shared/index.js'
const __dirname = getDirname(import.meta.url)
export function pageCollectionPlugin(_options: PageCollectionOptions = {}): Plugin {
return (app: App) => {
const {
// 客户端发起 functions 请求时的代理前缀
// 默认是 /api
// /api/* 的请求实际被转发到了 netlify functions 服务器的 /.netlify/functions/*
proxyPrefix,
// 在开发环境中,需要将 自定义到 functions 文件,注入到 netlify functions 服务中
// 这样才能够被请求到
preparePluginFunctions,
// vuepress 构建生产包时,打包这些 functions文件到 vuepress dest中。
generatePluginFunctions,
} = useNetlifyFunctionsPlugin(app, {
// 一般来说, 都是设置为 path.join(__dirname, 'functions')
// 即是当前文件同级的 functions 目录
// 这个目录的文件都会被监听,然后注入到 netlify functions 服务中
directory: path.join(__dirname, 'functions'),
})
return {
name: '@vuepress-plume/plugin-page-collection',
define: () => ({
// 将 proxyPrefix 注入到 客户端中
// 以便获取使用
__COLLECTION_PROXY_PREFIX__: proxyPrefix,
}),
clientConfigFile: path.resolve(__dirname, '../client/clientConfig.js'),
// 不要忘了,需要在这里 定义好 钩子
onPrepared: () => preparePluginFunctions(),
onGenerated: () => generatePluginFunctions(),
}
}
}

View File

@ -1 +0,0 @@
export type PageCollectionOptions = Record<string, unknown>

View File

@ -1,8 +0,0 @@
{
"extends": "../tsconfig.build.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib"
},
"include": ["./src"]
}

View File

@ -10,9 +10,7 @@
{ "path": "./plugin-caniuse/tsconfig.build.json" },
{ "path": "./plugin-copy-code/tsconfig.build.json" },
{ "path": "./plugin-iconify/tsconfig.build.json" },
{ "path": "./plugin-netlify-functions/tsconfig.build.json" },
{ "path": "./plugin-notes-data/tsconfig.build.json" },
{ "path": "./plugin-page-collection/tsconfig.build.json" },
{ "path": "./plugin-shikiji/tsconfig.build.json" },
{ "path": "./plugin-content-update/tsconfig.build.json" },
{ "path": "./plugin-search/tsconfig.build.json" },

6743
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -2,4 +2,3 @@ packages:
- docs
- theme
- plugins/*
- '!plugins/plugin-page-collection'

View File

@ -19,9 +19,6 @@
"vuepress-theme-plume": ["./theme/src/node/index.ts"],
"vuepress-theme-plume/composables": ["./theme/src/client/composables/index.ts"],
"@vuepress-plume/*/client": ["./plugins/*/src/client/index.ts"],
"vuepress-plugin-netlify-functions": [
"./plugins/plugin-netlify-functions/src/node/index.ts"
],
"vuepress-plugin-md-power": [
"./plugins/plugin-md-power/src/node/index.ts"
],