@gravito/signal 3.1.0 → 3.1.2

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.
Files changed (68) hide show
  1. package/dist/index.cjs +55126 -81925
  2. package/dist/index.d.ts +513 -1
  3. package/dist/index.js +61492 -0
  4. package/dist/index.mjs +55301 -82110
  5. package/package.json +12 -1
  6. package/CHANGELOG.md +0 -74
  7. package/build.ts +0 -133
  8. package/dist/index.cjs.map +0 -712
  9. package/dist/index.mjs.map +0 -710
  10. package/doc/ADVANCED_RENDERING.md +0 -71
  11. package/doc/DISTRIBUTED_MESSAGING.md +0 -79
  12. package/doc/OPTIMIZATION_PLAN.md +0 -496
  13. package/package.json.bak +0 -75
  14. package/scripts/check-coverage.ts +0 -64
  15. package/src/Mailable.ts +0 -674
  16. package/src/OrbitSignal.ts +0 -451
  17. package/src/Queueable.ts +0 -9
  18. package/src/TypedMailable.ts +0 -96
  19. package/src/dev/DevMailbox.ts +0 -146
  20. package/src/dev/DevServer.ts +0 -192
  21. package/src/dev/storage/FileMailboxStorage.ts +0 -66
  22. package/src/dev/storage/MailboxStorage.ts +0 -15
  23. package/src/dev/storage/MemoryMailboxStorage.ts +0 -36
  24. package/src/dev/ui/mailbox.ts +0 -77
  25. package/src/dev/ui/preview.ts +0 -103
  26. package/src/dev/ui/shared.ts +0 -60
  27. package/src/errors.ts +0 -69
  28. package/src/events.ts +0 -72
  29. package/src/index.ts +0 -41
  30. package/src/renderers/HtmlRenderer.ts +0 -41
  31. package/src/renderers/MjmlRenderer.ts +0 -73
  32. package/src/renderers/ReactMjmlRenderer.ts +0 -94
  33. package/src/renderers/ReactRenderer.ts +0 -66
  34. package/src/renderers/Renderer.ts +0 -67
  35. package/src/renderers/TemplateRenderer.ts +0 -84
  36. package/src/renderers/VueMjmlRenderer.ts +0 -99
  37. package/src/renderers/VueRenderer.ts +0 -71
  38. package/src/renderers/mjml-templates.ts +0 -50
  39. package/src/transports/BaseTransport.ts +0 -148
  40. package/src/transports/LogTransport.ts +0 -55
  41. package/src/transports/MemoryTransport.ts +0 -55
  42. package/src/transports/SesTransport.ts +0 -129
  43. package/src/transports/SmtpTransport.ts +0 -184
  44. package/src/transports/Transport.ts +0 -45
  45. package/src/types.ts +0 -309
  46. package/src/utils/html.ts +0 -43
  47. package/src/webhooks/SendGridWebhookDriver.ts +0 -80
  48. package/src/webhooks/SesWebhookDriver.ts +0 -44
  49. package/tests/DevMailbox.test.ts +0 -54
  50. package/tests/FileMailboxStorage.test.ts +0 -56
  51. package/tests/MjmlLayout.test.ts +0 -28
  52. package/tests/MjmlRenderer.test.ts +0 -53
  53. package/tests/OrbitSignalWebhook.test.ts +0 -56
  54. package/tests/ReactMjmlRenderer.test.ts +0 -33
  55. package/tests/SendGridWebhookDriver.test.ts +0 -69
  56. package/tests/SesWebhookDriver.test.ts +0 -46
  57. package/tests/VueMjmlRenderer.test.ts +0 -35
  58. package/tests/dev-server.test.ts +0 -66
  59. package/tests/log-transport.test.ts +0 -21
  60. package/tests/mailable-extra.test.ts +0 -68
  61. package/tests/mailable.test.ts +0 -77
  62. package/tests/orbit-signal.test.ts +0 -43
  63. package/tests/renderers.test.ts +0 -58
  64. package/tests/template-renderer.test.ts +0 -24
  65. package/tests/transports.test.ts +0 -52
  66. package/tests/ui.test.ts +0 -37
  67. package/tsconfig.build.json +0 -24
  68. package/tsconfig.json +0 -9
package/src/Mailable.ts DELETED
@@ -1,674 +0,0 @@
1
- import type { Queueable } from '@gravito/stream' // Import Queueable from orbit-queue
2
- import { HtmlRenderer } from './renderers/HtmlRenderer'
3
- import type { Renderer } from './renderers/Renderer'
4
- import { TemplateRenderer } from './renderers/TemplateRenderer'
5
- import type { Address, Attachment, Envelope, MailConfig } from './types'
6
-
7
- // Type placeholders for React/Vue components to avoid hard dependencies in core
8
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
- type ComponentType = any
10
-
11
- /**
12
- * Base class for all mailable messages.
13
- *
14
- * @description
15
- * Mailable provides a fluent API to build email envelopes and render content
16
- * using multiple engines: HTML, Prism templates, React, and Vue components.
17
- *
18
- * @architecture
19
- * ```
20
- * Mailable
21
- * ├── Envelope (from, to, subject, cc, bcc, attachments)
22
- * ├── Renderer (HtmlRenderer | TemplateRenderer | ReactRenderer | VueRenderer)
23
- * └── Queueable (queue support interface)
24
- * ```
25
- *
26
- * @lifecycle
27
- * 1. Create Mailable subclass
28
- * 2. Implement build() method to configure envelope and content
29
- * 3. Call send() to send immediately, or queue() for background processing
30
- * 4. OrbitSignal calls buildEnvelope() → renderContent() → transport.send()
31
- *
32
- * @example
33
- * ```typescript
34
- * import { Mailable } from '@gravito/signal'
35
- *
36
- * class WelcomeEmail extends Mailable {
37
- * constructor(private user: User) {
38
- * super()
39
- * }
40
- *
41
- * build() {
42
- * return this
43
- * .to(this.user.email)
44
- * .subject('Welcome!')
45
- * .view('emails/welcome', { name: this.user.name })
46
- * }
47
- * }
48
- *
49
- * // Send immediately
50
- * await mail.send(new WelcomeEmail(user))
51
- *
52
- * // Queue for background processing
53
- * await new WelcomeEmail(user).onQueue('emails').queue()
54
- * ```
55
- *
56
- * @see {@link OrbitSignal} Mail service orchestrator
57
- * @see {@link Renderer} Content rendering interface
58
- * @see {@link Envelope} Email metadata structure
59
- * @see {@link Queueable} Queue integration interface
60
- *
61
- * @public
62
- * @since 3.0.0
63
- */
64
- export abstract class Mailable implements Queueable {
65
- protected envelope: Partial<Envelope> = {}
66
- protected renderer?: Renderer
67
- private rendererResolver?: () => Promise<Renderer>
68
- protected renderData: Record<string, unknown> = {}
69
- protected config?: MailConfig
70
-
71
- // ===== Fluent API (Envelope Construction) =====
72
-
73
- /**
74
- * Set the sender address for the email.
75
- *
76
- * This defines the "From" field in the email envelope. If not called, the default
77
- * sender from the mail configuration will be used.
78
- *
79
- * @param address - The email address or address object containing name and address.
80
- * @returns The current mailable instance for chaining.
81
- *
82
- * @example
83
- * ```typescript
84
- * mailable.from('admin@example.com')
85
- * mailable.from({ name: 'Support', address: 'support@example.com' })
86
- * ```
87
- */
88
- from(address: string | Address): this {
89
- this.envelope.from = typeof address === 'string' ? { address } : address
90
- return this
91
- }
92
-
93
- /**
94
- * Set the primary recipient(s) for the email.
95
- *
96
- * Configures the "To" field. Supports single or multiple recipients in various formats.
97
- *
98
- * @param address - A single email string, an address object, or an array of either.
99
- * @returns The current mailable instance for chaining.
100
- *
101
- * @example
102
- * ```typescript
103
- * mailable.to('user@example.com')
104
- * mailable.to(['a@example.com', 'b@example.com'])
105
- * mailable.to({ name: 'John', address: 'john@example.com' })
106
- * ```
107
- */
108
- to(address: string | Address | (string | Address)[]): this {
109
- this.envelope.to = this.normalizeAddressArray(address)
110
- return this
111
- }
112
-
113
- /**
114
- * Set the carbon copy (CC) recipient(s).
115
- *
116
- * Adds recipients to the "Cc" field of the email.
117
- *
118
- * @param address - A single email string, an address object, or an array of either.
119
- * @returns The current mailable instance for chaining.
120
- *
121
- * @example
122
- * ```typescript
123
- * mailable.cc('manager@example.com')
124
- * ```
125
- */
126
- cc(address: string | Address | (string | Address)[]): this {
127
- this.envelope.cc = this.normalizeAddressArray(address)
128
- return this
129
- }
130
-
131
- /**
132
- * Set the blind carbon copy (BCC) recipient(s).
133
- *
134
- * Adds recipients to the "Bcc" field. These recipients are hidden from others.
135
- *
136
- * @param address - A single email string, an address object, or an array of either.
137
- * @returns The current mailable instance for chaining.
138
- *
139
- * @example
140
- * ```typescript
141
- * mailable.bcc('audit@example.com')
142
- * ```
143
- */
144
- bcc(address: string | Address | (string | Address)[]): this {
145
- this.envelope.bcc = this.normalizeAddressArray(address)
146
- return this
147
- }
148
-
149
- /**
150
- * Set the reply-to address.
151
- *
152
- * Specifies where replies to this email should be directed.
153
- *
154
- * @param address - The email address or address object for replies.
155
- * @returns The current mailable instance for chaining.
156
- *
157
- * @example
158
- * ```typescript
159
- * mailable.replyTo('no-reply@example.com')
160
- * ```
161
- */
162
- replyTo(address: string | Address): this {
163
- this.envelope.replyTo = typeof address === 'string' ? { address } : address
164
- return this
165
- }
166
-
167
- /**
168
- * Set the subject line for the email.
169
- *
170
- * Defines the text that appears in the recipient's inbox subject field.
171
- *
172
- * @param subject - The subject text.
173
- * @returns The current mailable instance for chaining.
174
- *
175
- * @example
176
- * ```typescript
177
- * mailable.subject('Your Order Confirmation')
178
- * ```
179
- */
180
- subject(subject: string): this {
181
- this.envelope.subject = subject
182
- return this
183
- }
184
-
185
- /**
186
- * Set the email priority.
187
- *
188
- * Hints to the email client how urgent this message is.
189
- *
190
- * @param level - The priority level: 'high', 'normal', or 'low'.
191
- * @returns The current mailable instance for chaining.
192
- *
193
- * @example
194
- * ```typescript
195
- * mailable.emailPriority('high')
196
- * ```
197
- */
198
- emailPriority(level: 'high' | 'normal' | 'low'): this {
199
- this.envelope.priority = level
200
- return this
201
- }
202
-
203
- /**
204
- * Attach a file to the email.
205
- *
206
- * Adds a file attachment to the message. Can be called multiple times for multiple files.
207
- *
208
- * @param attachment - The attachment configuration including path, content, or filename.
209
- * @returns The current mailable instance for chaining.
210
- *
211
- * @example
212
- * ```typescript
213
- * mailable.attach({
214
- * filename: 'invoice.pdf',
215
- * path: './storage/invoices/123.pdf'
216
- * })
217
- * ```
218
- */
219
- attach(attachment: Attachment): this {
220
- this.envelope.attachments = this.envelope.attachments || []
221
- this.envelope.attachments.push(attachment)
222
- return this
223
- }
224
-
225
- // ===== Content Methods (Renderer Selection) =====
226
-
227
- /**
228
- * Set the content using a raw HTML string.
229
- *
230
- * Use this for simple emails where a full template engine is not required.
231
- *
232
- * @param content - The raw HTML content.
233
- * @returns The current mailable instance for chaining.
234
- *
235
- * @example
236
- * ```typescript
237
- * mailable.html('<h1>Hello</h1><p>Welcome to our platform.</p>')
238
- * ```
239
- */
240
- html(content: string): this {
241
- this.renderer = new HtmlRenderer(content)
242
- return this
243
- }
244
-
245
- /**
246
- * Set the content using an OrbitPrism template.
247
- *
248
- * Renders a template file with the provided data. This is the recommended way
249
- * to build complex, data-driven emails.
250
- *
251
- * @param template - The template name or path relative to the configured views directory.
252
- * @param data - The data object to be injected into the template.
253
- * @returns The current mailable instance for chaining.
254
- *
255
- * @example
256
- * ```typescript
257
- * mailable.view('emails.welcome', { name: 'Alice' })
258
- * ```
259
- */
260
- view(template: string, data?: Record<string, unknown>): this {
261
- this.renderer = new TemplateRenderer(template, undefined) // Dir will be injected later if possible, or use default
262
- this.renderData = data || {}
263
- return this
264
- }
265
-
266
- /**
267
- * Set the content using a React component.
268
- *
269
- * Leverages React's component model for email design. The renderer is loaded
270
- * dynamically to keep the core package lightweight.
271
- *
272
- * @param component - The React component class or function.
273
- * @param props - The properties to pass to the component.
274
- * @param deps - Optional React/ReactDOMServer overrides for custom environments.
275
- * @returns The current mailable instance for chaining.
276
- *
277
- * @example
278
- * ```typescript
279
- * mailable.react(WelcomeEmailComponent, { name: 'Alice' })
280
- * ```
281
- */
282
- react<P extends object>(
283
- component: ComponentType,
284
- props?: P,
285
- deps?: {
286
- createElement?: (...args: any[]) => any
287
- renderToStaticMarkup?: (element: any) => string
288
- }
289
- ): this {
290
- this.rendererResolver = async () => {
291
- const { ReactRenderer } = await import('./renderers/ReactRenderer')
292
- return new ReactRenderer(component, props, deps)
293
- }
294
- return this
295
- }
296
-
297
- /**
298
- * Set the content using a Vue component.
299
- *
300
- * Leverages Vue's component model for email design. The renderer is loaded
301
- * dynamically to keep the core package lightweight.
302
- *
303
- * @param component - The Vue component object.
304
- * @param props - The properties to pass to the component.
305
- * @param deps - Optional Vue/VueServerRenderer overrides for custom environments.
306
- * @returns The current mailable instance for chaining.
307
- *
308
- * @example
309
- * ```typescript
310
- * mailable.vue(WelcomeEmailComponent, { name: 'Alice' })
311
- * ```
312
- */
313
- vue<P extends object>(
314
- component: ComponentType,
315
- props?: P,
316
- deps?: {
317
- createSSRApp?: (...args: any[]) => any
318
- h?: (...args: any[]) => any
319
- renderToString?: (app: any) => Promise<string>
320
- }
321
- ): this {
322
- this.rendererResolver = async () => {
323
- const { VueRenderer } = await import('./renderers/VueRenderer')
324
- return new VueRenderer(component, props as any, deps)
325
- }
326
- return this
327
- }
328
-
329
- /**
330
- * Set the content using an MJML markup string.
331
- *
332
- * MJML ensures responsive email compatibility across various clients.
333
- *
334
- * @param content - The MJML markup string or inner content.
335
- * @param options - MJML transformation options.
336
- * @param options.layout - Optional full MJML layout string. Use '{{content}}' as placeholder.
337
- * @returns The current mailable instance for chaining.
338
- *
339
- * @example
340
- * ```typescript
341
- * mailable.mjml('<mj-text>Hello</mj-text>', {
342
- * layout: '<mjml><mj-body>{{content}}</mj-body></mjml>'
343
- * })
344
- * ```
345
- */
346
- mjml(content: string, options?: Record<string, any> & { layout?: string }): this {
347
- const finalContent = options?.layout?.includes('{{content}}')
348
- ? options.layout.replace('{{content}}', content)
349
- : content
350
-
351
- this.rendererResolver = async () => {
352
- const { MjmlRenderer } = await import('./renderers/MjmlRenderer')
353
- return new MjmlRenderer(finalContent, options)
354
- }
355
- return this
356
- }
357
-
358
- /**
359
- * Set the content using a React component that outputs MJML.
360
- *
361
- * @param component - The React component.
362
- * @param props - Component properties.
363
- * @param options - MJML options.
364
- * @returns The current mailable instance for chaining.
365
- */
366
- mjmlReact<P extends object>(
367
- component: ComponentType,
368
- props?: P,
369
- options?: Record<string, any>
370
- ): this {
371
- this.rendererResolver = async () => {
372
- const { ReactMjmlRenderer } = await import('./renderers/ReactMjmlRenderer')
373
- return new ReactMjmlRenderer(component, props, options)
374
- }
375
- return this
376
- }
377
-
378
- /**
379
- * Set the content using a Vue component that outputs MJML.
380
- *
381
- * @param component - The Vue component.
382
- * @param props - Component properties.
383
- * @param options - MJML options.
384
- * @returns The current mailable instance for chaining.
385
- */
386
- mjmlVue<P extends object>(
387
- component: ComponentType,
388
- props?: P,
389
- options?: Record<string, any>
390
- ): this {
391
- this.rendererResolver = async () => {
392
- const { VueMjmlRenderer } = await import('./renderers/VueMjmlRenderer')
393
- return new VueMjmlRenderer(component, props, options)
394
- }
395
- return this
396
- }
397
-
398
- // ===== Life Cycle =====
399
-
400
- /**
401
- * Configure the mailable's envelope and content.
402
- *
403
- * This abstract method must be implemented by subclasses to define the email's
404
- * recipients, subject, and body using the fluent API.
405
- *
406
- * @returns The current mailable instance.
407
- *
408
- * @example
409
- * ```typescript
410
- * build() {
411
- * return this.to('user@example.com').subject('Hi').html('...')
412
- * }
413
- * ```
414
- */
415
- abstract build(): this
416
-
417
- // ===== Queueable Implementation =====
418
-
419
- /** The name of the queue to push this mailable to. */
420
- queueName?: string
421
- /** The connection name for the queue. */
422
- connectionName?: string
423
- /** Delay in seconds before the message is sent. */
424
- delaySeconds?: number
425
- /** Priority of the message in the queue. */
426
- priority?: number | string
427
-
428
- /**
429
- * Set the target queue for background processing.
430
- *
431
- * @param queue - The name of the queue.
432
- * @returns The current mailable instance for chaining.
433
- *
434
- * @example
435
- * ```typescript
436
- * mailable.onQueue('notifications')
437
- * ```
438
- */
439
- onQueue(queue: string): this {
440
- this.queueName = queue
441
- return this
442
- }
443
-
444
- /**
445
- * Set the queue connection to be used.
446
- *
447
- * @param connection - The name of the connection (e.g., 'redis', 'sqs').
448
- * @returns The current mailable instance for chaining.
449
- *
450
- * @example
451
- * ```typescript
452
- * mailable.onConnection('redis')
453
- * ```
454
- */
455
- onConnection(connection: string): this {
456
- this.connectionName = connection
457
- return this
458
- }
459
-
460
- /**
461
- * Set a delay for the queued message.
462
- *
463
- * The message will remain in the queue and only be processed after the delay.
464
- *
465
- * @param seconds - The delay in seconds.
466
- * @returns The current mailable instance for chaining.
467
- *
468
- * @example
469
- * ```typescript
470
- * mailable.delay(3600) // Delay for 1 hour
471
- * ```
472
- */
473
- delay(seconds: number): this {
474
- this.delaySeconds = seconds
475
- return this
476
- }
477
-
478
- /**
479
- * Set the priority for the queued message.
480
- *
481
- * Higher priority messages are typically processed before lower priority ones.
482
- *
483
- * @param priority - The priority value (numeric or string).
484
- * @returns The current mailable instance for chaining.
485
- *
486
- * @example
487
- * ```typescript
488
- * mailable.withPriority(10)
489
- * ```
490
- */
491
- withPriority(priority: string | number): this {
492
- this.priority = priority
493
- return this
494
- }
495
-
496
- /**
497
- * Push the mailable onto the configured queue.
498
- *
499
- * Automatically resolves the mail service from the Gravito container and
500
- * dispatches this mailable for background processing.
501
- *
502
- * @returns A promise that resolves when the mailable is queued.
503
- *
504
- * @example
505
- * ```typescript
506
- * await new WelcomeEmail(user).queue()
507
- * ```
508
- */
509
- async queue(): Promise<void> {
510
- // We should ideally use the container to get the mail service
511
- // But since Mailable might be used outside a core context, we'll try a safe approach.
512
- try {
513
- // biome-ignore lint/suspicious/noTsIgnore: Global access to app() helper from core
514
- // @ts-ignore
515
- const { app } = await import('@gravito/core')
516
- const mail = app().container.make<any>('mail')
517
- if (mail) {
518
- return mail.queue(this)
519
- }
520
- } catch (_e) {
521
- // Fallback if core is not available
522
- console.warn('[Mailable] Could not auto-resolve mail service for queuing.')
523
- }
524
- }
525
-
526
- // ===== I18n Support =====
527
-
528
- protected currentLocale?: string
529
- protected translator?: (key: string, replace?: Record<string, unknown>, locale?: string) => string
530
-
531
- /**
532
- * Set the locale for the email content.
533
- *
534
- * Used by the translator to resolve localized strings in templates or components.
535
- *
536
- * @param locale - The locale identifier (e.g., 'en-US', 'fr').
537
- * @returns The current mailable instance for chaining.
538
- *
539
- * @example
540
- * ```typescript
541
- * mailable.locale('es')
542
- * ```
543
- */
544
- locale(locale: string): this {
545
- this.currentLocale = locale
546
- return this
547
- }
548
-
549
- /**
550
- * Internal: Set the translator function (called by OrbitSignal)
551
- * @internal
552
- */
553
- setTranslator(
554
- translator: (key: string, replace?: Record<string, unknown>, locale?: string) => string
555
- ): void {
556
- this.translator = translator
557
- }
558
-
559
- /**
560
- * Translate a key into a localized string.
561
- *
562
- * Uses the configured translator and current locale to resolve the key.
563
- *
564
- * @param key - The translation key.
565
- * @param replace - Key-value pairs for string interpolation.
566
- * @returns The translated string, or the key itself if no translator is available.
567
- *
568
- * @example
569
- * ```typescript
570
- * const text = mailable.t('messages.welcome', { name: 'Alice' })
571
- * ```
572
- */
573
- t(key: string, replace?: Record<string, unknown>): string {
574
- if (this.translator) {
575
- return this.translator(key, replace, this.currentLocale)
576
- }
577
- return key // Fallback: just return the key if no translator
578
- }
579
-
580
- // ===== Internal Systems =====
581
-
582
- /**
583
- * Compile the final email envelope.
584
- *
585
- * Merges mailable-specific settings with global configuration defaults.
586
- * This is called internally by the mail service before sending.
587
- *
588
- * @param configPromise - The mail configuration or a promise resolving to it.
589
- * @returns The fully constructed envelope.
590
- *
591
- * @example
592
- * ```typescript
593
- * const envelope = await mailable.buildEnvelope(config)
594
- * ```
595
- */
596
- async buildEnvelope(configPromise: MailConfig | Promise<MailConfig>): Promise<Envelope> {
597
- const config = await Promise.resolve(configPromise)
598
- this.config = config
599
-
600
- // Inject translator from config if available
601
- if (config.translator) {
602
- this.setTranslator(config.translator)
603
- }
604
-
605
- this.build() // User logic executes here
606
-
607
- // Ensure Renderer is initialized if using TemplateRenderer with config path
608
- if (this.renderer instanceof TemplateRenderer && config.viewsDir) {
609
- // Re-initialize or update TemplateRenderer with the correct directory
610
- this.renderer = new TemplateRenderer((this.renderer as any).template, config.viewsDir)
611
- }
612
-
613
- const envelope: Envelope = {
614
- from: this.envelope.from || config.from,
615
- to: this.envelope.to || [],
616
- subject: this.envelope.subject || '(No Subject)',
617
- priority: this.envelope.priority || 'normal',
618
- }
619
-
620
- if (this.envelope.cc) {
621
- envelope.cc = this.envelope.cc
622
- }
623
- if (this.envelope.bcc) {
624
- envelope.bcc = this.envelope.bcc
625
- }
626
- if (this.envelope.replyTo) {
627
- envelope.replyTo = this.envelope.replyTo
628
- }
629
- if (this.envelope.attachments) {
630
- envelope.attachments = this.envelope.attachments
631
- }
632
-
633
- return envelope
634
- }
635
-
636
- /**
637
- * Render the email content to HTML and plain text.
638
- *
639
- * Executes the chosen renderer (HTML, Template, React, or Vue) with the
640
- * provided data and i18n helpers.
641
- *
642
- * @returns The rendered HTML and optional plain text content.
643
- * @throws {Error} If no renderer has been specified.
644
- *
645
- * @example
646
- * ```typescript
647
- * const { html } = await mailable.renderContent()
648
- * ```
649
- */
650
- async renderContent(): Promise<{ html: string; text?: string }> {
651
- // Resolve lazy renderer if needed
652
- if (!this.renderer && this.rendererResolver) {
653
- this.renderer = await this.rendererResolver()
654
- }
655
-
656
- if (!this.renderer) {
657
- throw new Error('No content renderer specified. Use html(), view(), react(), or vue().')
658
- }
659
-
660
- // Inject i18n helpers into renderData
661
- this.renderData = {
662
- ...this.renderData,
663
- locale: this.currentLocale,
664
- t: (key: string, replace?: Record<string, unknown>) => this.t(key, replace),
665
- }
666
-
667
- return this.renderer.render(this.renderData)
668
- }
669
-
670
- private normalizeAddressArray(input: string | Address | (string | Address)[]): Address[] {
671
- const arr = Array.isArray(input) ? input : [input]
672
- return arr.map((item) => (typeof item === 'string' ? { address: item } : item))
673
- }
674
- }