@gravito/signal 1.0.0-alpha.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 (50) hide show
  1. package/README.md +129 -0
  2. package/dist/OrbitMail-2Z7ZTKYA.mjs +7 -0
  3. package/dist/OrbitMail-BGV32HWN.mjs +7 -0
  4. package/dist/OrbitMail-FUYZQSAV.mjs +7 -0
  5. package/dist/OrbitMail-NAPCRK7B.mjs +7 -0
  6. package/dist/OrbitMail-REGJ276B.mjs +7 -0
  7. package/dist/OrbitMail-TCFBJWDT.mjs +7 -0
  8. package/dist/OrbitMail-XZZW6U4N.mjs +7 -0
  9. package/dist/OrbitSignal-ZKKMEC27.mjs +7 -0
  10. package/dist/ReactRenderer-L5INVYKT.mjs +27 -0
  11. package/dist/VueRenderer-S65ZARRI.mjs +37129 -0
  12. package/dist/VueRenderer-Z5PRVBNH.mjs +37298 -0
  13. package/dist/chunk-3U2CYJO5.mjs +367 -0
  14. package/dist/chunk-3XFC4T6M.mjs +392 -0
  15. package/dist/chunk-6DZX6EAA.mjs +37 -0
  16. package/dist/chunk-DT3R2TNV.mjs +367 -0
  17. package/dist/chunk-GADWIVC4.mjs +400 -0
  18. package/dist/chunk-HHKFAMSE.mjs +380 -0
  19. package/dist/chunk-OKRNL6PN.mjs +400 -0
  20. package/dist/chunk-ULN3GMY2.mjs +367 -0
  21. package/dist/chunk-XAWO7RSP.mjs +398 -0
  22. package/dist/index.d.mts +278 -0
  23. package/dist/index.d.ts +278 -0
  24. package/dist/index.js +38150 -0
  25. package/dist/index.mjs +316 -0
  26. package/package.json +73 -0
  27. package/src/Mailable.ts +245 -0
  28. package/src/OrbitSignal.ts +158 -0
  29. package/src/Queueable.ts +9 -0
  30. package/src/dev/DevMailbox.ts +64 -0
  31. package/src/dev/DevServer.ts +89 -0
  32. package/src/dev/ui/mailbox.ts +68 -0
  33. package/src/dev/ui/preview.ts +59 -0
  34. package/src/dev/ui/shared.ts +46 -0
  35. package/src/index.ts +20 -0
  36. package/src/renderers/HtmlRenderer.ts +22 -0
  37. package/src/renderers/ReactRenderer.ts +35 -0
  38. package/src/renderers/Renderer.ts +11 -0
  39. package/src/renderers/TemplateRenderer.ts +34 -0
  40. package/src/renderers/VueRenderer.ts +37 -0
  41. package/src/transports/LogTransport.ts +17 -0
  42. package/src/transports/MemoryTransport.ts +11 -0
  43. package/src/transports/SesTransport.ts +56 -0
  44. package/src/transports/SmtpTransport.ts +50 -0
  45. package/src/transports/Transport.ts +8 -0
  46. package/src/types.ts +71 -0
  47. package/tests/mailable.test.ts +77 -0
  48. package/tests/renderers.test.ts +56 -0
  49. package/tests/transports.test.ts +52 -0
  50. package/tsconfig.json +19 -0
@@ -0,0 +1,11 @@
1
+ import type { DevMailbox } from '../dev/DevMailbox'
2
+ import type { Message, Transport } from '../types'
3
+
4
+ export class MemoryTransport implements Transport {
5
+ constructor(private mailbox: DevMailbox) {}
6
+
7
+ async send(message: Message): Promise<void> {
8
+ this.mailbox.add(message)
9
+ // console.log(`[MemoryTransport] Email stored in DevMailbox for: ${message.to.map(t => t.address).join(', ')}`);
10
+ }
11
+ }
@@ -0,0 +1,56 @@
1
+ import { SESClient, SendRawEmailCommand } from '@aws-sdk/client-ses'
2
+ import nodemailer from 'nodemailer'
3
+ import type { Address, Message, Transport } from '../types'
4
+
5
+ export interface SesConfig {
6
+ region: string
7
+ accessKeyId?: string
8
+ secretAccessKey?: string
9
+ }
10
+
11
+ export class SesTransport implements Transport {
12
+ private transporter: nodemailer.Transporter
13
+
14
+ constructor(config: SesConfig) {
15
+ const clientConfig: any = { region: config.region }
16
+
17
+ if (config.accessKeyId && config.secretAccessKey) {
18
+ clientConfig.credentials = {
19
+ accessKeyId: config.accessKeyId,
20
+ secretAccessKey: config.secretAccessKey,
21
+ }
22
+ }
23
+
24
+ const ses = new SESClient(clientConfig)
25
+
26
+ this.transporter = nodemailer.createTransport({
27
+ SES: { ses, aws: { SendRawEmailCommand } },
28
+ } as any)
29
+ }
30
+
31
+ async send(message: Message): Promise<void> {
32
+ await this.transporter.sendMail({
33
+ from: this.formatAddress(message.from),
34
+ to: message.to.map(this.formatAddress),
35
+ cc: message.cc?.map(this.formatAddress),
36
+ bcc: message.bcc?.map(this.formatAddress),
37
+ replyTo: message.replyTo ? this.formatAddress(message.replyTo) : undefined,
38
+ subject: message.subject,
39
+ html: message.html,
40
+ text: message.text,
41
+ headers: message.headers,
42
+ priority: message.priority,
43
+ attachments: message.attachments?.map((a) => ({
44
+ filename: a.filename,
45
+ content: a.content,
46
+ contentType: a.contentType,
47
+ cid: a.cid,
48
+ encoding: a.encoding,
49
+ })),
50
+ })
51
+ }
52
+
53
+ private formatAddress(addr: Address): string {
54
+ return addr.name ? `"${addr.name}" <${addr.address}>` : addr.address
55
+ }
56
+ }
@@ -0,0 +1,50 @@
1
+ import nodemailer from 'nodemailer'
2
+ import type { Address, Message, Transport } from '../types'
3
+
4
+ export interface SmtpConfig {
5
+ host: string
6
+ port: number
7
+ secure?: boolean
8
+ auth?: {
9
+ user: string
10
+ pass: string
11
+ }
12
+ tls?: {
13
+ rejectUnauthorized?: boolean
14
+ ciphers?: string
15
+ }
16
+ }
17
+
18
+ export class SmtpTransport implements Transport {
19
+ private transporter: nodemailer.Transporter
20
+
21
+ constructor(config: SmtpConfig) {
22
+ this.transporter = nodemailer.createTransport(config)
23
+ }
24
+
25
+ async send(message: Message): Promise<void> {
26
+ await this.transporter.sendMail({
27
+ from: this.formatAddress(message.from),
28
+ to: message.to.map(this.formatAddress),
29
+ cc: message.cc?.map(this.formatAddress),
30
+ bcc: message.bcc?.map(this.formatAddress),
31
+ replyTo: message.replyTo ? this.formatAddress(message.replyTo) : undefined,
32
+ subject: message.subject,
33
+ html: message.html,
34
+ text: message.text,
35
+ headers: message.headers,
36
+ priority: message.priority,
37
+ attachments: message.attachments?.map((a) => ({
38
+ filename: a.filename,
39
+ content: a.content,
40
+ contentType: a.contentType,
41
+ cid: a.cid,
42
+ encoding: a.encoding,
43
+ })),
44
+ })
45
+ }
46
+
47
+ private formatAddress(addr: Address): string {
48
+ return addr.name ? `"${addr.name}" <${addr.address}>` : addr.address
49
+ }
50
+ }
@@ -0,0 +1,8 @@
1
+ import type { Message } from '../types'
2
+
3
+ export interface Transport {
4
+ /**
5
+ * Send the given message
6
+ */
7
+ send(message: Message): Promise<void>
8
+ }
package/src/types.ts ADDED
@@ -0,0 +1,71 @@
1
+ import type { Transport } from './transports/Transport'
2
+ export type { Transport }
3
+
4
+ export interface Address {
5
+ name?: string
6
+ address: string
7
+ }
8
+
9
+ export interface Attachment {
10
+ filename: string
11
+ content: string | Buffer // Buffer for Node.js
12
+ contentType?: string
13
+ cid?: string // Content-ID for inline images
14
+ encoding?: string
15
+ }
16
+
17
+ export interface Envelope {
18
+ from?: Address | undefined
19
+ to?: Address[] | undefined
20
+ cc?: Address[] | undefined
21
+ bcc?: Address[] | undefined
22
+ replyTo?: Address | undefined
23
+ subject?: string | undefined
24
+ priority?: 'high' | 'normal' | 'low' | undefined
25
+ attachments?: Attachment[] | undefined
26
+ }
27
+
28
+ export interface Message extends Envelope {
29
+ from: Address // From is required in finalized message
30
+ to: Address[] // To is required in finalized message
31
+ subject: string
32
+ html: string // The rendered HTML content
33
+ text?: string // The rendered plain text content
34
+ headers?: Record<string, string>
35
+ }
36
+
37
+ export interface MailConfig {
38
+ /**
39
+ * Default sender address
40
+ */
41
+ from?: Address
42
+
43
+ /**
44
+ * The transport mechanism used to send emails
45
+ */
46
+ transport?: Transport
47
+
48
+ /**
49
+ * Enable development mode (intercepts emails)
50
+ */
51
+ devMode?: boolean | undefined
52
+
53
+ /**
54
+ * Directory where email templates are located (for OrbitPrism)
55
+ * Default: src/emails
56
+ */
57
+ viewsDir?: string | undefined
58
+
59
+ /**
60
+ * URL prefix for Dev UI
61
+ * Default: /__mail
62
+ */
63
+ devUiPrefix?: string | undefined
64
+
65
+ /**
66
+ * Translation function for i18n support
67
+ */
68
+ translator?:
69
+ | ((key: string, replace?: Record<string, unknown>, locale?: string) => string)
70
+ | undefined
71
+ }
@@ -0,0 +1,77 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { Mailable } from '../src/Mailable'
3
+ import { OrbitSignal } from '../src/OrbitSignal'
4
+ import { LogTransport } from '../src/transports/LogTransport'
5
+ import type { Message, Transport } from '../src/transports/Transport'
6
+
7
+ // Mock Transport
8
+ class MockTransport implements Transport {
9
+ public sentMessages: Message[] = []
10
+ async send(message: Message): Promise<void> {
11
+ this.sentMessages.push(message)
12
+ }
13
+ }
14
+
15
+ // Example Mailable
16
+ class WelcomeMail extends Mailable {
17
+ constructor(private name: string) {
18
+ super()
19
+ }
20
+
21
+ build() {
22
+ return this.subject('Welcome!').html(`<h1>Welcome, ${this.name}</h1>`)
23
+ }
24
+ }
25
+
26
+ describe('OrbitSignal Core', () => {
27
+ it('should send a simple email via transport', async () => {
28
+ const transport = new MockTransport()
29
+
30
+ const mailer = OrbitSignal.configure({
31
+ from: { name: 'System', address: 'system@example.com' },
32
+ transport: transport,
33
+ })
34
+
35
+ const mail = new WelcomeMail('Carl').to('carl@example.com')
36
+
37
+ await mailer.send(mail)
38
+
39
+ expect(transport.sentMessages.length).toBe(1)
40
+ const msg = transport.sentMessages[0]
41
+
42
+ expect(msg.to[0].address).toBe('carl@example.com')
43
+ expect(msg.subject).toBe('Welcome!')
44
+ expect(msg.html).toBe('<h1>Welcome, Carl</h1>')
45
+ expect(msg.from.address).toBe('system@example.com')
46
+ })
47
+
48
+ it('should handle fluent API correctly', async () => {
49
+ const mail = new WelcomeMail('Carl')
50
+ .to(['a@b.com', { address: 'c@d.com', name: 'C' }])
51
+ .cc('cc@example.com')
52
+ .priority('high')
53
+
54
+ const config = {
55
+ from: { address: 'default@example.com' },
56
+ transport: new LogTransport(),
57
+ }
58
+
59
+ const envelope = await mail.buildEnvelope(config)
60
+
61
+ expect(envelope.to).toHaveLength(2)
62
+ expect(envelope.cc).toHaveLength(1)
63
+ expect(envelope.priority).toBe('high')
64
+ expect(envelope.from?.address).toBe('default@example.com')
65
+ })
66
+
67
+ it('should throw error if renderer is missing', async () => {
68
+ class EmptyMail extends Mailable {
69
+ build() {
70
+ return this
71
+ }
72
+ }
73
+
74
+ const mail = new EmptyMail()
75
+ expect(mail.renderContent()).rejects.toThrow('No content renderer')
76
+ })
77
+ })
@@ -0,0 +1,56 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { createElement } from 'react'
3
+ import { defineComponent, h } from 'vue'
4
+ import { Mailable } from '../src/Mailable'
5
+ import { LogTransport } from '../src/transports/LogTransport'
6
+
7
+ // Mock Config for Mailable
8
+ const mockConfig = {
9
+ from: { address: 'test@example.com' },
10
+ transport: new LogTransport(),
11
+ }
12
+
13
+ // React Component
14
+ const ReactTestComponent = ({ name }: { name: string }) => {
15
+ return createElement('div', {}, createElement('h1', {}, `Hello ${name}`))
16
+ }
17
+
18
+ // Vue Component
19
+ const VueTestComponent = defineComponent({
20
+ props: ['name'],
21
+ render() {
22
+ return h('div', [h('h1', `Hello ${this.name}`)])
23
+ },
24
+ })
25
+
26
+ class TestReactMail extends Mailable {
27
+ build() {
28
+ return this.react(ReactTestComponent, { name: 'World' }).subject('React Test')
29
+ }
30
+ }
31
+
32
+ class TestVueMail extends Mailable {
33
+ build() {
34
+ return this.vue(VueTestComponent, { name: 'World' }).subject('Vue Test')
35
+ }
36
+ }
37
+
38
+ describe('Renderers', () => {
39
+ it('should render React component to HTML', async () => {
40
+ const mail = new TestReactMail()
41
+ const _envelope = await mail.buildEnvelope(mockConfig)
42
+ const { html } = await mail.renderContent()
43
+
44
+ expect(html).toContain('<!DOCTYPE html>')
45
+ expect(html).toContain('<h1>Hello World</h1>')
46
+ })
47
+
48
+ it('should render Vue component to HTML', async () => {
49
+ const mail = new TestVueMail()
50
+ const _envelope = await mail.buildEnvelope(mockConfig)
51
+ const { html } = await mail.renderContent()
52
+
53
+ expect(html).toContain('<!DOCTYPE html>')
54
+ expect(html).toContain('<h1>Hello World</h1>')
55
+ })
56
+ })
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { DevMailbox } from '../src/dev/DevMailbox'
3
+ import { MemoryTransport } from '../src/transports/MemoryTransport'
4
+ import type { Message } from '../src/types'
5
+
6
+ describe('Transports', () => {
7
+ describe('MemoryTransport & DevMailbox', () => {
8
+ it('should store sent emails in mailbox', async () => {
9
+ const mailbox = new DevMailbox()
10
+ const transport = new MemoryTransport(mailbox)
11
+
12
+ const message: Message = {
13
+ from: { address: 'from@example.com' },
14
+ to: [{ address: 'to@example.com' }],
15
+ subject: 'Test Subject',
16
+ html: '<p>Content</p>',
17
+ priority: 'normal',
18
+ }
19
+
20
+ await transport.send(message)
21
+
22
+ const entries = mailbox.list()
23
+ expect(entries).toHaveLength(1)
24
+
25
+ const entry = entries[0]
26
+ expect(entry.envelope.subject).toBe('Test Subject')
27
+ expect(entry.html).toBe('<p>Content</p>')
28
+ expect(entry.sentAt).toBeInstanceOf(Date)
29
+ })
30
+
31
+ it('should limit mailbox size', async () => {
32
+ const mailbox = new DevMailbox()
33
+ // Hack: set max entries lower for test if possible, or just add 51 entries
34
+ // Since maxEntries is private, we just add 51
35
+ const transport = new MemoryTransport(mailbox)
36
+
37
+ for (let i = 0; i < 60; i++) {
38
+ await transport.send({
39
+ from: { address: 'a@b.com' },
40
+ to: [{ address: 'b@c.com' }],
41
+ subject: `Msg ${i}`,
42
+ html: 'content',
43
+ priority: 'normal',
44
+ })
45
+ }
46
+
47
+ expect(mailbox.list().length).toBeLessThanOrEqual(50)
48
+ // Should be the latest ones (last one added is index 0)
49
+ expect(mailbox.list()[0].envelope.subject).toBe('Msg 59')
50
+ })
51
+ })
52
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "baseUrl": ".",
6
+ "paths": {
7
+ "gravito-core": ["../../packages/core/src/index.ts"],
8
+ "@gravito/*": ["../../packages/*/src/index.ts"]
9
+ }
10
+ },
11
+ "include": [
12
+ "src/**/*"
13
+ ],
14
+ "exclude": [
15
+ "node_modules",
16
+ "dist",
17
+ "**/*.test.ts"
18
+ ]
19
+ }