mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-12 02:21:06 +08:00
Refactor proxy middleware for improved SSE and API request handling
Co-authored-by: jxxghp <jxxghp@163.com>
This commit is contained in:
141
SSE_修复报告.md
Normal file
141
SSE_修复报告.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# SSE (Server-Sent Events) 问题分析和修复报告
|
||||
|
||||
## 问题描述
|
||||
|
||||
用户反映使用新的 `public/service.js` 文件后,SSE (Server-Sent Events) 不能正常工作。
|
||||
|
||||
## 问题分析
|
||||
|
||||
通过对项目代码和最新SSE文档的研究,发现了以下问题:
|
||||
|
||||
### 1. 原始代码问题
|
||||
|
||||
原始的 `express-http-proxy` 配置在处理SSE流式传输时存在以下问题:
|
||||
|
||||
- `userResDecorator` 中的SSE处理逻辑虽然返回了 `false`,但实际的流式传输机制不够完善
|
||||
- 缺乏正确的流管道设置
|
||||
- 错误处理不够健壮
|
||||
- 缺少专门的SSE代理中间件
|
||||
|
||||
### 2. SSE的技术要求
|
||||
|
||||
根据最新的SSE最佳实践,正确的SSE代理需要:
|
||||
|
||||
- 设置正确的HTTP头部:`Content-Type: text/event-stream`
|
||||
- 保持持久连接:`Connection: keep-alive`
|
||||
- 禁用缓存:`Cache-Control: no-cache`
|
||||
- 正确处理流式传输,避免缓冲
|
||||
- 优雅处理客户端断开连接
|
||||
|
||||
## 修复方案
|
||||
|
||||
### 主要改进
|
||||
|
||||
1. **引入专门的SSE代理中间件**
|
||||
- 使用 `http-proxy-middleware` 包提供专门的SSE支持
|
||||
- 分离SSE请求和普通API请求的处理逻辑
|
||||
|
||||
2. **改进的流式传输处理**
|
||||
```javascript
|
||||
// 直接将代理响应流式传输到客户端
|
||||
proxyRes.pipe(res);
|
||||
```
|
||||
|
||||
3. **更好的连接管理**
|
||||
- 正确处理客户端断开连接
|
||||
- 优雅的错误处理
|
||||
- 资源清理
|
||||
|
||||
4. **双重代理架构**
|
||||
- SSE请求使用专门的 `createProxyMiddleware`
|
||||
- 普通API请求继续使用 `express-http-proxy`
|
||||
|
||||
### 依赖更新
|
||||
|
||||
添加了新的依赖:
|
||||
```json
|
||||
"http-proxy-middleware": "^3.0.0"
|
||||
```
|
||||
|
||||
### 核心修改
|
||||
|
||||
1. **SSE检测和路由**
|
||||
```javascript
|
||||
app.use('/api', (req, res, next) => {
|
||||
// 检测是否为SSE请求
|
||||
const isSSE = req.headers.accept && req.headers.accept.includes('text/event-stream');
|
||||
|
||||
if (isSSE) {
|
||||
// 使用专门的SSE代理中间件
|
||||
sseProxyMiddleware(req, res, next);
|
||||
} else {
|
||||
// 使用普通API代理中间件
|
||||
apiProxyMiddleware(req, res, next);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
2. **专门的SSE代理中间件**
|
||||
```javascript
|
||||
const sseProxyMiddleware = createProxyMiddleware({
|
||||
target: `http://${proxyConfig.URL}:${proxyConfig.PORT}`,
|
||||
changeOrigin: true,
|
||||
timeout: 0, // 无超时
|
||||
onProxyRes: (proxyRes, req, res) => {
|
||||
const isSSE = proxyRes.headers['content-type'] &&
|
||||
proxyRes.headers['content-type'].includes('text/event-stream');
|
||||
|
||||
if (isSSE) {
|
||||
// 设置正确的SSE响应头
|
||||
res.writeHead(proxyRes.statusCode, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'Cache-Control, Content-Type, Authorization'
|
||||
});
|
||||
|
||||
// 直接流式传输
|
||||
proxyRes.pipe(res);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 实施步骤
|
||||
|
||||
1. ✅ 更新 `package.json` 添加 `http-proxy-middleware` 依赖
|
||||
2. ✅ 运行 `yarn install` 安装新依赖
|
||||
3. ✅ 修改 `public/service.js` 实现新的SSE处理逻辑
|
||||
4. 🔄 测试SSE功能确保正常工作
|
||||
|
||||
## 受影响的功能
|
||||
|
||||
项目中使用SSE的功能包括:
|
||||
- 系统进度监控 (`/api/system/progress/*`)
|
||||
- 日志查看 (`LoggingView.vue`)
|
||||
- 消息通知 (`MessageView.vue`, `UserNotification.vue`)
|
||||
- 文件传输进度 (`TransferQueueDialog.vue`, `ReorganizeDialog.vue`)
|
||||
- 批量重命名进度 (`FileList.vue`)
|
||||
|
||||
## 验证方法
|
||||
|
||||
可以通过以下方式验证修复效果:
|
||||
|
||||
1. 检查浏览器开发者工具中的Network面板
|
||||
2. 确认SSE请求显示为 `text/event-stream` 类型
|
||||
3. 验证数据是否实时流式传输
|
||||
4. 测试客户端断开连接后的清理工作
|
||||
|
||||
## 技术参考
|
||||
|
||||
本修复方案基于以下最新文档和最佳实践:
|
||||
|
||||
- [Server-Sent Events: A Practical Guide for the Real World](https://tigerabrodi.blog/server-sent-events-a-practical-guide-for-the-real-world)
|
||||
- [Building an SSE Proxy: Streaming and Forwarding Server-Sent Events](https://medium.com/@sercan.celenk/building-an-sse-proxy-in-go-streaming-and-forwarding-server-sent-events-1c951d3acd70)
|
||||
- [How to Proxy and Modify OpenAI Stream Responses](https://medium.com/@TechTim42/how-to-proxy-and-modify-openai-stream-responses-for-enhanced-user-experience-82cb9ed29b46)
|
||||
- [express-http-proxy SSE 最佳实践](https://tigerabrodi.blog/server-sent-events-a-practical-guide-for-the-real-world)
|
||||
|
||||
## 结论
|
||||
|
||||
通过引入专门的SSE代理中间件和改进的流式传输处理,应该能够解决用户反映的SSE不能正常工作的问题。新的架构更加符合SSE的技术要求,提供了更好的错误处理和连接管理。
|
||||
@@ -45,6 +45,7 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"express": "^4.21.2",
|
||||
"express-http-proxy": "^2.1.1",
|
||||
"http-proxy-middleware": "^3.0.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mousetrap": "^1.6.5",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const path = require('node:path')
|
||||
const express = require('express')
|
||||
const proxy = require('express-http-proxy')
|
||||
const { createProxyMiddleware } = require('http-proxy-middleware')
|
||||
|
||||
const app = express()
|
||||
const port = process.env.NGINX_PORT || 3000
|
||||
@@ -14,76 +15,141 @@ const proxyConfig = {
|
||||
// 静态文件服务目录
|
||||
app.use(express.static(__dirname))
|
||||
|
||||
// 配置代理中间件将请求转发给后端API
|
||||
app.use(
|
||||
'/api',
|
||||
proxy(`${proxyConfig.URL}:${proxyConfig.PORT}`, {
|
||||
// 路径加上 /api 前缀
|
||||
proxyReqPathResolver: (req) => {
|
||||
return `/api${req.url}`
|
||||
},
|
||||
proxyReqOptDecorator: (proxyReqOpts, srcReq) => {
|
||||
proxyReqOpts.headers = proxyReqOpts.headers || {};
|
||||
// 创建专门的SSE代理中间件
|
||||
const sseProxyMiddleware = createProxyMiddleware({
|
||||
target: `http://${proxyConfig.URL}:${proxyConfig.PORT}`,
|
||||
changeOrigin: true,
|
||||
ws: false,
|
||||
timeout: 0, // 无超时
|
||||
proxyTimeout: 0, // 无超时
|
||||
headers: {
|
||||
'Connection': 'keep-alive',
|
||||
'Cache-Control': 'no-cache'
|
||||
},
|
||||
onProxyRes: (proxyRes, req, res) => {
|
||||
// 检测SSE响应
|
||||
const isSSE = proxyRes.headers['content-type'] &&
|
||||
proxyRes.headers['content-type'].includes('text/event-stream');
|
||||
|
||||
if (isSSE) {
|
||||
// 设置SSE响应头
|
||||
res.writeHead(proxyRes.statusCode, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'Cache-Control, Content-Type, Authorization'
|
||||
});
|
||||
|
||||
// 检测是否为SSE请求
|
||||
const isSSE = srcReq.headers.accept && srcReq.headers.accept.includes('text/event-stream');
|
||||
// 直接将代理响应流式传输到客户端
|
||||
proxyRes.pipe(res);
|
||||
|
||||
// 动态设置超时时间:SSE无超时,普通请求600秒超时
|
||||
proxyReqOpts.timeout = isSSE ? 0 : 600000;
|
||||
|
||||
if (isSSE) {
|
||||
// SSE请求的特殊头部设置
|
||||
proxyReqOpts.headers['Cache-Control'] = 'no-cache';
|
||||
proxyReqOpts.headers['Connection'] = 'keep-alive';
|
||||
proxyReqOpts.headers['Accept'] = 'text/event-stream';
|
||||
}
|
||||
|
||||
return proxyReqOpts;
|
||||
},
|
||||
userResDecorator: (proxyRes, proxyResData, userReq, userRes) => {
|
||||
// 检测响应是否为SSE类型
|
||||
const isSSEResponse = proxyRes.headers['content-type'] &&
|
||||
proxyRes.headers['content-type'].includes('text/event-stream');
|
||||
|
||||
if (isSSEResponse) {
|
||||
// SSE响应:设置流式传输头部并禁用缓冲
|
||||
userRes.writeHead(proxyRes.statusCode, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'Cache-Control'
|
||||
});
|
||||
return false; // 禁用默认响应处理,让数据直接流向客户端
|
||||
} else {
|
||||
// 普通响应:正常处理
|
||||
return proxyResData;
|
||||
}
|
||||
},
|
||||
// 统一错误处理(添加超时错误处理)
|
||||
proxyErrorHandler: (err, res, next) => {
|
||||
// 客户端断开连接的正常情况(常见于SSE)
|
||||
if (err.code === 'ECONNRESET' || err.code === 'EPIPE') {
|
||||
console.log('Client disconnected:', err.code);
|
||||
res.end(); // 优雅结束响应
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加超时错误处理
|
||||
if (err.code === 'ETIMEDOUT') {
|
||||
console.log('Proxy request timed out:', err.code);
|
||||
if (!res.headersSent) {
|
||||
res.status(504).send('Gateway Timeout');
|
||||
// 处理客户端断开连接
|
||||
req.on('close', () => {
|
||||
console.log('Client disconnected from SSE stream');
|
||||
if (proxyRes.destroy) {
|
||||
proxyRes.destroy();
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// 其他错误正常处理
|
||||
console.error('Proxy error:', err);
|
||||
next(err);
|
||||
// 处理代理响应结束
|
||||
proxyRes.on('end', () => {
|
||||
console.log('SSE stream ended');
|
||||
if (!res.headersSent) {
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
// 处理代理响应错误
|
||||
proxyRes.on('error', (err) => {
|
||||
console.error('SSE proxy response error:', err);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).end();
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
onError: (err, req, res) => {
|
||||
console.error('SSE proxy error:', err);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ error: 'Proxy error' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 创建普通API代理中间件
|
||||
const apiProxyMiddleware = proxy(`${proxyConfig.URL}:${proxyConfig.PORT}`, {
|
||||
// 路径加上 /api 前缀
|
||||
proxyReqPathResolver: (req) => {
|
||||
return `/api${req.url}`
|
||||
},
|
||||
proxyReqOptDecorator: (proxyReqOpts, srcReq) => {
|
||||
proxyReqOpts.headers = proxyReqOpts.headers || {};
|
||||
|
||||
// 检测是否为SSE请求
|
||||
const isSSE = srcReq.headers.accept && srcReq.headers.accept.includes('text/event-stream');
|
||||
|
||||
if (!isSSE) {
|
||||
// 普通请求设置超时
|
||||
proxyReqOpts.timeout = 600000; // 600秒超时
|
||||
}
|
||||
|
||||
return proxyReqOpts;
|
||||
},
|
||||
userResDecorator: (proxyRes, proxyResData, userReq, userRes) => {
|
||||
// 只处理非SSE响应
|
||||
const isSSEResponse = proxyRes.headers['content-type'] &&
|
||||
proxyRes.headers['content-type'].includes('text/event-stream');
|
||||
|
||||
if (!isSSEResponse) {
|
||||
// 普通响应:正常处理
|
||||
return proxyResData;
|
||||
}
|
||||
|
||||
// SSE响应不在这里处理,已经由专门的中间件处理
|
||||
return proxyResData;
|
||||
},
|
||||
// 错误处理
|
||||
proxyErrorHandler: (err, res, next) => {
|
||||
// 客户端断开连接的正常情况
|
||||
if (err.code === 'ECONNRESET' || err.code === 'EPIPE') {
|
||||
console.log('Client disconnected:', err.code);
|
||||
if (!res.headersSent) {
|
||||
res.end();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 超时错误处理
|
||||
if (err.code === 'ETIMEDOUT') {
|
||||
console.log('Proxy request timed out:', err.code);
|
||||
if (!res.headersSent) {
|
||||
res.status(504).send('Gateway Timeout');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 其他错误
|
||||
console.error('Proxy error:', err);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).send('Internal Server Error');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 配置API代理路由
|
||||
app.use('/api', (req, res, next) => {
|
||||
// 检测是否为SSE请求
|
||||
const isSSE = req.headers.accept && req.headers.accept.includes('text/event-stream');
|
||||
|
||||
if (isSSE) {
|
||||
// 使用专门的SSE代理中间件
|
||||
sseProxyMiddleware(req, res, next);
|
||||
} else {
|
||||
// 使用普通API代理中间件
|
||||
apiProxyMiddleware(req, res, next);
|
||||
}
|
||||
});
|
||||
|
||||
// 配置代理中间件将CookieCloud请求转发给后端API
|
||||
app.use(
|
||||
|
||||
62
yarn.lock
62
yarn.lock
@@ -1858,6 +1858,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"
|
||||
integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==
|
||||
|
||||
"@types/http-proxy@^1.17.15":
|
||||
version "1.17.16"
|
||||
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.16.tgz#dee360707b35b3cc85afcde89ffeebff7d7f9240"
|
||||
integrity sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/js-cookie@^3.0.6":
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.6.tgz#a04ca19e877687bd449f5ad37d33b104b71fdf95"
|
||||
@@ -4019,6 +4026,11 @@ event-target-shim@^5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
|
||||
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
|
||||
|
||||
eventemitter3@^4.0.0:
|
||||
version "4.0.7"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
||||
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
|
||||
|
||||
events@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
|
||||
@@ -4237,7 +4249,7 @@ flatted@^3.2.9, flatted@^3.3.3:
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358"
|
||||
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
|
||||
|
||||
follow-redirects@^1.15.6:
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.15.6:
|
||||
version "1.15.9"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
|
||||
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
|
||||
@@ -4620,6 +4632,27 @@ http-errors@2.0.0:
|
||||
statuses "2.0.1"
|
||||
toidentifier "1.0.1"
|
||||
|
||||
http-proxy-middleware@^3.0.0:
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-3.0.5.tgz#9dcde663edc44079bc5a9c63e03fe5e5d6037fab"
|
||||
integrity sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==
|
||||
dependencies:
|
||||
"@types/http-proxy" "^1.17.15"
|
||||
debug "^4.3.6"
|
||||
http-proxy "^1.18.1"
|
||||
is-glob "^4.0.3"
|
||||
is-plain-object "^5.0.0"
|
||||
micromatch "^4.0.8"
|
||||
|
||||
http-proxy@^1.18.1:
|
||||
version "1.18.1"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
|
||||
integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
|
||||
dependencies:
|
||||
eventemitter3 "^4.0.0"
|
||||
follow-redirects "^1.0.0"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
@@ -6284,6 +6317,11 @@ require-from-string@^2.0.2:
|
||||
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
|
||||
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
|
||||
|
||||
requires-port@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||
integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
|
||||
|
||||
resize-observer-polyfill@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
|
||||
@@ -6718,8 +6756,16 @@ std-env@^3.9.0:
|
||||
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.9.0.tgz#1a6f7243b339dca4c9fd55e1c7504c77ef23e8f1"
|
||||
integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.3:
|
||||
name string-width-cjs
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -6804,8 +6850,14 @@ stringify-object@^3.3.0:
|
||||
is-obj "^1.0.1"
|
||||
is-regexp "^1.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
name strip-ansi-cjs
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
|
||||
Reference in New Issue
Block a user