@gravito/signal 1.0.0-beta.1 → 1.0.1
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/dist/ReactRenderer-CMCAOEPH.mjs +28 -0
- package/dist/VueRenderer-DWTCD2RF.mjs +31 -0
- package/dist/index.d.mts +24 -26
- package/dist/index.d.ts +24 -26
- package/dist/index.js +499 -501
- package/dist/index.mjs +493 -15
- package/package.json +17 -16
- package/src/Mailable.ts +44 -10
- package/src/OrbitSignal.ts +28 -64
- package/src/augmentation.ts +1 -1
- package/src/dev/DevServer.ts +135 -73
- package/src/dev/ui/preview.ts +35 -0
- package/src/dev/ui/shared.ts +1 -0
- package/src/renderers/ReactRenderer.ts +8 -3
- package/src/renderers/VueRenderer.ts +10 -3
- package/src/types.ts +11 -0
- package/tests/dev-server.test.ts +66 -0
- package/tests/log-transport.test.ts +21 -0
- package/tests/mailable-extra.test.ts +61 -0
- package/tests/mailable.test.ts +2 -2
- package/tests/orbit-signal.test.ts +43 -0
- package/tests/renderers.test.ts +14 -12
- package/tests/template-renderer.test.ts +24 -0
- package/tests/ui.test.ts +37 -0
- package/tsconfig.json +11 -4
package/src/Mailable.ts
CHANGED
|
@@ -13,6 +13,7 @@ export abstract class Mailable implements Queueable {
|
|
|
13
13
|
protected renderer?: Renderer
|
|
14
14
|
private rendererResolver?: () => Promise<Renderer>
|
|
15
15
|
protected renderData: Record<string, unknown> = {}
|
|
16
|
+
protected config?: MailConfig
|
|
16
17
|
|
|
17
18
|
// ===== Fluent API (Envelope Construction) =====
|
|
18
19
|
|
|
@@ -46,7 +47,7 @@ export abstract class Mailable implements Queueable {
|
|
|
46
47
|
return this
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
emailPriority(level: 'high' | 'normal' | 'low'): this {
|
|
50
51
|
this.envelope.priority = level
|
|
51
52
|
return this
|
|
52
53
|
}
|
|
@@ -82,10 +83,17 @@ export abstract class Mailable implements Queueable {
|
|
|
82
83
|
* Set the content using a React component.
|
|
83
84
|
* Dynamically imports ReactRenderer to avoid hard dependency errors if React is not installed.
|
|
84
85
|
*/
|
|
85
|
-
react<P extends object>(
|
|
86
|
+
react<P extends object>(
|
|
87
|
+
component: ComponentType,
|
|
88
|
+
props?: P,
|
|
89
|
+
deps?: {
|
|
90
|
+
createElement?: (...args: any[]) => any
|
|
91
|
+
renderToStaticMarkup?: (element: any) => string
|
|
92
|
+
}
|
|
93
|
+
): this {
|
|
86
94
|
this.rendererResolver = async () => {
|
|
87
95
|
const { ReactRenderer } = await import('./renderers/ReactRenderer')
|
|
88
|
-
return new ReactRenderer(component, props)
|
|
96
|
+
return new ReactRenderer(component, props, deps)
|
|
89
97
|
}
|
|
90
98
|
return this
|
|
91
99
|
}
|
|
@@ -94,10 +102,18 @@ export abstract class Mailable implements Queueable {
|
|
|
94
102
|
* Set the content using a Vue component.
|
|
95
103
|
* Dynamically imports VueRenderer to avoid hard dependency errors if Vue is not installed.
|
|
96
104
|
*/
|
|
97
|
-
vue<P extends object>(
|
|
105
|
+
vue<P extends object>(
|
|
106
|
+
component: ComponentType,
|
|
107
|
+
props?: P,
|
|
108
|
+
deps?: {
|
|
109
|
+
createSSRApp?: (...args: any[]) => any
|
|
110
|
+
h?: (...args: any[]) => any
|
|
111
|
+
renderToString?: (app: any) => Promise<string>
|
|
112
|
+
}
|
|
113
|
+
): this {
|
|
98
114
|
this.rendererResolver = async () => {
|
|
99
115
|
const { VueRenderer } = await import('./renderers/VueRenderer')
|
|
100
|
-
return new VueRenderer(component, props as any)
|
|
116
|
+
return new VueRenderer(component, props as any, deps)
|
|
101
117
|
}
|
|
102
118
|
return this
|
|
103
119
|
}
|
|
@@ -114,6 +130,7 @@ export abstract class Mailable implements Queueable {
|
|
|
114
130
|
queueName?: string
|
|
115
131
|
connectionName?: string
|
|
116
132
|
delaySeconds?: number
|
|
133
|
+
priority?: number | string
|
|
117
134
|
|
|
118
135
|
onQueue(queue: string): this {
|
|
119
136
|
this.queueName = queue
|
|
@@ -130,13 +147,29 @@ export abstract class Mailable implements Queueable {
|
|
|
130
147
|
return this
|
|
131
148
|
}
|
|
132
149
|
|
|
150
|
+
withPriority(priority: string | number): this {
|
|
151
|
+
this.priority = priority
|
|
152
|
+
return this
|
|
153
|
+
}
|
|
154
|
+
|
|
133
155
|
/**
|
|
134
156
|
* Queue the mailable for sending.
|
|
135
157
|
*/
|
|
136
158
|
async queue(): Promise<void> {
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
159
|
+
// We should ideally use the container to get the mail service
|
|
160
|
+
// But since Mailable might be used outside a core context, we'll try a safe approach.
|
|
161
|
+
try {
|
|
162
|
+
// biome-ignore lint/suspicious/noTsIgnore: Global access to app() helper from core
|
|
163
|
+
// @ts-ignore
|
|
164
|
+
const { app } = await import('@gravito/core')
|
|
165
|
+
const mail = app().container.make<any>('mail')
|
|
166
|
+
if (mail) {
|
|
167
|
+
return mail.queue(this)
|
|
168
|
+
}
|
|
169
|
+
} catch (_e) {
|
|
170
|
+
// Fallback if core is not available
|
|
171
|
+
console.warn('[Mailable] Could not auto-resolve mail service for queuing.')
|
|
172
|
+
}
|
|
140
173
|
}
|
|
141
174
|
|
|
142
175
|
// ===== I18n Support =====
|
|
@@ -178,6 +211,7 @@ export abstract class Mailable implements Queueable {
|
|
|
178
211
|
*/
|
|
179
212
|
async buildEnvelope(configPromise: MailConfig | Promise<MailConfig>): Promise<Envelope> {
|
|
180
213
|
const config = await Promise.resolve(configPromise)
|
|
214
|
+
this.config = config
|
|
181
215
|
|
|
182
216
|
// Inject translator from config if available
|
|
183
217
|
if (config.translator) {
|
|
@@ -188,8 +222,8 @@ export abstract class Mailable implements Queueable {
|
|
|
188
222
|
|
|
189
223
|
// Ensure Renderer is initialized if using TemplateRenderer with config path
|
|
190
224
|
if (this.renderer instanceof TemplateRenderer && config.viewsDir) {
|
|
191
|
-
//
|
|
192
|
-
|
|
225
|
+
// Re-initialize or update TemplateRenderer with the correct directory
|
|
226
|
+
this.renderer = new TemplateRenderer((this.renderer as any).template, config.viewsDir)
|
|
193
227
|
}
|
|
194
228
|
|
|
195
229
|
const envelope: Envelope = {
|
package/src/OrbitSignal.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GravitoOrbit, PlanetCore } from 'gravito
|
|
1
|
+
import type { GravitoOrbit, PlanetCore } from '@gravito/core'
|
|
2
2
|
import { DevMailbox } from './dev/DevMailbox'
|
|
3
3
|
import { DevServer } from './dev/DevServer'
|
|
4
4
|
import type { Mailable } from './Mailable'
|
|
@@ -7,41 +7,12 @@ import { MemoryTransport } from './transports/MemoryTransport'
|
|
|
7
7
|
import type { MailConfig, Message } from './types'
|
|
8
8
|
|
|
9
9
|
export class OrbitSignal implements GravitoOrbit {
|
|
10
|
-
private static instance?: OrbitSignal
|
|
11
10
|
private config: MailConfig
|
|
12
11
|
private devMailbox?: DevMailbox
|
|
12
|
+
private core?: PlanetCore
|
|
13
13
|
|
|
14
14
|
constructor(config: MailConfig = {}) {
|
|
15
15
|
this.config = config
|
|
16
|
-
OrbitSignal.instance = this
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Get the singleton instance of OrbitSignal
|
|
21
|
-
*
|
|
22
|
-
* @returns The singleton instance of OrbitSignal.
|
|
23
|
-
* @throws {Error} If OrbitSignal has not been initialized.
|
|
24
|
-
*/
|
|
25
|
-
static getInstance(): OrbitSignal {
|
|
26
|
-
if (!OrbitSignal.instance) {
|
|
27
|
-
throw new Error('OrbitSignal has not been initialized. Call OrbitSignal.configure() first.')
|
|
28
|
-
}
|
|
29
|
-
return OrbitSignal.instance
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Configure the OrbitSignal instance
|
|
34
|
-
*
|
|
35
|
-
* @param config - The mail configuration object.
|
|
36
|
-
* @returns A new instance of OrbitSignal.
|
|
37
|
-
*/
|
|
38
|
-
static configure(config: MailConfig): OrbitSignal {
|
|
39
|
-
// Basic validation
|
|
40
|
-
if (!config.transport && !config.devMode) {
|
|
41
|
-
console.warn('[OrbitSignal] No transport provided, falling back to LogTransport')
|
|
42
|
-
config.transport = new LogTransport()
|
|
43
|
-
}
|
|
44
|
-
return new OrbitSignal(config)
|
|
45
16
|
}
|
|
46
17
|
|
|
47
18
|
/**
|
|
@@ -50,29 +21,33 @@ export class OrbitSignal implements GravitoOrbit {
|
|
|
50
21
|
* @param core - The PlanetCore instance.
|
|
51
22
|
*/
|
|
52
23
|
install(core: PlanetCore): void {
|
|
53
|
-
core
|
|
24
|
+
this.core = core
|
|
25
|
+
core.logger.info('[OrbitSignal] Initializing Mail Service')
|
|
54
26
|
|
|
55
|
-
// Ensure transport exists (fallback to Log if not set)
|
|
27
|
+
// 1. Ensure transport exists (fallback to Log if not set)
|
|
56
28
|
if (!this.config.transport && !this.config.devMode) {
|
|
57
29
|
this.config.transport = new LogTransport()
|
|
58
30
|
}
|
|
59
31
|
|
|
60
|
-
// In Dev Mode, override transport and setup Dev Server
|
|
32
|
+
// 2. In Dev Mode, override transport and setup Dev Server
|
|
61
33
|
if (this.config.devMode) {
|
|
62
34
|
this.devMailbox = new DevMailbox()
|
|
63
35
|
this.config.transport = new MemoryTransport(this.devMailbox)
|
|
64
36
|
core.logger.info('[OrbitSignal] Dev Mode Enabled: Emails will be intercepted to Dev Mailbox')
|
|
65
37
|
|
|
66
|
-
const devServer = new DevServer(this.devMailbox, this.config.devUiPrefix || '/__mail'
|
|
38
|
+
const devServer = new DevServer(this.devMailbox, this.config.devUiPrefix || '/__mail', {
|
|
39
|
+
allowInProduction: this.config.devUiAllowInProduction,
|
|
40
|
+
gate: this.config.devUiGate,
|
|
41
|
+
})
|
|
67
42
|
devServer.register(core)
|
|
68
43
|
}
|
|
69
44
|
|
|
70
|
-
//
|
|
45
|
+
// 3. Register in container
|
|
46
|
+
core.container.singleton('mail', () => this)
|
|
47
|
+
|
|
48
|
+
// 4. Inject mail service into context for easy access in routes
|
|
71
49
|
core.adapter.use('*', async (c, next) => {
|
|
72
|
-
c.set('mail',
|
|
73
|
-
send: (mailable: Mailable) => this.send(mailable),
|
|
74
|
-
queue: (mailable: Mailable) => this.queue(mailable),
|
|
75
|
-
})
|
|
50
|
+
c.set('mail', this)
|
|
76
51
|
await next()
|
|
77
52
|
return undefined
|
|
78
53
|
})
|
|
@@ -116,38 +91,27 @@ export class OrbitSignal implements GravitoOrbit {
|
|
|
116
91
|
|
|
117
92
|
// 4. Send via transport
|
|
118
93
|
if (!this.config.transport) {
|
|
119
|
-
throw new Error(
|
|
120
|
-
'[OrbitSignal] No transport configured. Did you call configure() or register the orbit?'
|
|
121
|
-
)
|
|
94
|
+
throw new Error('[OrbitSignal] No transport configured. Did you call register the orbit?')
|
|
122
95
|
}
|
|
123
96
|
await this.config.transport.send(message)
|
|
124
97
|
}
|
|
125
98
|
|
|
126
99
|
/**
|
|
127
100
|
* Queue a mailable instance
|
|
128
|
-
*
|
|
129
|
-
* Push a mailable into the queue for execution.
|
|
130
|
-
* Requires OrbitStream to be installed and available in the context.
|
|
131
|
-
*
|
|
132
|
-
* @param mailable - The mailable object to queue.
|
|
133
|
-
* @returns A promise that resolves when the job is pushed to the queue or sent immediately if no queue service is found.
|
|
134
101
|
*/
|
|
135
102
|
async queue(mailable: Mailable): Promise<void> {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
} else {
|
|
146
|
-
// Fallback: send immediately (backward compatible).
|
|
147
|
-
console.warn(
|
|
148
|
-
'[OrbitSignal] Queue service not available, sending immediately. Install OrbitStream to enable queuing.'
|
|
149
|
-
)
|
|
150
|
-
await this.send(mailable)
|
|
103
|
+
try {
|
|
104
|
+
// 嘗試從容器獲取隊列服務 (OrbitStream)
|
|
105
|
+
const queue = this.core?.container.make<any>('queue')
|
|
106
|
+
if (queue) {
|
|
107
|
+
await queue.push(mailable)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
} catch (_e) {
|
|
111
|
+
// 找不到隊列服務時,會拋出錯誤,我們捕捉並降級
|
|
151
112
|
}
|
|
113
|
+
|
|
114
|
+
// Fallback: 直接發送
|
|
115
|
+
await this.send(mailable)
|
|
152
116
|
}
|
|
153
117
|
}
|
package/src/augmentation.ts
CHANGED
package/src/dev/DevServer.ts
CHANGED
|
@@ -1,104 +1,166 @@
|
|
|
1
|
-
import type { PlanetCore } from 'gravito
|
|
1
|
+
import type { GravitoContext, PlanetCore } from '@gravito/core'
|
|
2
2
|
import type { DevMailbox } from './DevMailbox'
|
|
3
3
|
import { getMailboxHtml } from './ui/mailbox'
|
|
4
4
|
import { getPreviewHtml } from './ui/preview'
|
|
5
5
|
|
|
6
|
+
export type DevServerOptions = {
|
|
7
|
+
allowInProduction?: boolean
|
|
8
|
+
gate?: (c: GravitoContext) => boolean | Promise<boolean>
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
export class DevServer {
|
|
7
12
|
constructor(
|
|
8
13
|
private mailbox: DevMailbox,
|
|
9
|
-
private base = '/__mail'
|
|
14
|
+
private base = '/__mail',
|
|
15
|
+
private options?: DevServerOptions
|
|
10
16
|
) {}
|
|
11
17
|
|
|
18
|
+
private async canAccess(ctx: GravitoContext): Promise<boolean> {
|
|
19
|
+
const isProduction = process.env.NODE_ENV === 'production'
|
|
20
|
+
if (isProduction && !this.options?.allowInProduction && !this.options?.gate) {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
if (this.options?.gate) {
|
|
24
|
+
return await this.options.gate(ctx)
|
|
25
|
+
}
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
register(core: PlanetCore): void {
|
|
13
30
|
const router = core.router
|
|
31
|
+
const isProduction = process.env.NODE_ENV === 'production'
|
|
32
|
+
|
|
33
|
+
if (isProduction && !this.options?.allowInProduction && !this.options?.gate) {
|
|
34
|
+
core.logger.warn(
|
|
35
|
+
'[OrbitSignal] Dev Mailbox disabled in production. Configure a gate to allow access.'
|
|
36
|
+
)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
14
39
|
|
|
15
40
|
// Remove trailing slash
|
|
16
41
|
const prefix = this.base.replace(/\/$/, '')
|
|
17
42
|
|
|
43
|
+
const wrap = (handler: (ctx: GravitoContext) => Response | Promise<Response>) => {
|
|
44
|
+
return async (ctx: GravitoContext) => {
|
|
45
|
+
const allowed = await this.canAccess(ctx)
|
|
46
|
+
if (!allowed) {
|
|
47
|
+
return ctx.text('Unauthorized', 403)
|
|
48
|
+
}
|
|
49
|
+
return await handler(ctx)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
18
53
|
// 1. Mailbox List
|
|
19
|
-
router.get(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
54
|
+
router.get(
|
|
55
|
+
prefix,
|
|
56
|
+
wrap((ctx) => {
|
|
57
|
+
const entries = this.mailbox.list()
|
|
58
|
+
ctx.header('Content-Type', 'text/html; charset=utf-8')
|
|
59
|
+
return ctx.html(getMailboxHtml(entries, prefix))
|
|
60
|
+
})
|
|
61
|
+
)
|
|
23
62
|
|
|
24
63
|
// 2. Single Email Preview
|
|
25
|
-
router.get(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
64
|
+
router.get(
|
|
65
|
+
`${prefix}/:id`,
|
|
66
|
+
wrap((ctx) => {
|
|
67
|
+
const id = ctx.req.param('id')
|
|
68
|
+
if (!id) {
|
|
69
|
+
return ctx.text('Bad Request', 400)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const entry = this.mailbox.get(id)
|
|
73
|
+
if (!entry) {
|
|
74
|
+
return ctx.text('Email not found', 404)
|
|
75
|
+
}
|
|
76
|
+
ctx.header('Content-Type', 'text/html; charset=utf-8')
|
|
77
|
+
return ctx.html(getPreviewHtml(entry, prefix))
|
|
78
|
+
})
|
|
79
|
+
)
|
|
37
80
|
|
|
38
81
|
// 3. Iframe Content: HTML
|
|
39
|
-
router.get(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
82
|
+
router.get(
|
|
83
|
+
`${prefix}/:id/html`,
|
|
84
|
+
wrap((ctx) => {
|
|
85
|
+
const id = ctx.req.param('id')
|
|
86
|
+
if (!id) {
|
|
87
|
+
return ctx.text('Bad Request', 400)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const entry = this.mailbox.get(id)
|
|
91
|
+
if (!entry) {
|
|
92
|
+
return ctx.text('Not found', 404)
|
|
93
|
+
}
|
|
94
|
+
ctx.header('Content-Type', 'text/html; charset=utf-8')
|
|
95
|
+
return ctx.html(entry.html)
|
|
96
|
+
})
|
|
97
|
+
)
|
|
51
98
|
|
|
52
99
|
// 4. Iframe Content: Text
|
|
53
|
-
router.get(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
100
|
+
router.get(
|
|
101
|
+
`${prefix}/:id/text`,
|
|
102
|
+
wrap((ctx) => {
|
|
103
|
+
const id = ctx.req.param('id')
|
|
104
|
+
if (!id) {
|
|
105
|
+
return ctx.text('Bad Request', 400)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const entry = this.mailbox.get(id)
|
|
109
|
+
if (!entry) {
|
|
110
|
+
return ctx.text('Not found', 404)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
ctx.header('Content-Type', 'text/plain; charset=utf-8')
|
|
114
|
+
return ctx.text(entry.text || 'No text content', 200)
|
|
115
|
+
})
|
|
116
|
+
)
|
|
67
117
|
|
|
68
118
|
// 5. Raw JSON
|
|
69
|
-
router.get(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
119
|
+
router.get(
|
|
120
|
+
`${prefix}/:id/raw`,
|
|
121
|
+
wrap((ctx) => {
|
|
122
|
+
const id = ctx.req.param('id')
|
|
123
|
+
if (!id) {
|
|
124
|
+
return ctx.json({ error: 'Bad Request' }, 400)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const entry = this.mailbox.get(id)
|
|
128
|
+
if (!entry) {
|
|
129
|
+
return ctx.json({ error: 'Not found' }, 404)
|
|
130
|
+
}
|
|
131
|
+
return ctx.json(entry)
|
|
132
|
+
})
|
|
133
|
+
)
|
|
81
134
|
|
|
82
135
|
// 6. API: Delete Single
|
|
83
|
-
router.get(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
136
|
+
router.get(
|
|
137
|
+
`${prefix}/:id/delete`,
|
|
138
|
+
wrap((ctx) => {
|
|
139
|
+
return ctx.text('Method not allowed', 405)
|
|
140
|
+
})
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
router.delete(
|
|
144
|
+
`${prefix}/:id`,
|
|
145
|
+
wrap((ctx) => {
|
|
146
|
+
const id = ctx.req.param('id')
|
|
147
|
+
if (!id) {
|
|
148
|
+
return ctx.json({ success: false, error: 'Bad Request' }, 400)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const success = this.mailbox.delete(id)
|
|
152
|
+
return ctx.json({ success })
|
|
153
|
+
})
|
|
154
|
+
)
|
|
96
155
|
|
|
97
156
|
// 7. API: Clear All
|
|
98
|
-
router.delete(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
157
|
+
router.delete(
|
|
158
|
+
prefix,
|
|
159
|
+
wrap((ctx) => {
|
|
160
|
+
this.mailbox.clear()
|
|
161
|
+
return ctx.json({ success: true })
|
|
162
|
+
})
|
|
163
|
+
)
|
|
102
164
|
|
|
103
165
|
core.logger.info(`[OrbitSignal] Dev Mailbox available at ${prefix}`)
|
|
104
166
|
}
|
package/src/dev/ui/preview.ts
CHANGED
|
@@ -20,7 +20,42 @@ export function getPreviewHtml(entry: MailboxEntry, prefix: string): string {
|
|
|
20
20
|
<div style="font-size: 18px; font-weight: bold; margin-bottom: 10px;">${entry.envelope.subject || '(No Subject)'}</div>
|
|
21
21
|
<div class="meta" style="margin-bottom: 5px;">From: ${from}</div>
|
|
22
22
|
<div class="meta" style="margin-bottom: 5px;">To: ${to}</div>
|
|
23
|
+
${
|
|
24
|
+
entry.envelope.cc
|
|
25
|
+
? `<div class="meta" style="margin-bottom: 5px;">CC: ${entry.envelope.cc.map((t) => t.address).join(', ')}</div>`
|
|
26
|
+
: ''
|
|
27
|
+
}
|
|
28
|
+
${
|
|
29
|
+
entry.envelope.bcc
|
|
30
|
+
? `<div class="meta" style="margin-bottom: 5px;">BCC: ${entry.envelope.bcc.map((t) => t.address).join(', ')}</div>`
|
|
31
|
+
: ''
|
|
32
|
+
}
|
|
33
|
+
${entry.envelope.priority ? `<div class="meta" style="margin-bottom: 5px;">Priority: ${entry.envelope.priority}</div>` : ''}
|
|
23
34
|
<div class="meta">Date: ${entry.sentAt.toLocaleString()}</div>
|
|
35
|
+
${
|
|
36
|
+
entry.envelope.attachments && entry.envelope.attachments.length > 0
|
|
37
|
+
? `
|
|
38
|
+
<div style="margin-top: 15px; border-top: 1px solid var(--border); padding-top: 10px;">
|
|
39
|
+
<div style="font-size: 14px; font-weight: bold; margin-bottom: 5px; color: var(--text-muted);">Attachments (${entry.envelope.attachments.length})</div>
|
|
40
|
+
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
|
41
|
+
${entry.envelope.attachments
|
|
42
|
+
.map(
|
|
43
|
+
(att) => `
|
|
44
|
+
<div style="background: var(--bg-dark); padding: 8px 12px; border-radius: 6px; border: 1px solid var(--border); display: flex; align-items: center; gap: 8px;">
|
|
45
|
+
<span style="font-size: 20px;">📎</span>
|
|
46
|
+
<div>
|
|
47
|
+
<div style="font-size: 14px; font-weight: 500;">${att.filename || 'untitled'}</div>
|
|
48
|
+
<div style="font-size: 12px; color: var(--text-muted);">${att.contentType || 'application/octet-stream'}</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
`
|
|
52
|
+
)
|
|
53
|
+
.join('')}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
`
|
|
57
|
+
: ''
|
|
58
|
+
}
|
|
24
59
|
</div>
|
|
25
60
|
|
|
26
61
|
<div style="margin-bottom: 10px;">
|
package/src/dev/ui/shared.ts
CHANGED
|
@@ -3,13 +3,18 @@ import type { Renderer, RenderResult } from './Renderer'
|
|
|
3
3
|
export class ReactRenderer<P extends object = object> implements Renderer {
|
|
4
4
|
constructor(
|
|
5
5
|
private component: any, // Use any to avoid hard React dependency in types
|
|
6
|
-
private props?: P
|
|
6
|
+
private props?: P,
|
|
7
|
+
private deps: {
|
|
8
|
+
createElement?: (...args: any[]) => any
|
|
9
|
+
renderToStaticMarkup?: (element: any) => string
|
|
10
|
+
} = {}
|
|
7
11
|
) {}
|
|
8
12
|
|
|
9
13
|
async render(data: Record<string, unknown>): Promise<RenderResult> {
|
|
10
14
|
// Dynamic imports to avoid hard dependencies on react/react-dom
|
|
11
|
-
const
|
|
12
|
-
const
|
|
15
|
+
const createElement = this.deps.createElement ?? (await import('react')).createElement
|
|
16
|
+
const renderToStaticMarkup =
|
|
17
|
+
this.deps.renderToStaticMarkup ?? (await import('react-dom/server')).renderToStaticMarkup
|
|
13
18
|
|
|
14
19
|
const mergedProps = { ...this.props, ...data } as P
|
|
15
20
|
|
|
@@ -3,13 +3,20 @@ import type { Renderer, RenderResult } from './Renderer'
|
|
|
3
3
|
export class VueRenderer<P extends object = object> implements Renderer {
|
|
4
4
|
constructor(
|
|
5
5
|
private component: any, // Use any to avoid hard Vue dependency in types
|
|
6
|
-
private props?: P
|
|
6
|
+
private props?: P,
|
|
7
|
+
private deps: {
|
|
8
|
+
createSSRApp?: (...args: any[]) => any
|
|
9
|
+
h?: (...args: any[]) => any
|
|
10
|
+
renderToString?: (app: any) => Promise<string>
|
|
11
|
+
} = {}
|
|
7
12
|
) {}
|
|
8
13
|
|
|
9
14
|
async render(data: Record<string, unknown>): Promise<RenderResult> {
|
|
10
15
|
// Dynamic imports to avoid hard dependencies on vue/@vue/server-renderer
|
|
11
|
-
const
|
|
12
|
-
const
|
|
16
|
+
const createSSRApp = this.deps.createSSRApp ?? (await import('vue')).createSSRApp
|
|
17
|
+
const h = this.deps.h ?? (await import('vue')).h
|
|
18
|
+
const renderToString =
|
|
19
|
+
this.deps.renderToString ?? (await import('@vue/server-renderer')).renderToString
|
|
13
20
|
|
|
14
21
|
const mergedProps = { ...this.props, ...data }
|
|
15
22
|
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GravitoContext } from '@gravito/core'
|
|
1
2
|
import type { Transport } from './transports/Transport'
|
|
2
3
|
export type { Transport }
|
|
3
4
|
|
|
@@ -62,6 +63,16 @@ export interface MailConfig {
|
|
|
62
63
|
*/
|
|
63
64
|
devUiPrefix?: string | undefined
|
|
64
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Allow Dev UI in production. Default: false.
|
|
68
|
+
*/
|
|
69
|
+
devUiAllowInProduction?: boolean | undefined
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Gate access to Dev UI (required in production unless allowInProduction is true).
|
|
73
|
+
*/
|
|
74
|
+
devUiGate?: ((ctx: GravitoContext) => boolean | Promise<boolean>) | undefined
|
|
75
|
+
|
|
65
76
|
/**
|
|
66
77
|
* Translation function for i18n support
|
|
67
78
|
*/
|