@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.
Files changed (104) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +89 -60
  3. package/README.zh-TW.md +140 -9
  4. package/dist/MjmlRenderer-IUH663FT.mjs +8 -0
  5. package/dist/ReactMjmlRenderer-C3P5YO5L.mjs +8 -0
  6. package/dist/ReactRenderer-2JFLRVST.mjs +45 -0
  7. package/dist/{ReactRenderer-L5INVYKT.mjs → ReactRenderer-LYEOSYFS.mjs} +9 -8
  8. package/dist/ReactRenderer-V54CUUEI.mjs +45 -0
  9. package/dist/VueMjmlRenderer-4F4CXHDB.mjs +8 -0
  10. package/dist/VueMjmlRenderer-5WZR4CQG.mjs +8 -0
  11. package/dist/VueMjmlRenderer-U5YMWI44.mjs +8 -0
  12. package/dist/VueRenderer-3YBRQXME.mjs +48 -0
  13. package/dist/VueRenderer-46JGXTJ2.mjs +48 -0
  14. package/dist/VueRenderer-5KWD4R3C.mjs +48 -0
  15. package/dist/VueRenderer-C23U4O5E.mjs +48 -0
  16. package/dist/VueRenderer-LEVDFLHP.mjs +31 -0
  17. package/dist/VueRenderer-RNHSCCRI.mjs +48 -0
  18. package/dist/chunk-3WOR3XSL.mjs +82 -0
  19. package/dist/chunk-DBFIVHHG.mjs +79 -0
  20. package/dist/{chunk-6DZX6EAA.mjs → chunk-HEBXNMVQ.mjs} +12 -1
  21. package/dist/chunk-KB7IDDBT.mjs +82 -0
  22. package/dist/chunk-LZL5UUPC.mjs +82 -0
  23. package/dist/chunk-W6LXIJKK.mjs +57 -0
  24. package/dist/chunk-XBIVBJS2.mjs +8 -0
  25. package/dist/index.d.mts +1680 -209
  26. package/dist/index.d.ts +1680 -209
  27. package/dist/index.js +69405 -542
  28. package/dist/index.mjs +993 -110
  29. package/dist/lib-HJTRWKU5.mjs +67788 -0
  30. package/dist/{VueRenderer-Z5PRVBNH.mjs → server-renderer-4IM3P5XZ.mjs} +308 -423
  31. package/dist/server-renderer-7KWFSTPV.mjs +37193 -0
  32. package/dist/{VueRenderer-S65ZARRI.mjs → server-renderer-S5FPSTJ2.mjs} +931 -877
  33. package/dist/server-renderer-X5LUFVWT.mjs +37193 -0
  34. package/doc/OPTIMIZATION_PLAN.md +496 -0
  35. package/package.json +14 -12
  36. package/scripts/check-coverage.ts +64 -0
  37. package/src/Mailable.ts +340 -44
  38. package/src/OrbitSignal.ts +350 -50
  39. package/src/TypedMailable.ts +96 -0
  40. package/src/dev/DevMailbox.ts +89 -33
  41. package/src/dev/DevServer.ts +14 -14
  42. package/src/dev/storage/FileMailboxStorage.ts +66 -0
  43. package/src/dev/storage/MailboxStorage.ts +15 -0
  44. package/src/dev/storage/MemoryMailboxStorage.ts +36 -0
  45. package/src/dev/ui/mailbox.ts +1 -1
  46. package/src/dev/ui/preview.ts +4 -4
  47. package/src/errors.ts +69 -0
  48. package/src/events.ts +72 -0
  49. package/src/index.ts +20 -1
  50. package/src/renderers/HtmlRenderer.ts +20 -18
  51. package/src/renderers/MjmlRenderer.ts +73 -0
  52. package/src/renderers/ReactMjmlRenderer.ts +94 -0
  53. package/src/renderers/ReactRenderer.ts +26 -21
  54. package/src/renderers/Renderer.ts +43 -3
  55. package/src/renderers/TemplateRenderer.ts +48 -15
  56. package/src/renderers/VueMjmlRenderer.ts +99 -0
  57. package/src/renderers/VueRenderer.ts +26 -21
  58. package/src/renderers/mjml-templates.ts +50 -0
  59. package/src/transports/BaseTransport.ts +148 -0
  60. package/src/transports/LogTransport.ts +28 -6
  61. package/src/transports/MemoryTransport.ts +34 -6
  62. package/src/transports/SesTransport.ts +62 -17
  63. package/src/transports/SmtpTransport.ts +123 -27
  64. package/src/transports/Transport.ts +33 -4
  65. package/src/types.ts +172 -3
  66. package/src/utils/html.ts +43 -0
  67. package/src/webhooks/SendGridWebhookDriver.ts +80 -0
  68. package/src/webhooks/SesWebhookDriver.ts +44 -0
  69. package/tests/DevMailbox.test.ts +54 -0
  70. package/tests/FileMailboxStorage.test.ts +56 -0
  71. package/tests/MjmlLayout.test.ts +28 -0
  72. package/tests/MjmlRenderer.test.ts +53 -0
  73. package/tests/OrbitSignalWebhook.test.ts +56 -0
  74. package/tests/ReactMjmlRenderer.test.ts +33 -0
  75. package/tests/SendGridWebhookDriver.test.ts +69 -0
  76. package/tests/SesWebhookDriver.test.ts +46 -0
  77. package/tests/VueMjmlRenderer.test.ts +35 -0
  78. package/tests/dev-server.test.ts +1 -1
  79. package/tests/transports.test.ts +3 -3
  80. package/tsconfig.json +12 -24
  81. package/dist/OrbitMail-2Z7ZTKYA.mjs +0 -7
  82. package/dist/OrbitMail-BGV32HWN.mjs +0 -7
  83. package/dist/OrbitMail-FUYZQSAV.mjs +0 -7
  84. package/dist/OrbitMail-NAPCRK7B.mjs +0 -7
  85. package/dist/OrbitMail-REGJ276B.mjs +0 -7
  86. package/dist/OrbitMail-TCFBJWDT.mjs +0 -7
  87. package/dist/OrbitMail-XZZW6U4N.mjs +0 -7
  88. package/dist/OrbitSignal-IPSA2CDO.mjs +0 -7
  89. package/dist/OrbitSignal-MABW4DDW.mjs +0 -7
  90. package/dist/OrbitSignal-QSW5VQ5M.mjs +0 -7
  91. package/dist/OrbitSignal-R22QHWAA.mjs +0 -7
  92. package/dist/OrbitSignal-ZKKMEC27.mjs +0 -7
  93. package/dist/chunk-3U2CYJO5.mjs +0 -367
  94. package/dist/chunk-3XFC4T6M.mjs +0 -392
  95. package/dist/chunk-456QRYFW.mjs +0 -401
  96. package/dist/chunk-DT3R2TNV.mjs +0 -367
  97. package/dist/chunk-F6MVTUCT.mjs +0 -421
  98. package/dist/chunk-GADWIVC4.mjs +0 -400
  99. package/dist/chunk-HHKFAMSE.mjs +0 -380
  100. package/dist/chunk-NEQCQSZI.mjs +0 -406
  101. package/dist/chunk-OKRNL6PN.mjs +0 -400
  102. package/dist/chunk-ULN3GMY2.mjs +0 -367
  103. package/dist/chunk-XAWO7RSP.mjs +0 -398
  104. package/dist/chunk-YLVDJSED.mjs +0 -431
@@ -0,0 +1,73 @@
1
+ import { stripHtml } from '../utils/html'
2
+ import type { Renderer, RenderResult } from './Renderer'
3
+
4
+ /**
5
+ * Renderer for MJML-based emails.
6
+ *
7
+ * MJML is a markup language designed to reduce the pain of coding a responsive email.
8
+ * This renderer lazily loads the `mjml` package to keep the core lightweight.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const renderer = new MjmlRenderer('<mjml><mj-body>...</mj-body></mjml>');
13
+ * const result = await renderer.render();
14
+ * ```
15
+ *
16
+ * @public
17
+ * @since 1.1.0
18
+ */
19
+ export class MjmlRenderer implements Renderer {
20
+ /**
21
+ * Creates an instance of MjmlRenderer.
22
+ *
23
+ * @param content - The MJML markup string to be rendered.
24
+ * @param options - Optional MJML transformation options.
25
+ * @param deps - Optional dependency injection for testing.
26
+ */
27
+ constructor(
28
+ private content: string,
29
+ private options: Record<string, any> = {},
30
+ private deps: {
31
+ mjml2html?: (mjml: string, options?: any) => any
32
+ } = {}
33
+ ) {}
34
+
35
+ /**
36
+ * Renders the MJML content to static HTML.
37
+ *
38
+ * This method performs a dynamic import of `mjml` to ensure it's only
39
+ * loaded if this renderer is actually used.
40
+ *
41
+ * @returns A promise resolving to the rendered content.
42
+ * @throws {Error} If MJML dependencies cannot be loaded or rendering fails.
43
+ */
44
+ async render(): Promise<RenderResult> {
45
+ let mjml2html = this.deps.mjml2html
46
+
47
+ if (!mjml2html) {
48
+ try {
49
+ mjml2html = (await import('mjml')).default
50
+ } catch (_e) {
51
+ throw new Error(
52
+ '[OrbitSignal] The "mjml" package is required for MjmlRenderer. Please install it using "bun add mjml".'
53
+ )
54
+ }
55
+ }
56
+
57
+ const { html, errors } = mjml2html(this.content, {
58
+ validationLevel: 'soft',
59
+ ...this.options,
60
+ })
61
+
62
+ if (errors && errors.length > 0 && this.options.validationLevel === 'strict') {
63
+ throw new Error(
64
+ `MJML rendering failed: ${errors.map((e: any) => e.formattedMessage).join(', ')}`
65
+ )
66
+ }
67
+
68
+ return {
69
+ html,
70
+ text: stripHtml(html),
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,94 @@
1
+ import { stripHtml } from '../utils/html'
2
+ import type { Renderer, RenderResult } from './Renderer'
3
+
4
+ /**
5
+ * Renderer for React component-based MJML emails.
6
+ *
7
+ * Renders React components to MJML string using SSR, then converts
8
+ * the MJML to responsive HTML.
9
+ *
10
+ * @typeParam P - Props type for the React component.
11
+ * @public
12
+ * @since 1.1.0
13
+ */
14
+ export class ReactMjmlRenderer<P extends object = object> implements Renderer {
15
+ /**
16
+ * Creates an instance of ReactMjmlRenderer.
17
+ *
18
+ * @param component - The React component to render.
19
+ * @param props - Initial props for the component.
20
+ * @param options - Optional MJML transformation options.
21
+ * @param deps - Optional dependency injection for testing.
22
+ */
23
+ constructor(
24
+ private component: any,
25
+ private props?: P,
26
+ private options: Record<string, any> = {},
27
+ private deps: {
28
+ createElement?: (...args: any[]) => any
29
+ renderToStaticMarkup?: (element: any) => string
30
+ mjml2html?: (mjml: string, options?: any) => any
31
+ } = {}
32
+ ) {}
33
+
34
+ /**
35
+ * Renders the React component to a static HTML string via MJML.
36
+ *
37
+ * @param data - Runtime data to be merged with initial props.
38
+ * @returns A promise resolving to the rendered content.
39
+ * @throws {Error} If MJML rendering fails.
40
+ */
41
+ async render(data: Record<string, unknown>): Promise<RenderResult> {
42
+ let { createElement, renderToStaticMarkup, mjml2html } = this.deps
43
+
44
+ if (!createElement || !renderToStaticMarkup) {
45
+ try {
46
+ const react = await import('react')
47
+ const reactDomServer = await import('react-dom/server')
48
+ createElement ??= react.createElement
49
+ renderToStaticMarkup ??= reactDomServer.renderToStaticMarkup
50
+ } catch (_e) {
51
+ throw new Error(
52
+ '[OrbitSignal] The "react" and "react-dom" packages are required for ReactMjmlRenderer. Please install them using "bun add react react-dom".'
53
+ )
54
+ }
55
+ }
56
+
57
+ if (!mjml2html) {
58
+ try {
59
+ mjml2html = (await import('mjml')).default
60
+ } catch (_e) {
61
+ throw new Error(
62
+ '[OrbitSignal] The "mjml" package is required for ReactMjmlRenderer. Please install it using "bun add mjml".'
63
+ )
64
+ }
65
+ }
66
+
67
+ const mergedProps = { ...this.props, ...data } as P
68
+ const element = createElement?.(this.component, mergedProps)
69
+ const mjml = renderToStaticMarkup?.(element)
70
+ if (!mjml) {
71
+ throw new Error('Failed to render MJML template')
72
+ }
73
+
74
+ const result = mjml2html?.(mjml, {
75
+ validationLevel: 'soft',
76
+ ...this.options,
77
+ })
78
+ if (!result) {
79
+ throw new Error('Failed to convert MJML to HTML')
80
+ }
81
+ const { html, errors } = result
82
+
83
+ if (errors && errors.length > 0 && this.options.validationLevel === 'strict') {
84
+ throw new Error(
85
+ `MJML rendering failed: ${errors.map((e: any) => e.formattedMessage).join(', ')}`
86
+ )
87
+ }
88
+
89
+ return {
90
+ html,
91
+ text: stripHtml(html),
92
+ }
93
+ }
94
+ }
@@ -1,26 +1,31 @@
1
+ import { stripHtml } from '../utils/html'
1
2
  import type { Renderer, RenderResult } from './Renderer'
2
3
 
3
4
  /**
4
5
  * Renderer for React component-based emails.
5
6
  *
6
- * Renders React components to static HTML for email delivery.
7
- * Supports server-side rendering with optional dependency injection
8
- * for testing.
7
+ * Renders React components to static HTML using server-side rendering (SSR).
8
+ * It lazily loads React and ReactDOM dependencies only when needed, preventing
9
+ * unnecessary bundle bloat for users who do not use React renderers.
9
10
  *
10
11
  * @example
11
12
  * ```typescript
12
- * import { WelcomeEmail } from './emails/WelcomeEmail'
13
- *
14
- * const renderer = new ReactRenderer(WelcomeEmail, { name: 'John' })
15
- * const { html, text } = await renderer.render({ date: new Date() })
13
+ * const renderer = new ReactRenderer(MyComponent, { title: 'Welcome' });
14
+ * const result = await renderer.render({ name: 'John' });
16
15
  * ```
17
16
  *
18
- * @typeParam P - Props type for the React component
19
- *
20
- * @since 3.0.0
17
+ * @typeParam P - Props type for the React component.
21
18
  * @public
19
+ * @since 3.0.0
22
20
  */
23
21
  export class ReactRenderer<P extends object = object> implements Renderer {
22
+ /**
23
+ * Creates an instance of ReactRenderer.
24
+ *
25
+ * @param component - The React component to render.
26
+ * @param props - Initial props for the component.
27
+ * @param deps - Optional dependency injection for testing.
28
+ */
24
29
  constructor(
25
30
  private component: any, // Use any to avoid hard React dependency in types
26
31
  private props?: P,
@@ -30,6 +35,16 @@ export class ReactRenderer<P extends object = object> implements Renderer {
30
35
  } = {}
31
36
  ) {}
32
37
 
38
+ /**
39
+ * Renders the React component to a static HTML string.
40
+ *
41
+ * This method performs dynamic imports of `react` and `react-dom/server`
42
+ * to ensure they are only loaded if this renderer is actually used.
43
+ *
44
+ * @param data - Runtime data to be merged with initial props.
45
+ * @returns A promise resolving to the rendered content.
46
+ * @throws {Error} If React dependencies cannot be loaded or rendering fails.
47
+ */
33
48
  async render(data: Record<string, unknown>): Promise<RenderResult> {
34
49
  // Dynamic imports to avoid hard dependencies on react/react-dom
35
50
  const createElement = this.deps.createElement ?? (await import('react')).createElement
@@ -45,17 +60,7 @@ export class ReactRenderer<P extends object = object> implements Renderer {
45
60
 
46
61
  return {
47
62
  html: fullHtml,
48
- text: this.stripHtml(html),
63
+ text: stripHtml(html),
49
64
  }
50
65
  }
51
-
52
- private stripHtml(html: string): string {
53
- return html
54
- .replace(/<style(?:\s[^>]*)?>[\s\S]*?<\/style>/gi, '')
55
- .replace(/<script(?:\s[^>]*)?>[\s\S]*?<\/script>/gi, '')
56
- .replace(/<[^>]+>/g, '')
57
- .replace(/&nbsp;/g, ' ')
58
- .replace(/\s+/g, ' ')
59
- .trim()
60
- }
61
66
  }
@@ -1,18 +1,53 @@
1
1
  /**
2
2
  * Result of a content rendering operation.
3
3
  *
4
+ * This interface defines the structure of the output produced by any renderer.
5
+ * It ensures consistency across different rendering strategies (HTML, React, Vue, etc.),
6
+ * providing both the final HTML for the email body and an optional plain text version
7
+ * for clients that do not support HTML.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const result: RenderResult = {
12
+ * html: '<html><body><h1>Hello</h1></body></html>',
13
+ * text: 'Hello'
14
+ * };
15
+ * ```
16
+ *
4
17
  * @public
5
18
  * @since 3.0.0
6
19
  */
7
20
  export interface RenderResult {
8
- /** Rendered HTML string. */
21
+ /**
22
+ * The rendered HTML string.
23
+ *
24
+ * This is the primary content used for the email body.
25
+ */
9
26
  html: string
10
- /** Optional rendered plain text string. */
27
+ /**
28
+ * Optional rendered plain text string.
29
+ *
30
+ * Used as a fallback for email clients that cannot display HTML or for accessibility.
31
+ */
11
32
  text?: string
12
33
  }
13
34
 
14
35
  /**
15
- * Interface for email content renderers (HTML, Template, React, Vue).
36
+ * Interface for email content renderers.
37
+ *
38
+ * Renderers are responsible for transforming various input formats (raw HTML,
39
+ * templates, or UI components) into a standardized {@link RenderResult}.
40
+ * This abstraction allows the mail system to support multiple view engines
41
+ * and frameworks interchangeably.
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * class MyRenderer implements Renderer {
46
+ * async render(data: Record<string, unknown>): Promise<RenderResult> {
47
+ * return { html: `<div>${data.name}</div>`, text: String(data.name) };
48
+ * }
49
+ * }
50
+ * ```
16
51
  *
17
52
  * @public
18
53
  * @since 3.0.0
@@ -21,7 +56,12 @@ export interface Renderer {
21
56
  /**
22
57
  * Render the content into HTML and optionally plain text.
23
58
  *
59
+ * This method performs the actual transformation of the source content
60
+ * using the provided data context.
61
+ *
24
62
  * @param data - The data context for rendering.
63
+ * @returns A promise resolving to the rendered content.
64
+ * @throws {Error} If rendering fails due to syntax errors or missing dependencies.
25
65
  */
26
66
  render(data: Record<string, unknown>): Promise<RenderResult>
27
67
  }
@@ -1,51 +1,84 @@
1
+ import { stripHtml } from '../utils/html'
1
2
  import type { Renderer, RenderResult } from './Renderer'
2
3
 
3
4
  /**
4
5
  * Renderer for template-based emails using Gravito Prism.
5
6
  *
6
- * Renders email templates from the filesystem using the Prism
7
- * template engine with support for data binding and layouts.
7
+ * Renders email templates from the filesystem using the Prism template engine.
8
+ * It uses a static cache for the template engine to avoid redundant initialization
9
+ * costs when rendering multiple emails from the same directory.
8
10
  *
9
11
  * @example
10
12
  * ```typescript
11
- * const renderer = new TemplateRenderer('welcome', './src/emails')
12
- * const { html, text } = await renderer.render({ name: 'John' })
13
+ * const renderer = new TemplateRenderer('welcome', './src/emails');
14
+ * const result = await renderer.render({ name: 'John' });
13
15
  * ```
14
16
  *
15
- * @since 3.0.0
16
17
  * @public
18
+ * @since 3.0.0
17
19
  */
18
20
  export class TemplateRenderer implements Renderer {
19
21
  private template: string
20
22
  private viewsDir: string
23
+ private static engineCache = new Map<string, any>()
21
24
 
25
+ /**
26
+ * Creates an instance of TemplateRenderer.
27
+ *
28
+ * @param templateName - The name of the template file (without extension).
29
+ * @param viewsDir - The directory containing template files. Defaults to `src/emails`.
30
+ */
22
31
  constructor(templateName: string, viewsDir?: string) {
23
32
  this.template = templateName
24
33
  // Default to src/emails if not provided, falling back to process cwd
25
34
  this.viewsDir = viewsDir || `${process.cwd()}/src/emails`
26
35
  }
27
36
 
37
+ /**
38
+ * Renders the template with the provided data.
39
+ *
40
+ * This method lazily loads `@gravito/prism` to ensure the core package
41
+ * remains lightweight for users who don't need template rendering.
42
+ *
43
+ * @param data - The data context for template interpolation.
44
+ * @returns A promise resolving to the rendered content.
45
+ * @throws {Error} If the template engine fails to load or rendering fails.
46
+ */
28
47
  async render(data: Record<string, unknown>): Promise<RenderResult> {
29
48
  // Dynamic import to avoid hard dependency on @gravito/prism
30
49
  const { TemplateEngine } = await import('@gravito/prism')
31
- const engine = new TemplateEngine(this.viewsDir)
50
+
51
+ const cached = TemplateRenderer.engineCache.get(this.viewsDir)
52
+ const engine = cached || new TemplateEngine(this.viewsDir)
53
+
54
+ if (!cached) {
55
+ TemplateRenderer.engineCache.set(this.viewsDir, engine)
56
+ }
32
57
 
33
58
  // Disable automatic layout by default for emails, unless explicitly handled in template
34
59
  const html = engine.render(this.template, data, {})
35
60
 
36
61
  return {
37
62
  html,
38
- text: this.stripHtml(html),
63
+ text: stripHtml(html),
39
64
  }
40
65
  }
41
66
 
42
- private stripHtml(html: string): string {
43
- return html
44
- .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
45
- .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
46
- .replace(/<[^>]+>/g, '')
47
- .replace(/&nbsp;/g, ' ')
48
- .replace(/\s+/g, ' ')
49
- .trim()
67
+ /**
68
+ * Clear template engine cache.
69
+ *
70
+ * Useful in development environments to force recompilation of templates
71
+ * after they have been modified on disk.
72
+ *
73
+ * @example
74
+ * ```typescript
75
+ * TemplateRenderer.clearCache();
76
+ * ```
77
+ *
78
+ * @public
79
+ * @since 3.1.0
80
+ */
81
+ static clearCache(): void {
82
+ TemplateRenderer.engineCache.clear()
50
83
  }
51
84
  }
@@ -0,0 +1,99 @@
1
+ import { stripHtml } from '../utils/html'
2
+ import type { Renderer, RenderResult } from './Renderer'
3
+
4
+ /**
5
+ * Renderer for Vue component-based MJML emails.
6
+ *
7
+ * Renders Vue 3 components to MJML string using SSR, then converts
8
+ * the MJML to responsive HTML.
9
+ *
10
+ * @typeParam P - Props type for the Vue component.
11
+ * @public
12
+ * @since 1.1.0
13
+ */
14
+ export class VueMjmlRenderer<P extends object = object> implements Renderer {
15
+ /**
16
+ * Creates an instance of VueMjmlRenderer.
17
+ *
18
+ * @param component - The Vue component to render.
19
+ * @param props - Initial props for the component.
20
+ * @param options - Optional MJML transformation options.
21
+ * @param deps - Optional dependency injection for testing.
22
+ */
23
+ constructor(
24
+ private component: any,
25
+ private props?: P,
26
+ private options: Record<string, any> = {},
27
+ private deps: {
28
+ createSSRApp?: (...args: any[]) => any
29
+ h?: (...args: any[]) => any
30
+ renderToString?: (app: any) => Promise<string>
31
+ mjml2html?: (mjml: string, options?: any) => any
32
+ } = {}
33
+ ) {}
34
+
35
+ /**
36
+ * Renders the Vue component to a static HTML string via MJML.
37
+ *
38
+ * @param data - Runtime data to be merged with initial props.
39
+ * @returns A promise resolving to the rendered content.
40
+ * @throws {Error} If MJML rendering fails.
41
+ */
42
+ async render(data: Record<string, unknown>): Promise<RenderResult> {
43
+ let { createSSRApp, h, renderToString, mjml2html } = this.deps
44
+
45
+ if (!createSSRApp || !h || !renderToString) {
46
+ try {
47
+ const vue = await import('vue')
48
+ const vueServerRenderer = await import('@vue/server-renderer')
49
+ createSSRApp ??= vue.createSSRApp
50
+ h ??= vue.h
51
+ renderToString ??= vueServerRenderer.renderToString
52
+ } catch (_e) {
53
+ throw new Error(
54
+ '[OrbitSignal] The "vue" and "@vue/server-renderer" packages are required for VueMjmlRenderer. Please install them using "bun add vue @vue/server-renderer".'
55
+ )
56
+ }
57
+ }
58
+
59
+ if (!mjml2html) {
60
+ try {
61
+ mjml2html = (await import('mjml')).default
62
+ } catch (_e) {
63
+ throw new Error(
64
+ '[OrbitSignal] The "mjml" package is required for VueMjmlRenderer. Please install it using "bun add mjml".'
65
+ )
66
+ }
67
+ }
68
+
69
+ const mergedProps = { ...this.props, ...data }
70
+ const app = createSSRApp?.({
71
+ render: () => h?.(this.component, mergedProps),
72
+ })
73
+
74
+ const mjml = await renderToString?.(app)
75
+ if (!mjml) {
76
+ throw new Error('Failed to render MJML template')
77
+ }
78
+
79
+ const result = mjml2html?.(mjml, {
80
+ validationLevel: 'soft',
81
+ ...this.options,
82
+ })
83
+ if (!result) {
84
+ throw new Error('Failed to convert MJML to HTML')
85
+ }
86
+ const { html, errors } = result
87
+
88
+ if (errors && errors.length > 0 && this.options.validationLevel === 'strict') {
89
+ throw new Error(
90
+ `MJML rendering failed: ${errors.map((e: any) => e.formattedMessage).join(', ')}`
91
+ )
92
+ }
93
+
94
+ return {
95
+ html,
96
+ text: stripHtml(html),
97
+ }
98
+ }
99
+ }
@@ -1,26 +1,31 @@
1
+ import { stripHtml } from '../utils/html'
1
2
  import type { Renderer, RenderResult } from './Renderer'
2
3
 
3
4
  /**
4
5
  * Renderer for Vue component-based emails.
5
6
  *
6
- * Renders Vue 3 components to static HTML for email delivery
7
- * using server-side rendering. Supports optional dependency
8
- * injection for testing.
7
+ * Renders Vue 3 components to static HTML using server-side rendering (SSR).
8
+ * It lazily loads Vue and `@vue/server-renderer` dependencies only when needed,
9
+ * preventing unnecessary bundle bloat for users who do not use Vue renderers.
9
10
  *
10
11
  * @example
11
12
  * ```typescript
12
- * import { WelcomeEmail } from './emails/WelcomeEmail.vue'
13
- *
14
- * const renderer = new VueRenderer(WelcomeEmail, { name: 'John' })
15
- * const { html, text } = await renderer.render({ date: new Date() })
13
+ * const renderer = new VueRenderer(MyComponent, { title: 'Welcome' });
14
+ * const result = await renderer.render({ name: 'John' });
16
15
  * ```
17
16
  *
18
- * @typeParam P - Props type for the Vue component
19
- *
20
- * @since 3.0.0
17
+ * @typeParam P - Props type for the Vue component.
21
18
  * @public
19
+ * @since 3.0.0
22
20
  */
23
21
  export class VueRenderer<P extends object = object> implements Renderer {
22
+ /**
23
+ * Creates an instance of VueRenderer.
24
+ *
25
+ * @param component - The Vue component to render.
26
+ * @param props - Initial props for the component.
27
+ * @param deps - Optional dependency injection for testing.
28
+ */
24
29
  constructor(
25
30
  private component: any, // Use any to avoid hard Vue dependency in types
26
31
  private props?: P,
@@ -31,6 +36,16 @@ export class VueRenderer<P extends object = object> implements Renderer {
31
36
  } = {}
32
37
  ) {}
33
38
 
39
+ /**
40
+ * Renders the Vue component to a static HTML string.
41
+ *
42
+ * This method performs dynamic imports of `vue` and `@vue/server-renderer`
43
+ * to ensure they are only loaded if this renderer is actually used.
44
+ *
45
+ * @param data - Runtime data to be merged with initial props.
46
+ * @returns A promise resolving to the rendered content.
47
+ * @throws {Error} If Vue dependencies cannot be loaded or rendering fails.
48
+ */
34
49
  async render(data: Record<string, unknown>): Promise<RenderResult> {
35
50
  // Dynamic imports to avoid hard dependencies on vue/@vue/server-renderer
36
51
  const createSSRApp = this.deps.createSSRApp ?? (await import('vue')).createSSRApp
@@ -50,17 +65,7 @@ export class VueRenderer<P extends object = object> implements Renderer {
50
65
 
51
66
  return {
52
67
  html: fullHtml,
53
- text: this.stripHtml(html),
68
+ text: stripHtml(html),
54
69
  }
55
70
  }
56
-
57
- private stripHtml(html: string): string {
58
- return html
59
- .replace(/<style(?:\s[^>]*)?>[\s\S]*?<\/style>/gi, '')
60
- .replace(/<script(?:\s[^>]*)?>[\s\S]*?<\/script>/gi, '')
61
- .replace(/<[^>]+>/g, '')
62
- .replace(/&nbsp;/g, ' ')
63
- .replace(/\s+/g, ' ')
64
- .trim()
65
- }
66
71
  }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Base layout for MJML emails.
3
+ * Includes common head styles and responsive settings.
4
+ *
5
+ * Placeholder: {{content}}
6
+ */
7
+ export const baseLayout = `
8
+ <mjml>
9
+ <mj-head>
10
+ <mj-attributes>
11
+ <mj-all font-family="Arial, Helvetica, sans-serif" />
12
+ <mj-text font-size="16px" color="#333333" line-height="1.5" />
13
+ <mj-section padding="20px" />
14
+ </mj-attributes>
15
+ <mj-style>
16
+ .link-white { color: #ffffff !important; text-decoration: none; }
17
+ .footer-text { font-size: 12px; color: #999999; }
18
+ </mj-style>
19
+ </mj-head>
20
+ <mj-body background-color="#f4f4f4">
21
+ <mj-section background-color="#ffffff" padding-bottom="0px">
22
+ <mj-column>
23
+ <mj-image width="150px" src="https://gravito.dev/logo.png" alt="Gravito" />
24
+ </mj-column>
25
+ </mj-section>
26
+
27
+ {{content}}
28
+
29
+ <mj-section>
30
+ <mj-column>
31
+ <mj-divider border-width="1px" border-color="#dddddd" />
32
+ <mj-text align="center" css-class="footer-text">
33
+ &copy; ${new Date().getFullYear()} Gravito Framework. All rights reserved.
34
+ </mj-text>
35
+ </mj-column>
36
+ </mj-section>
37
+ </mj-body>
38
+ </mjml>
39
+ `
40
+
41
+ /**
42
+ * A simple transactional component layout.
43
+ */
44
+ export const transactionLayout = `
45
+ <mj-section background-color="#ffffff" padding-top="0px">
46
+ <mj-column>
47
+ {{content}}
48
+ </mj-column>
49
+ </mj-section>
50
+ `