@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.
- package/README.md +129 -0
- package/dist/OrbitMail-2Z7ZTKYA.mjs +7 -0
- package/dist/OrbitMail-BGV32HWN.mjs +7 -0
- package/dist/OrbitMail-FUYZQSAV.mjs +7 -0
- package/dist/OrbitMail-NAPCRK7B.mjs +7 -0
- package/dist/OrbitMail-REGJ276B.mjs +7 -0
- package/dist/OrbitMail-TCFBJWDT.mjs +7 -0
- package/dist/OrbitMail-XZZW6U4N.mjs +7 -0
- package/dist/OrbitSignal-ZKKMEC27.mjs +7 -0
- package/dist/ReactRenderer-L5INVYKT.mjs +27 -0
- package/dist/VueRenderer-S65ZARRI.mjs +37129 -0
- package/dist/VueRenderer-Z5PRVBNH.mjs +37298 -0
- package/dist/chunk-3U2CYJO5.mjs +367 -0
- package/dist/chunk-3XFC4T6M.mjs +392 -0
- package/dist/chunk-6DZX6EAA.mjs +37 -0
- package/dist/chunk-DT3R2TNV.mjs +367 -0
- package/dist/chunk-GADWIVC4.mjs +400 -0
- package/dist/chunk-HHKFAMSE.mjs +380 -0
- package/dist/chunk-OKRNL6PN.mjs +400 -0
- package/dist/chunk-ULN3GMY2.mjs +367 -0
- package/dist/chunk-XAWO7RSP.mjs +398 -0
- package/dist/index.d.mts +278 -0
- package/dist/index.d.ts +278 -0
- package/dist/index.js +38150 -0
- package/dist/index.mjs +316 -0
- package/package.json +73 -0
- package/src/Mailable.ts +245 -0
- package/src/OrbitSignal.ts +158 -0
- package/src/Queueable.ts +9 -0
- package/src/dev/DevMailbox.ts +64 -0
- package/src/dev/DevServer.ts +89 -0
- package/src/dev/ui/mailbox.ts +68 -0
- package/src/dev/ui/preview.ts +59 -0
- package/src/dev/ui/shared.ts +46 -0
- package/src/index.ts +20 -0
- package/src/renderers/HtmlRenderer.ts +22 -0
- package/src/renderers/ReactRenderer.ts +35 -0
- package/src/renderers/Renderer.ts +11 -0
- package/src/renderers/TemplateRenderer.ts +34 -0
- package/src/renderers/VueRenderer.ts +37 -0
- package/src/transports/LogTransport.ts +17 -0
- package/src/transports/MemoryTransport.ts +11 -0
- package/src/transports/SesTransport.ts +56 -0
- package/src/transports/SmtpTransport.ts +50 -0
- package/src/transports/Transport.ts +8 -0
- package/src/types.ts +71 -0
- package/tests/mailable.test.ts +77 -0
- package/tests/renderers.test.ts +56 -0
- package/tests/transports.test.ts +52 -0
- 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
|
+
}
|
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
|
+
}
|