@gravito/signal 3.0.3 → 3.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +89 -60
- package/README.zh-TW.md +140 -9
- package/dist/MjmlRenderer-IUH663FT.mjs +8 -0
- package/dist/ReactMjmlRenderer-C3P5YO5L.mjs +8 -0
- package/dist/ReactRenderer-2JFLRVST.mjs +45 -0
- package/dist/{ReactRenderer-L5INVYKT.mjs → ReactRenderer-LYEOSYFS.mjs} +9 -8
- package/dist/ReactRenderer-V54CUUEI.mjs +45 -0
- package/dist/VueMjmlRenderer-4F4CXHDB.mjs +8 -0
- package/dist/VueMjmlRenderer-5WZR4CQG.mjs +8 -0
- package/dist/VueMjmlRenderer-U5YMWI44.mjs +8 -0
- package/dist/VueRenderer-3YBRQXME.mjs +48 -0
- package/dist/VueRenderer-46JGXTJ2.mjs +48 -0
- package/dist/VueRenderer-5KWD4R3C.mjs +48 -0
- package/dist/VueRenderer-C23U4O5E.mjs +48 -0
- package/dist/VueRenderer-LEVDFLHP.mjs +31 -0
- package/dist/VueRenderer-RNHSCCRI.mjs +48 -0
- package/dist/chunk-3WOR3XSL.mjs +82 -0
- package/dist/chunk-DBFIVHHG.mjs +79 -0
- package/dist/{chunk-6DZX6EAA.mjs → chunk-HEBXNMVQ.mjs} +12 -1
- package/dist/chunk-KB7IDDBT.mjs +82 -0
- package/dist/chunk-LZL5UUPC.mjs +82 -0
- package/dist/chunk-W6LXIJKK.mjs +57 -0
- package/dist/chunk-XBIVBJS2.mjs +8 -0
- package/dist/index.d.mts +1680 -209
- package/dist/index.d.ts +1680 -209
- package/dist/index.js +69405 -542
- package/dist/index.mjs +993 -110
- package/dist/lib-HJTRWKU5.mjs +67788 -0
- package/dist/{VueRenderer-Z5PRVBNH.mjs → server-renderer-4IM3P5XZ.mjs} +308 -423
- package/dist/server-renderer-7KWFSTPV.mjs +37193 -0
- package/dist/{VueRenderer-S65ZARRI.mjs → server-renderer-S5FPSTJ2.mjs} +931 -877
- package/dist/server-renderer-X5LUFVWT.mjs +37193 -0
- package/doc/OPTIMIZATION_PLAN.md +496 -0
- package/package.json +14 -12
- package/scripts/check-coverage.ts +64 -0
- package/src/Mailable.ts +340 -44
- package/src/OrbitSignal.ts +350 -50
- package/src/TypedMailable.ts +96 -0
- package/src/dev/DevMailbox.ts +89 -33
- package/src/dev/DevServer.ts +14 -14
- package/src/dev/storage/FileMailboxStorage.ts +66 -0
- package/src/dev/storage/MailboxStorage.ts +15 -0
- package/src/dev/storage/MemoryMailboxStorage.ts +36 -0
- package/src/dev/ui/mailbox.ts +1 -1
- package/src/dev/ui/preview.ts +4 -4
- package/src/errors.ts +69 -0
- package/src/events.ts +72 -0
- package/src/index.ts +20 -1
- package/src/renderers/HtmlRenderer.ts +20 -18
- package/src/renderers/MjmlRenderer.ts +73 -0
- package/src/renderers/ReactMjmlRenderer.ts +94 -0
- package/src/renderers/ReactRenderer.ts +26 -21
- package/src/renderers/Renderer.ts +43 -3
- package/src/renderers/TemplateRenderer.ts +48 -15
- package/src/renderers/VueMjmlRenderer.ts +99 -0
- package/src/renderers/VueRenderer.ts +26 -21
- package/src/renderers/mjml-templates.ts +50 -0
- package/src/transports/BaseTransport.ts +148 -0
- package/src/transports/LogTransport.ts +28 -6
- package/src/transports/MemoryTransport.ts +34 -6
- package/src/transports/SesTransport.ts +62 -17
- package/src/transports/SmtpTransport.ts +123 -27
- package/src/transports/Transport.ts +33 -4
- package/src/types.ts +172 -3
- package/src/utils/html.ts +43 -0
- package/src/webhooks/SendGridWebhookDriver.ts +80 -0
- package/src/webhooks/SesWebhookDriver.ts +44 -0
- package/tests/DevMailbox.test.ts +54 -0
- package/tests/FileMailboxStorage.test.ts +56 -0
- package/tests/MjmlLayout.test.ts +28 -0
- package/tests/MjmlRenderer.test.ts +53 -0
- package/tests/OrbitSignalWebhook.test.ts +56 -0
- package/tests/ReactMjmlRenderer.test.ts +33 -0
- package/tests/SendGridWebhookDriver.test.ts +69 -0
- package/tests/SesWebhookDriver.test.ts +46 -0
- package/tests/VueMjmlRenderer.test.ts +35 -0
- package/tests/dev-server.test.ts +1 -1
- package/tests/transports.test.ts +3 -3
- package/tsconfig.json +12 -24
- package/dist/OrbitMail-2Z7ZTKYA.mjs +0 -7
- package/dist/OrbitMail-BGV32HWN.mjs +0 -7
- package/dist/OrbitMail-FUYZQSAV.mjs +0 -7
- package/dist/OrbitMail-NAPCRK7B.mjs +0 -7
- package/dist/OrbitMail-REGJ276B.mjs +0 -7
- package/dist/OrbitMail-TCFBJWDT.mjs +0 -7
- package/dist/OrbitMail-XZZW6U4N.mjs +0 -7
- package/dist/OrbitSignal-IPSA2CDO.mjs +0 -7
- package/dist/OrbitSignal-MABW4DDW.mjs +0 -7
- package/dist/OrbitSignal-QSW5VQ5M.mjs +0 -7
- package/dist/OrbitSignal-R22QHWAA.mjs +0 -7
- package/dist/OrbitSignal-ZKKMEC27.mjs +0 -7
- package/dist/chunk-3U2CYJO5.mjs +0 -367
- package/dist/chunk-3XFC4T6M.mjs +0 -392
- package/dist/chunk-456QRYFW.mjs +0 -401
- package/dist/chunk-DT3R2TNV.mjs +0 -367
- package/dist/chunk-F6MVTUCT.mjs +0 -421
- package/dist/chunk-GADWIVC4.mjs +0 -400
- package/dist/chunk-HHKFAMSE.mjs +0 -380
- package/dist/chunk-NEQCQSZI.mjs +0 -406
- package/dist/chunk-OKRNL6PN.mjs +0 -400
- package/dist/chunk-ULN3GMY2.mjs +0 -367
- package/dist/chunk-XAWO7RSP.mjs +0 -398
- package/dist/chunk-YLVDJSED.mjs +0 -431
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
# @gravito/signal 優化改進計劃
|
|
2
|
+
|
|
3
|
+
## 目前架構概覽
|
|
4
|
+
|
|
5
|
+
`@gravito/signal` 是 Gravito 框架的郵件服務模組,提供以下核心功能:
|
|
6
|
+
|
|
7
|
+
- **多傳輸層支援**:SMTP、AWS SES、Log、Memory
|
|
8
|
+
- **多渲染引擎**:HTML、Template (Prism)、React、Vue
|
|
9
|
+
- **開發模式**:內建郵件攔截與預覽 UI (`/__mail`)
|
|
10
|
+
- **佇列整合**:透過 `@gravito/stream` 支援非同步發送
|
|
11
|
+
- **國際化支援**:內建翻譯函式整合
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 第一階段:程式碼重構與 JSDoc 增強
|
|
16
|
+
|
|
17
|
+
### 1.1 JSDoc 文檔優化
|
|
18
|
+
|
|
19
|
+
**目標**:增強 AI 可讀性與開發者體驗
|
|
20
|
+
|
|
21
|
+
| 檔案 | 優先級 | 改進內容 |
|
|
22
|
+
|------|--------|----------|
|
|
23
|
+
| `types.ts` | 高 | 補充 `@example`、`@see` 關聯說明 |
|
|
24
|
+
| `Mailable.ts` | 高 | 增加完整使用範例、方法間的關聯說明 |
|
|
25
|
+
| `OrbitSignal.ts` | 高 | 補充配置說明、錯誤處理說明 |
|
|
26
|
+
| `transports/*.ts` | 中 | 增加配置範例、錯誤碼說明 |
|
|
27
|
+
| `renderers/*.ts` | 中 | 補充渲染流程說明 |
|
|
28
|
+
|
|
29
|
+
**具體改進項目**:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// 範例:增強 Mailable 的 JSDoc
|
|
33
|
+
/**
|
|
34
|
+
* Base class for all mailable messages.
|
|
35
|
+
*
|
|
36
|
+
* @description
|
|
37
|
+
* Mailable 提供流式 API 來建構郵件信封並使用多種引擎渲染內容。
|
|
38
|
+
* 支援 HTML、Prism 模板、React 與 Vue 組件渲染。
|
|
39
|
+
*
|
|
40
|
+
* @architecture
|
|
41
|
+
* ```
|
|
42
|
+
* Mailable
|
|
43
|
+
* ├── Envelope (from, to, subject, etc.)
|
|
44
|
+
* ├── Renderer (HtmlRenderer | TemplateRenderer | ReactRenderer | VueRenderer)
|
|
45
|
+
* └── Queueable (佇列支援介面)
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @lifecycle
|
|
49
|
+
* 1. 建立 Mailable 子類別
|
|
50
|
+
* 2. 實作 build() 方法設定信封與內容
|
|
51
|
+
* 3. 呼叫 send() 或 queue() 發送
|
|
52
|
+
*
|
|
53
|
+
* @see {@link OrbitSignal} 郵件服務主類別
|
|
54
|
+
* @see {@link Renderer} 內容渲染器介面
|
|
55
|
+
* @see {@link Queueable} 佇列介面
|
|
56
|
+
*/
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 1.2 stripHtml 方法重複問題
|
|
60
|
+
|
|
61
|
+
**現狀**:`HtmlRenderer` 與 `TemplateRenderer` 各自實作相同的 `stripHtml` 方法
|
|
62
|
+
|
|
63
|
+
**改進方案**:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// 新增 src/utils/html.ts
|
|
67
|
+
/**
|
|
68
|
+
* HTML 工具函式集合
|
|
69
|
+
*
|
|
70
|
+
* @module utils/html
|
|
71
|
+
* @since 3.1.0
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 將 HTML 內容轉換為純文字。
|
|
76
|
+
*
|
|
77
|
+
* @description
|
|
78
|
+
* 移除所有 HTML 標籤、樣式、腳本,並正規化空白字元。
|
|
79
|
+
* 用於產生郵件的純文字版本。
|
|
80
|
+
*
|
|
81
|
+
* @param html - 原始 HTML 字串
|
|
82
|
+
* @returns 純文字內容
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* const text = stripHtml('<h1>Hello</h1><p>World</p>')
|
|
87
|
+
* // Returns: 'Hello World'
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export function stripHtml(html: string): string {
|
|
91
|
+
return html
|
|
92
|
+
.replace(/<style(?:\s[^>]*)?>[\s\S]*?<\/style>/gi, '')
|
|
93
|
+
.replace(/<script(?:\s[^>]*)?>[\s\S]*?<\/script>/gi, '')
|
|
94
|
+
.replace(/<[^>]+>/g, '')
|
|
95
|
+
.replace(/ /g, ' ')
|
|
96
|
+
.replace(/&/g, '&')
|
|
97
|
+
.replace(/</g, '<')
|
|
98
|
+
.replace(/>/g, '>')
|
|
99
|
+
.replace(/"/g, '"')
|
|
100
|
+
.replace(/'/g, "'")
|
|
101
|
+
.replace(/\s+/g, ' ')
|
|
102
|
+
.trim()
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 第二階段:功能增強
|
|
109
|
+
|
|
110
|
+
### 2.1 傳輸層錯誤處理增強
|
|
111
|
+
|
|
112
|
+
**現狀**:傳輸層錯誤直接拋出,缺乏統一的錯誤類型
|
|
113
|
+
|
|
114
|
+
**改進方案**:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// 新增 src/errors.ts
|
|
118
|
+
/**
|
|
119
|
+
* 郵件傳輸相關的錯誤類別
|
|
120
|
+
*
|
|
121
|
+
* @since 3.1.0
|
|
122
|
+
*/
|
|
123
|
+
export class MailTransportError extends Error {
|
|
124
|
+
constructor(
|
|
125
|
+
message: string,
|
|
126
|
+
public readonly code: MailErrorCode,
|
|
127
|
+
public readonly cause?: Error
|
|
128
|
+
) {
|
|
129
|
+
super(message)
|
|
130
|
+
this.name = 'MailTransportError'
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export enum MailErrorCode {
|
|
135
|
+
/** 連線失敗 */
|
|
136
|
+
CONNECTION_FAILED = 'CONNECTION_FAILED',
|
|
137
|
+
/** 認證失敗 */
|
|
138
|
+
AUTH_FAILED = 'AUTH_FAILED',
|
|
139
|
+
/** 收件人被拒 */
|
|
140
|
+
RECIPIENT_REJECTED = 'RECIPIENT_REJECTED',
|
|
141
|
+
/** 訊息被拒 */
|
|
142
|
+
MESSAGE_REJECTED = 'MESSAGE_REJECTED',
|
|
143
|
+
/** 速率限制 */
|
|
144
|
+
RATE_LIMIT = 'RATE_LIMIT',
|
|
145
|
+
/** 未知錯誤 */
|
|
146
|
+
UNKNOWN = 'UNKNOWN',
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 2.2 重試機制
|
|
151
|
+
|
|
152
|
+
**目標**:為傳輸層增加可配置的重試機制
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// Transport 介面擴展
|
|
156
|
+
export interface TransportOptions {
|
|
157
|
+
/** 最大重試次數,預設 3 */
|
|
158
|
+
maxRetries?: number
|
|
159
|
+
/** 重試延遲(毫秒),預設 1000 */
|
|
160
|
+
retryDelay?: number
|
|
161
|
+
/** 指數退避乘數,預設 2 */
|
|
162
|
+
backoffMultiplier?: number
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 新增 BaseTransport 抽象類別
|
|
166
|
+
export abstract class BaseTransport implements Transport {
|
|
167
|
+
protected options: Required<TransportOptions>
|
|
168
|
+
|
|
169
|
+
constructor(options?: TransportOptions) {
|
|
170
|
+
this.options = {
|
|
171
|
+
maxRetries: options?.maxRetries ?? 3,
|
|
172
|
+
retryDelay: options?.retryDelay ?? 1000,
|
|
173
|
+
backoffMultiplier: options?.backoffMultiplier ?? 2,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async send(message: Message): Promise<void> {
|
|
178
|
+
let lastError: Error | undefined
|
|
179
|
+
let delay = this.options.retryDelay
|
|
180
|
+
|
|
181
|
+
for (let attempt = 0; attempt <= this.options.maxRetries; attempt++) {
|
|
182
|
+
try {
|
|
183
|
+
return await this.doSend(message)
|
|
184
|
+
} catch (error) {
|
|
185
|
+
lastError = error as Error
|
|
186
|
+
if (attempt < this.options.maxRetries) {
|
|
187
|
+
await this.sleep(delay)
|
|
188
|
+
delay *= this.options.backoffMultiplier
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
throw new MailTransportError(
|
|
194
|
+
`發送失敗,已重試 ${this.options.maxRetries} 次`,
|
|
195
|
+
MailErrorCode.UNKNOWN,
|
|
196
|
+
lastError
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
protected abstract doSend(message: Message): Promise<void>
|
|
201
|
+
|
|
202
|
+
private sleep(ms: number): Promise<void> {
|
|
203
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### 2.3 事件鉤子系統
|
|
209
|
+
|
|
210
|
+
**目標**:提供郵件生命週期事件鉤子
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// 新增 src/events.ts
|
|
214
|
+
export type MailEventType =
|
|
215
|
+
| 'beforeSend'
|
|
216
|
+
| 'afterSend'
|
|
217
|
+
| 'sendFailed'
|
|
218
|
+
| 'beforeRender'
|
|
219
|
+
| 'afterRender'
|
|
220
|
+
|
|
221
|
+
export interface MailEvent {
|
|
222
|
+
type: MailEventType
|
|
223
|
+
mailable: Mailable
|
|
224
|
+
message?: Message
|
|
225
|
+
error?: Error
|
|
226
|
+
timestamp: Date
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export type MailEventHandler = (event: MailEvent) => void | Promise<void>
|
|
230
|
+
|
|
231
|
+
// OrbitSignal 擴展
|
|
232
|
+
export class OrbitSignal implements GravitoOrbit {
|
|
233
|
+
private eventHandlers = new Map<MailEventType, MailEventHandler[]>()
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* 註冊事件處理器
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```typescript
|
|
240
|
+
* mail.on('afterSend', async (event) => {
|
|
241
|
+
* await analytics.track('email_sent', {
|
|
242
|
+
* to: event.message?.to,
|
|
243
|
+
* subject: event.message?.subject
|
|
244
|
+
* })
|
|
245
|
+
* })
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
248
|
+
on(event: MailEventType, handler: MailEventHandler): this {
|
|
249
|
+
const handlers = this.eventHandlers.get(event) || []
|
|
250
|
+
handlers.push(handler)
|
|
251
|
+
this.eventHandlers.set(event, handlers)
|
|
252
|
+
return this
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private async emit(event: MailEvent): Promise<void> {
|
|
256
|
+
const handlers = this.eventHandlers.get(event.type) || []
|
|
257
|
+
for (const handler of handlers) {
|
|
258
|
+
await handler(event)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## 第三階段:效能優化
|
|
267
|
+
|
|
268
|
+
### 3.1 渲染器快取
|
|
269
|
+
|
|
270
|
+
**目標**:快取已編譯的模板以提升效能
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// TemplateRenderer 增強
|
|
274
|
+
export class TemplateRenderer implements Renderer {
|
|
275
|
+
private static engineCache = new Map<string, TemplateEngine>()
|
|
276
|
+
|
|
277
|
+
private getEngine(): TemplateEngine {
|
|
278
|
+
const cached = TemplateRenderer.engineCache.get(this.viewsDir)
|
|
279
|
+
if (cached) return cached
|
|
280
|
+
|
|
281
|
+
const engine = new TemplateEngine(this.viewsDir)
|
|
282
|
+
TemplateRenderer.engineCache.set(this.viewsDir, engine)
|
|
283
|
+
return engine
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 清除模板引擎快取
|
|
288
|
+
*
|
|
289
|
+
* @description
|
|
290
|
+
* 在開發模式下,當模板檔案變更時呼叫此方法。
|
|
291
|
+
*/
|
|
292
|
+
static clearCache(): void {
|
|
293
|
+
TemplateRenderer.engineCache.clear()
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### 3.2 連線池管理
|
|
299
|
+
|
|
300
|
+
**目標**:為 SMTP 傳輸實作連線池
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// SmtpTransport 增強
|
|
304
|
+
export interface SmtpConfig {
|
|
305
|
+
// ... 現有配置
|
|
306
|
+
/** 連線池大小,預設 5 */
|
|
307
|
+
poolSize?: number
|
|
308
|
+
/** 連線最大閒置時間(毫秒),預設 30000 */
|
|
309
|
+
maxIdleTime?: number
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export class SmtpTransport implements Transport {
|
|
313
|
+
constructor(config: SmtpConfig) {
|
|
314
|
+
this.transporter = nodemailer.createTransport({
|
|
315
|
+
...config,
|
|
316
|
+
pool: true,
|
|
317
|
+
maxConnections: config.poolSize ?? 5,
|
|
318
|
+
maxMessages: Infinity,
|
|
319
|
+
rateDelta: 1000,
|
|
320
|
+
rateLimit: 10,
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* 關閉連線池
|
|
326
|
+
*
|
|
327
|
+
* @description
|
|
328
|
+
* 在應用程式關閉時呼叫以釋放資源。
|
|
329
|
+
*/
|
|
330
|
+
async close(): Promise<void> {
|
|
331
|
+
this.transporter.close()
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* 驗證 SMTP 連線
|
|
336
|
+
*
|
|
337
|
+
* @returns 連線是否成功
|
|
338
|
+
*/
|
|
339
|
+
async verify(): Promise<boolean> {
|
|
340
|
+
try {
|
|
341
|
+
await this.transporter.verify()
|
|
342
|
+
return true
|
|
343
|
+
} catch {
|
|
344
|
+
return false
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## 第四階段:開發者體驗增強
|
|
353
|
+
|
|
354
|
+
### 4.1 Dev UI 功能增強
|
|
355
|
+
|
|
356
|
+
**目標**:增強開發郵件預覽 UI 的功能
|
|
357
|
+
|
|
358
|
+
- [ ] 郵件搜尋功能
|
|
359
|
+
- [ ] 標籤/分類過濾
|
|
360
|
+
- [ ] 響應式預覽(桌面/平板/手機)
|
|
361
|
+
- [ ] 郵件匯出功能(EML 格式)
|
|
362
|
+
- [ ] 批次刪除功能
|
|
363
|
+
- [ ] WebSocket 即時更新
|
|
364
|
+
|
|
365
|
+
### 4.2 CLI 工具
|
|
366
|
+
|
|
367
|
+
**目標**:提供郵件相關的 CLI 命令
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
# 發送測試郵件
|
|
371
|
+
bun gravito mail:test --to test@example.com
|
|
372
|
+
|
|
373
|
+
# 預覽 Mailable
|
|
374
|
+
bun gravito mail:preview WelcomeEmail --data '{"name": "John"}'
|
|
375
|
+
|
|
376
|
+
# 清空開發郵箱
|
|
377
|
+
bun gravito mail:clear
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### 4.3 型別安全增強
|
|
381
|
+
|
|
382
|
+
**目標**:提升 TypeScript 型別推斷
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
// 強型別 Mailable
|
|
386
|
+
export abstract class TypedMailable<TData extends Record<string, unknown>> extends Mailable {
|
|
387
|
+
protected abstract data: TData
|
|
388
|
+
|
|
389
|
+
view<K extends keyof TData>(template: string, data: TData): this {
|
|
390
|
+
// 編譯時型別檢查
|
|
391
|
+
return super.view(template, data)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// 使用範例
|
|
396
|
+
interface WelcomeData {
|
|
397
|
+
name: string
|
|
398
|
+
email: string
|
|
399
|
+
activationUrl: string
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
class WelcomeEmail extends TypedMailable<WelcomeData> {
|
|
403
|
+
protected data: WelcomeData
|
|
404
|
+
|
|
405
|
+
constructor(data: WelcomeData) {
|
|
406
|
+
super()
|
|
407
|
+
this.data = data
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
build() {
|
|
411
|
+
return this
|
|
412
|
+
.to(this.data.email)
|
|
413
|
+
.subject('歡迎加入!')
|
|
414
|
+
.view('welcome', this.data) // 型別安全
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## 第五階段:測試與品質保證
|
|
422
|
+
|
|
423
|
+
### 5.1 測試覆蓋率目標
|
|
424
|
+
|
|
425
|
+
| 模組 | 目前覆蓋率 | 目標覆蓋率 |
|
|
426
|
+
|------|-----------|-----------|
|
|
427
|
+
| OrbitSignal | ~75% | 90% |
|
|
428
|
+
| Mailable | ~70% | 90% |
|
|
429
|
+
| Transports | ~60% | 85% |
|
|
430
|
+
| Renderers | ~65% | 85% |
|
|
431
|
+
| DevServer | ~50% | 80% |
|
|
432
|
+
|
|
433
|
+
### 5.2 新增測試案例
|
|
434
|
+
|
|
435
|
+
- [ ] 傳輸層重試機制測試
|
|
436
|
+
- [ ] 事件鉤子系統測試
|
|
437
|
+
- [ ] 連線池管理測試
|
|
438
|
+
- [ ] 渲染器快取測試
|
|
439
|
+
- [ ] 錯誤處理邊界情況測試
|
|
440
|
+
- [ ] 國際化功能測試
|
|
441
|
+
- [ ] 佇列整合測試
|
|
442
|
+
|
|
443
|
+
### 5.3 效能基準測試
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
// benchmarks/mail-sending.bench.ts
|
|
447
|
+
import { bench, describe } from 'vitest'
|
|
448
|
+
|
|
449
|
+
describe('Mail Sending Performance', () => {
|
|
450
|
+
bench('SmtpTransport - single message', async () => {
|
|
451
|
+
await transport.send(testMessage)
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
bench('SmtpTransport - 100 messages concurrent', async () => {
|
|
455
|
+
await Promise.all(
|
|
456
|
+
Array.from({ length: 100 }, () => transport.send(testMessage))
|
|
457
|
+
)
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
bench('TemplateRenderer - with cache', async () => {
|
|
461
|
+
await renderer.render(testData)
|
|
462
|
+
})
|
|
463
|
+
})
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## 實施時程建議
|
|
469
|
+
|
|
470
|
+
| 階段 | 內容 | 優先級 |
|
|
471
|
+
|------|------|--------|
|
|
472
|
+
| 第一階段 | JSDoc 增強、程式碼重構 | 高 |
|
|
473
|
+
| 第二階段 | 錯誤處理、重試機制、事件系統 | 高 |
|
|
474
|
+
| 第三階段 | 效能優化(快取、連線池) | 中 |
|
|
475
|
+
| 第四階段 | 開發者體驗(Dev UI、CLI) | 中 |
|
|
476
|
+
| 第五階段 | 測試與品質保證 | 高 |
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## 破壞性變更注意事項
|
|
481
|
+
|
|
482
|
+
以下改進可能導致破壞性變更,需要在下一個主版本(4.0)中實施:
|
|
483
|
+
|
|
484
|
+
1. **Transport 介面變更**:增加 `close()` 方法
|
|
485
|
+
2. **錯誤類型變更**:使用自定義錯誤類別
|
|
486
|
+
3. **配置選項變更**:傳輸層配置擴展
|
|
487
|
+
|
|
488
|
+
建議在 3.x 版本中以可選方式引入新功能,在 4.0 版本中設為預設行為。
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## 參考資源
|
|
493
|
+
|
|
494
|
+
- [Nodemailer 文檔](https://nodemailer.com/)
|
|
495
|
+
- [AWS SES 最佳實踐](https://docs.aws.amazon.com/ses/latest/dg/best-practices.html)
|
|
496
|
+
- [郵件傳送最佳實踐](https://postmarkapp.com/guides/best-practices-for-email-delivery)
|
package/package.json
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gravito/signal",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.4",
|
|
4
4
|
"description": "Powerful email framework for Gravito applications with Dev UI and multi-renderer support.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"scripts": {
|
|
9
9
|
"build": "tsup src/index.ts --format cjs,esm --dts --external @gravito/core --external @gravito/stream --external @gravito/prism",
|
|
10
|
-
"test": "bun test",
|
|
11
|
-
"test:coverage": "bun test --coverage --coverage-
|
|
12
|
-
"test:ci": "bun test --coverage --coverage-
|
|
13
|
-
"typecheck": "bun tsc -p tsconfig.json --noEmit --skipLibCheck"
|
|
10
|
+
"test": "bun test --timeout=10000",
|
|
11
|
+
"test:coverage": "bun test --timeout=10000 --coverage --coverage-reporter=lcov --coverage-dir coverage && bun run --bun scripts/check-coverage.ts",
|
|
12
|
+
"test:ci": "bun test --timeout=10000 --coverage --coverage-reporter=lcov --coverage-dir coverage && bun run --bun scripts/check-coverage.ts",
|
|
13
|
+
"typecheck": "bun tsc -p tsconfig.json --noEmit --skipLibCheck",
|
|
14
|
+
"test:unit": "bun test tests/ --timeout=10000",
|
|
15
|
+
"test:integration": "test $(find tests -name '*.integration.test.ts' 2>/dev/null | wc -l) -gt 0 && find tests -name '*.integration.test.ts' -print0 | xargs -0 bun test --timeout=10000 || echo 'No integration tests found'"
|
|
14
16
|
},
|
|
15
17
|
"keywords": [
|
|
16
18
|
"gravito",
|
|
@@ -27,9 +29,9 @@
|
|
|
27
29
|
"nodemailer": "^7.0.11"
|
|
28
30
|
},
|
|
29
31
|
"peerDependencies": {
|
|
30
|
-
"@gravito/core": "
|
|
31
|
-
"@gravito/stream": "
|
|
32
|
-
"@gravito/prism": "
|
|
32
|
+
"@gravito/core": "^1.6.1",
|
|
33
|
+
"@gravito/stream": "^2.0.2",
|
|
34
|
+
"@gravito/prism": "^3.1.1",
|
|
33
35
|
"react": "^19.0.0",
|
|
34
36
|
"react-dom": "^19.0.0",
|
|
35
37
|
"vue": "^3.0.0"
|
|
@@ -49,13 +51,13 @@
|
|
|
49
51
|
}
|
|
50
52
|
},
|
|
51
53
|
"devDependencies": {
|
|
52
|
-
"@gravito/prism": "
|
|
53
|
-
"@gravito/stream": "
|
|
54
|
+
"@gravito/prism": "^3.1.1",
|
|
55
|
+
"@gravito/stream": "^2.0.2",
|
|
54
56
|
"@types/nodemailer": "^6.4.14",
|
|
55
57
|
"@types/react": "^19.0.0",
|
|
56
58
|
"@types/react-dom": "^19.0.0",
|
|
57
59
|
"@vue/server-renderer": "^3.0.0",
|
|
58
|
-
"@gravito/core": "
|
|
60
|
+
"@gravito/core": "^1.6.1",
|
|
59
61
|
"tsup": "8.5.1",
|
|
60
62
|
"typescript": "^5.9.3",
|
|
61
63
|
"vue": "^3.0.0",
|
|
@@ -70,4 +72,4 @@
|
|
|
70
72
|
"url": "git+https://github.com/gravito-framework/gravito.git",
|
|
71
73
|
"directory": "packages/signal"
|
|
72
74
|
}
|
|
73
|
-
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
const lcovPath = process.argv[2] ?? 'coverage/lcov.info'
|
|
5
|
+
const threshold = Number.parseFloat(process.env.COVERAGE_THRESHOLD ?? '80')
|
|
6
|
+
|
|
7
|
+
const root = resolve(process.cwd())
|
|
8
|
+
const srcRoot = `${resolve(root, 'src')}/`
|
|
9
|
+
|
|
10
|
+
// 檢查 lcov.info 是否存在
|
|
11
|
+
if (!existsSync(lcovPath)) {
|
|
12
|
+
console.error(`Coverage file not found: ${lcovPath}`)
|
|
13
|
+
process.exit(1)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let content: string
|
|
17
|
+
try {
|
|
18
|
+
content = readFileSync(lcovPath, 'utf-8')
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error(`Failed to read coverage file: ${error}`)
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const lines = content.split('\n')
|
|
25
|
+
|
|
26
|
+
let currentFile: string | null = null
|
|
27
|
+
let total = 0
|
|
28
|
+
let hit = 0
|
|
29
|
+
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
if (line.startsWith('SF:')) {
|
|
32
|
+
const filePath = line.slice(3).trim()
|
|
33
|
+
const abs = resolve(root, filePath)
|
|
34
|
+
currentFile = abs.startsWith(srcRoot) ? abs : null
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!currentFile) {
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (line.startsWith('DA:')) {
|
|
43
|
+
const parts = line.slice(3).split(',')
|
|
44
|
+
if (parts.length >= 2) {
|
|
45
|
+
total += 1
|
|
46
|
+
const count = Number.parseInt(parts[1] ?? '0', 10)
|
|
47
|
+
if (count > 0) {
|
|
48
|
+
hit += 1
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const percent = total === 0 ? 0 : (hit / total) * 100
|
|
55
|
+
const rounded = Math.round(percent * 100) / 100
|
|
56
|
+
|
|
57
|
+
if (rounded < threshold) {
|
|
58
|
+
console.error(
|
|
59
|
+
`signal coverage ${rounded}% is below threshold ${threshold}%. Covered lines: ${hit}/${total}.`
|
|
60
|
+
)
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(`signal coverage ${rounded}% (${hit}/${total}) meets threshold ${threshold}%.`)
|