feat(plugin-netlify-functions): 新增 netlify-functions 插件

1. 站点部署在 netlify时,提供 netlify functions 支持;
2. 支持functions开发时调试;
3. 支持其他插件使用本插件开发功能;
3. 支持 dotenv
设置环境变量
This commit is contained in:
pengzhanbo 2022-05-09 19:00:50 +08:00
parent f721129a35
commit 10cfbdb80f
20 changed files with 6714 additions and 346 deletions

View File

@ -23,9 +23,12 @@
"commitlint",
"composables",
"Docsearch",
"esbuild",
"gsap",
"iarna",
"nprogress",
"pnpm",
"portfinder",
"Tongji",
"tsbuildinfo",
"vite",

View File

@ -0,0 +1,21 @@
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

@ -0,0 +1,17 @@
# `@vuepress-plume/vuepress-plugin-netlify-functions`
## Install
```
yarn add @vuepress-plume/vuepress-plugin-netlify-functions
```
## Usage
``` js
// .vuepress/config.js
module.exports = {
//...
plugins: [
netlifyFunctionsPlugin()
]
// ...
}
```

View File

@ -0,0 +1,53 @@
{
"name": "@vuepress-plume/vuepress-plugin-netlify-functions",
"version": "1.0.0-beta.30",
"description": "The Plugin for VuePres 2, Support Netlify Functions",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",
"bugs": {
"url": "https://github.com/pengzhanbo/vuepress-theme-plume/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/pengzhanbo/vuepress-theme-plume.git"
},
"license": "MIT",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"main": "lib/node/index.js",
"files": [
"lib"
],
"scripts": {
"build": "pnpm run clean && pnpm run copy && pnpm run ts",
"clean": "rimraf lib *.tsbuildinfo",
"copy": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png}\" lib",
"copy:watch": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png}\" lib -w",
"dev": "concurrently \"pnpm copy:watch\" \"pnpm ts:watch\"",
"ts": "tsc -b tsconfig.build.json",
"ts:watch": "tsc -b tsconfig.build.json --watch"
},
"dependencies": {
"@iarna/toml": "^2.2.5",
"@netlify/functions": "^1.0.0",
"@vuepress/core": "2.0.0-beta.43",
"@vuepress/shared": "2.0.0-beta.43",
"@vuepress/utils": "2.0.0-beta.43",
"chokidar": "^3.5.3",
"cpx2": "^4.2.0",
"dotenv": "^16.0.0",
"esbuild": "^0.14.38",
"execa": "5.1.1",
"netlify-cli": "^10.3.0",
"portfinder": "^1.0.28"
},
"publishConfig": {
"access": "public"
},
"keyword": [
"VuePress",
"vuepress plugin",
"netlify",
"netlify functions",
"netlifyFunctions",
"vuepress-plugin-plugin-netlify-functions"
]
}

View File

@ -0,0 +1,40 @@
import type { App } from '@vuepress/core'
import type { NetlifyFunctionsPluginOptions } from '../shared'
export const 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, isServer, isBuild) => {
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

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

View File

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

View File

@ -0,0 +1,80 @@
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'
import { readFileList } from '../utils'
export const generateFunctions = async (
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 const initialFunctions = async (
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 const 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

@ -0,0 +1,71 @@
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'
export interface NetlifyConfig {
functions: Record<string, any>
redirects: Record<string, any>[]
}
const configName = 'netlify.toml'
const readConfig = (filepath: string): NetlifyConfig => {
let netlifyConfig = ''
if (fs.existsSync(filepath)) {
netlifyConfig = fs.readFileSync(filepath, 'utf-8') || ''
}
return (parse(netlifyConfig) as unknown as NetlifyConfig) || {}
}
const writeConfig = (filepath: string, netlifyConfig: NetlifyConfig): void => {
fs.writeFileSync(
filepath,
stringify(netlifyConfig as unknown as JsonMap),
'utf-8'
)
}
const 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)
}
const 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 const 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

@ -0,0 +1,47 @@
import { fs, path } from '@vuepress/utils'
import dotenv from 'dotenv'
import * as execa from 'execa'
import * as portFinder from 'portfinder'
import type { NetlifyFunctionsPluginOptions } from '../../shared'
const 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 const netlifyServe = async ({
directory,
}: NetlifyFunctionsPluginOptions): Promise<string> => {
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 } = execa(
path.resolve(__dirname, '../../../node_modules/.bin/netlify'),
argv,
{
cwd: process.cwd(),
env: {
...loadEnvConfig(),
},
}
)
stdout?.pipe(process.stdout)
return 'http://localhost:' + port
}

View File

@ -0,0 +1,102 @@
/**
* 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'
import { extendsBundlerOptions } from './extendsBundlerOptions'
import {
generateFunctions,
generateNetlifyConfig,
initialFunctions,
netlifyServe,
} from './netlify'
const initOptions = (
app: App,
{
sourceDirectory,
destDirectory,
proxyPrefix = '/api',
}: NetlifyFunctionsOptions
): NetlifyFunctionsPluginOptions => ({
directory: {
source: [sourceDirectory || app.dir.source('.vuepress/functions')],
dest: destDirectory || app.dir.dest('functions'),
temp: app.dir.temp('functions'),
},
proxyPrefix,
})
const cache = {
options: {},
}
export const getOptions = (): NetlifyFunctionsPluginOptions => {
return cache.options as NetlifyFunctionsPluginOptions
}
/**
*
* netlify function netlify functions
*
* @param options
* @returns
*/
export const netlifyFunctionsPlugin = (
options: NetlifyFunctionsOptions = {}
): Plugin => {
return (app: App) => {
const opts = initOptions(app, options)
cache.options = opts
let server = ''
return {
name: '@vuepress-plume/vuepress-plugin-netlify-functions',
onInitialized: async (app) => {
// 启动netlify functions server
if (!app.env.isBuild) {
server = await netlifyServe(opts)
// 初始化用户侧的 functions
await initialFunctions(app, opts)
}
},
extendsBundlerOptions: (bundlerOption, app: App) => {
extendsBundlerOptions(bundlerOption, app, opts, server)
},
onGenerated: async (app: App) => {
// 生成配置文件
generateNetlifyConfig(app, opts)
await generateFunctions(app, opts)
},
}
}
}

View File

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

View File

@ -0,0 +1,59 @@
import type { App, PluginObject } from '@vuepress/core'
import { path } from '@vuepress/utils'
import * as cpx2 from 'cpx2'
import type { UseNetlifyFunctionPluginsOptions } from '../shared'
import { getOptions, netlifyFunctionsPlugin } from './plugin'
interface UseNetlifyFunctionResult {
/**
* functions
*/
proxyPrefix: string
/**
* onPrepare functions
*/
preparePluginFunctions: () => void
/**
* onGenerate functions dest中
*/
generatePluginFunctions: () => void
}
export const 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-plume/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

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

View File

@ -0,0 +1,20 @@
import { fs, path } from '@vuepress/utils'
export const readFileList = (
source: string,
fileList: string[] = []
): string[] => {
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

@ -0,0 +1,39 @@
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

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"references": [
{
"path": "./tsconfig.cjs.json"
}
],
"files": []
}

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "CommonJS",
"rootDir": "./src",
"outDir": "./lib"
},
"include": ["./src/node", "./src/shared"]
}

6468
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@
"references": [
{ "path": "./packages/theme/tsconfig.build.json" },
{ "path": "./packages/plugin-caniuse/tsconfig.build.json" },
{ "path": "./packages/plugin-copy-code/tsconfig.build.json" }
{ "path": "./packages/plugin-copy-code/tsconfig.build.json" },
{ "path": "./packages/plugin-netlify-functions/tsconfig.build.json" }
]
}