@forinda/kickjs-mailer 3.1.3 → 4.0.0
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 +18 -29
- package/dist/index.d.mts +25 -20
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +61 -22
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -12
package/README.md
CHANGED
|
@@ -1,59 +1,48 @@
|
|
|
1
1
|
# @forinda/kickjs-mailer
|
|
2
2
|
|
|
3
|
-
Pluggable email
|
|
3
|
+
Pluggable email for KickJS — SMTP (`nodemailer`), Resend, SES, Console (dev), or any custom `MailProvider`.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
# Using the KickJS CLI (recommended — auto-installs peer dependencies)
|
|
9
8
|
kick add mailer
|
|
10
|
-
|
|
11
|
-
# Manual install
|
|
12
|
-
pnpm add @forinda/kickjs-mailer nodemailer
|
|
13
9
|
```
|
|
14
10
|
|
|
15
|
-
## Features
|
|
16
|
-
|
|
17
|
-
- `MailerAdapter` — lifecycle adapter that registers the mailer in DI
|
|
18
|
-
- `MailerService` — injectable service for sending email
|
|
19
|
-
- Built-in providers: `SmtpProvider` (nodemailer), `ConsoleProvider` (dev logging)
|
|
20
|
-
- `MAILER` token for DI injection
|
|
21
|
-
- Pluggable `MailProvider` interface for custom transports (Resend, SES, etc.)
|
|
22
|
-
|
|
23
11
|
## Quick Example
|
|
24
12
|
|
|
25
|
-
```
|
|
26
|
-
import {
|
|
13
|
+
```ts
|
|
14
|
+
import { bootstrap, getEnv, Inject, Service } from '@forinda/kickjs'
|
|
15
|
+
import { MailerAdapter, SmtpProvider, ConsoleProvider, MAILER, type MailerService } from '@forinda/kickjs-mailer'
|
|
16
|
+
import { modules } from './modules'
|
|
27
17
|
|
|
28
|
-
bootstrap({
|
|
18
|
+
export const app = await bootstrap({
|
|
29
19
|
modules,
|
|
30
20
|
adapters: [
|
|
31
|
-
|
|
32
|
-
provider:
|
|
33
|
-
? new SmtpProvider({
|
|
21
|
+
MailerAdapter({
|
|
22
|
+
provider: getEnv('NODE_ENV') === 'production'
|
|
23
|
+
? new SmtpProvider({
|
|
24
|
+
host: 'smtp.example.com',
|
|
25
|
+
port: 587,
|
|
26
|
+
auth: { user: getEnv('SMTP_USER'), pass: getEnv('SMTP_PASS') },
|
|
27
|
+
})
|
|
34
28
|
: new ConsoleProvider(),
|
|
35
29
|
}),
|
|
36
30
|
],
|
|
37
31
|
})
|
|
38
32
|
|
|
39
|
-
// In a service
|
|
40
33
|
@Service()
|
|
41
34
|
class NotifyService {
|
|
42
|
-
@Inject(MAILER) private mailer
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
to: email,
|
|
47
|
-
subject: 'Welcome!',
|
|
48
|
-
html: '<h1>Welcome to our app</h1>',
|
|
49
|
-
})
|
|
35
|
+
constructor(@Inject(MAILER) private mailer: MailerService) {}
|
|
36
|
+
|
|
37
|
+
sendWelcome(to: string) {
|
|
38
|
+
return this.mailer.send({ to, subject: 'Welcome', html: '<h1>Welcome</h1>' })
|
|
50
39
|
}
|
|
51
40
|
}
|
|
52
41
|
```
|
|
53
42
|
|
|
54
43
|
## Documentation
|
|
55
44
|
|
|
56
|
-
[
|
|
45
|
+
[forinda.github.io/kick-js/guide/mailer](https://forinda.github.io/kick-js/guide/mailer)
|
|
57
46
|
|
|
58
47
|
## License
|
|
59
48
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import
|
|
2
|
+
import * as _$_forinda_kickjs0 from "@forinda/kickjs";
|
|
3
3
|
|
|
4
4
|
//#region src/types.d.ts
|
|
5
5
|
interface MailAddress {
|
|
@@ -121,8 +121,8 @@ interface MailerOptions {
|
|
|
121
121
|
}
|
|
122
122
|
//#endregion
|
|
123
123
|
//#region src/mailer.service.d.ts
|
|
124
|
-
/** DI token for resolving MailerService from the container */
|
|
125
|
-
declare const MAILER:
|
|
124
|
+
/** DI token for resolving MailerService from the container. */
|
|
125
|
+
declare const MAILER: _$_forinda_kickjs0.InjectionToken<MailerService>;
|
|
126
126
|
/**
|
|
127
127
|
* Central mail service — send emails through any provider.
|
|
128
128
|
*
|
|
@@ -151,10 +151,16 @@ declare const MAILER: unique symbol;
|
|
|
151
151
|
* ```
|
|
152
152
|
*/
|
|
153
153
|
declare class MailerService {
|
|
154
|
-
private provider;
|
|
155
|
-
private defaultFrom?;
|
|
156
|
-
private templateEngine?;
|
|
157
|
-
private enabled;
|
|
154
|
+
private readonly provider;
|
|
155
|
+
private readonly defaultFrom?;
|
|
156
|
+
private readonly templateEngine?;
|
|
157
|
+
private readonly enabled;
|
|
158
|
+
/** Total messages successfully accepted by the provider. */
|
|
159
|
+
sentCount: number;
|
|
160
|
+
/** Total messages that threw inside provider.send(). */
|
|
161
|
+
failedCount: number;
|
|
162
|
+
/** Total messages skipped because `enabled = false` (dry-run mode). */
|
|
163
|
+
dryRunCount: number;
|
|
158
164
|
constructor(options: MailerOptions);
|
|
159
165
|
/**
|
|
160
166
|
* Send an email message.
|
|
@@ -186,24 +192,23 @@ declare class MailerService {
|
|
|
186
192
|
*
|
|
187
193
|
* bootstrap({
|
|
188
194
|
* adapters: [
|
|
189
|
-
*
|
|
195
|
+
* MailerAdapter({
|
|
190
196
|
* provider: new SmtpProvider({ host: 'smtp.gmail.com', port: 587, auth: { ... } }),
|
|
191
197
|
* defaultFrom: { name: 'My App', address: 'noreply@myapp.com' },
|
|
192
198
|
* }),
|
|
193
199
|
* ],
|
|
194
200
|
* })
|
|
201
|
+
*
|
|
202
|
+
* // Multiple providers via .scoped() — e.g. transactional + marketing pipelines:
|
|
203
|
+
* bootstrap({
|
|
204
|
+
* adapters: [
|
|
205
|
+
* MailerAdapter.scoped('transactional', { provider: new ResendProvider({ ... }) }),
|
|
206
|
+
* MailerAdapter.scoped('marketing', { provider: new SesProvider({ ... }) }),
|
|
207
|
+
* ],
|
|
208
|
+
* })
|
|
195
209
|
* ```
|
|
196
210
|
*/
|
|
197
|
-
declare
|
|
198
|
-
private readonly options;
|
|
199
|
-
name: string;
|
|
200
|
-
private readonly mailer;
|
|
201
|
-
constructor(options: MailerOptions);
|
|
202
|
-
beforeStart({
|
|
203
|
-
container
|
|
204
|
-
}: AdapterContext): void;
|
|
205
|
-
shutdown(): Promise<void>;
|
|
206
|
-
}
|
|
211
|
+
declare const MailerAdapter: _$_forinda_kickjs0.AdapterFactory<MailerOptions, unknown>;
|
|
207
212
|
//#endregion
|
|
208
213
|
//#region src/providers/smtp.provider.d.ts
|
|
209
214
|
interface SmtpOptions {
|
|
@@ -243,7 +248,7 @@ interface SmtpOptions {
|
|
|
243
248
|
* host: 'smtp.resend.com',
|
|
244
249
|
* port: 465,
|
|
245
250
|
* secure: true,
|
|
246
|
-
* auth: { user: 'resend', pass:
|
|
251
|
+
* auth: { user: 'resend', pass: getEnv('RESEND_API_KEY') },
|
|
247
252
|
* })
|
|
248
253
|
*
|
|
249
254
|
* // Mailpit (local dev)
|
|
@@ -267,7 +272,7 @@ declare class SmtpProvider implements MailProvider {
|
|
|
267
272
|
*
|
|
268
273
|
* @example
|
|
269
274
|
* ```ts
|
|
270
|
-
*
|
|
275
|
+
* MailerAdapter({
|
|
271
276
|
* provider: new ConsoleProvider(),
|
|
272
277
|
* defaultFrom: 'dev@localhost',
|
|
273
278
|
* })
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/mailer.service.ts","../src/adapter.ts","../src/providers/smtp.provider.ts","../src/providers/console.provider.ts"],"mappings":";;;;UAEiB,WAAA;EACf,IAAA;EACA,OAAA;AAAA;AAAA,KAGU,aAAA,YAAyB,WAAA;AAAA,UAEpB,cAAA;EACf,QAAA;EACA,OAAA,YAAmB,MAAA;EACnB,IAAA;EACA,WAAA;EACA,QAAA;AAAA;AAAA,UAGe,WAAA;EAV+B;EAY9C,IAAA,GAAO,aAAA;EAVsB;EAY7B,EAAA,EAAI,aAAA,GAAgB,aAAA;EAVK;EAYzB,EAAA,GAAK,aAAA,GAAgB,aAAA;EAZrB;EAcA,GAAA,GAAM,aAAA,GAAgB,aAAA;EAbtB;EAeA,OAAA,GAAU,aAAA;EAbV;EAeA,OAAA;EAfQ;EAiBR,IAAA;EAd0B;EAgB1B,IAAA;EAdO;EAgBP,WAAA,GAAc,cAAA;EAdM;EAgBpB,OAAA,GAAU,MAAA;EAdW;EAgBrB,QAAA,GAAW,MAAA;AAAA;AAAA,UAGI,UAAA;EAPD;EASd,SAAA;EALW;EAOX,QAAA;EAPiB;EASjB,GAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;AANF;UAkCiB,kBAAA;;EAEf,MAAA,CAAO,QAAA,UAAkB,IAAA,EAAM,MAAA,gBAAsB,OAAA;AAAA;;;;;AAFvD;;;;;;;;;;;AAmCA;;;;;;;;;;;;;UAAiB,YAAA;EAKqB;EAHpC,IAAA;EAMa;EAHb,IAAA,CAAK,OAAA,EAAS,WAAA,GAAc,OAAA,CAAQ,UAAA;EAGhB;EAApB,QAAA,KAAa,OAAA;AAAA;AAAA,UAKE,aAAA;EAEL;EAAV,QAAA,EAAU,YAAA;EAMO;EAHjB,WAAA,GAAc,aAAA;EAGqB;EAAnC,cAAA,GAAiB,kBAAA;EANP;EASV,OAAA;AAAA;;;;cC1HW,MAAA;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/mailer.service.ts","../src/adapter.ts","../src/providers/smtp.provider.ts","../src/providers/console.provider.ts"],"mappings":";;;;UAEiB,WAAA;EACf,IAAA;EACA,OAAA;AAAA;AAAA,KAGU,aAAA,YAAyB,WAAA;AAAA,UAEpB,cAAA;EACf,QAAA;EACA,OAAA,YAAmB,MAAA;EACnB,IAAA;EACA,WAAA;EACA,QAAA;AAAA;AAAA,UAGe,WAAA;EAV+B;EAY9C,IAAA,GAAO,aAAA;EAVsB;EAY7B,EAAA,EAAI,aAAA,GAAgB,aAAA;EAVK;EAYzB,EAAA,GAAK,aAAA,GAAgB,aAAA;EAZrB;EAcA,GAAA,GAAM,aAAA,GAAgB,aAAA;EAbtB;EAeA,OAAA,GAAU,aAAA;EAbV;EAeA,OAAA;EAfQ;EAiBR,IAAA;EAd0B;EAgB1B,IAAA;EAdO;EAgBP,WAAA,GAAc,cAAA;EAdM;EAgBpB,OAAA,GAAU,MAAA;EAdW;EAgBrB,QAAA,GAAW,MAAA;AAAA;AAAA,UAGI,UAAA;EAPD;EASd,SAAA;EALW;EAOX,QAAA;EAPiB;EASjB,GAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;AANF;UAkCiB,kBAAA;;EAEf,MAAA,CAAO,QAAA,UAAkB,IAAA,EAAM,MAAA,gBAAsB,OAAA;AAAA;;;;;AAFvD;;;;;;;;;;;AAmCA;;;;;;;;;;;;;UAAiB,YAAA;EAKqB;EAHpC,IAAA;EAMa;EAHb,IAAA,CAAK,OAAA,EAAS,WAAA,GAAc,OAAA,CAAQ,UAAA;EAGhB;EAApB,QAAA,KAAa,OAAA;AAAA;AAAA,UAKE,aAAA;EAEL;EAAV,QAAA,EAAU,YAAA;EAMO;EAHjB,WAAA,GAAc,aAAA;EAGqB;EAAnC,cAAA,GAAiB,kBAAA;EANP;EASV,OAAA;AAAA;;;;cC1HW,MAAA,EAAM,kBAAA,CAAA,cAAA,CAAA,aAAA;;ADXnB;;;;;AAKA;;;;;AAEA;;;;;;;;;;;;AAQA;;;;cCyBa,aAAA;EAAA,iBACM,QAAA;EAAA,iBACA,WAAA;EAAA,iBACA,cAAA;EAAA,iBACA,OAAA;EDrBK;EC2Bf,SAAA;EDjBO;ECmBP,WAAA;EDfI;ECiBJ,WAAA;cAEK,OAAA,EAAS,aAAA;EDvCrB;;;;ECkDM,IAAA,CAAK,OAAA,EAAS,WAAA,GAAc,OAAA,CAAQ,UAAA;ED9C1C;;;;;;;;ECgFM,YAAA,CACJ,QAAA,UACA,OAAA,EAAS,IAAA,CAAK,WAAA,WACd,IAAA,EAAM,MAAA,gBACL,OAAA,CAAQ,UAAA;ED5EX;ECyFA,WAAA,CAAA,GAAe,YAAA;EDrFf;EC0FM,QAAA,CAAA,GAAY,OAAA;AAAA;;;;;;AD3HpB;;;;;AAKA;;;;;AAEA;;;;;;;;;;;;cEuBa,aAAA,EAAa,kBAAA,CAAA,cAAA,CAAA,aAAA;;;UC9BT,WAAA;;EAEf,IAAA;EHFe;EGIf,IAAA;;EAEA,MAAA;EHJO;EGMP,IAAA;IACE,IAAA;IACA,IAAA;EAAA;EHL4C;EGQ9C,iBAAA;AAAA;;;;;;;;;;;AHEF;;;;;;;;;;;;;;;;;;;cG8Ba,YAAA,YAAwB,YAAA;EAAA,QAIf,OAAA;EAHpB,IAAA;EAAA,QACQ,WAAA;cAEY,OAAA,EAAS,WAAA;EAAA,QAEf,iBAAA;EAiBR,IAAA,CAAK,OAAA,EAAS,WAAA,GAAc,OAAA,CAAQ,UAAA;EAuBpC,QAAA,CAAA,GAAY,OAAA;AAAA;;;;;;AH3FpB;;;;;AAKA;;;;cIYa,eAAA,YAA2B,YAAA;EACtC,IAAA;EAEM,IAAA,CAAK,OAAA,EAAS,WAAA,GAAc,OAAA,CAAQ,UAAA;AAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-mailer
|
|
2
|
+
* @forinda/kickjs-mailer v4.0.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
11
|
import "reflect-metadata";
|
|
12
|
-
import { Logger, ref } from "@forinda/kickjs";
|
|
12
|
+
import { Logger, createToken, defineAdapter, ref } from "@forinda/kickjs";
|
|
13
|
+
import { PROTOCOL_VERSION } from "@forinda/kickjs-devtools-kit";
|
|
13
14
|
//#region src/mailer.service.ts
|
|
14
15
|
const log$2 = Logger.for("Mailer");
|
|
15
|
-
/** DI token for resolving MailerService from the container */
|
|
16
|
-
const MAILER =
|
|
16
|
+
/** DI token for resolving MailerService from the container. */
|
|
17
|
+
const MAILER = createToken("kick/mailer/Service");
|
|
17
18
|
/**
|
|
18
19
|
* Central mail service — send emails through any provider.
|
|
19
20
|
*
|
|
@@ -46,6 +47,12 @@ var MailerService = class {
|
|
|
46
47
|
defaultFrom;
|
|
47
48
|
templateEngine;
|
|
48
49
|
enabled;
|
|
50
|
+
/** Total messages successfully accepted by the provider. */
|
|
51
|
+
sentCount = 0;
|
|
52
|
+
/** Total messages that threw inside provider.send(). */
|
|
53
|
+
failedCount = 0;
|
|
54
|
+
/** Total messages skipped because `enabled = false` (dry-run mode). */
|
|
55
|
+
dryRunCount = 0;
|
|
49
56
|
constructor(options) {
|
|
50
57
|
this.provider = options.provider;
|
|
51
58
|
this.defaultFrom = options.defaultFrom;
|
|
@@ -60,6 +67,7 @@ var MailerService = class {
|
|
|
60
67
|
const msg = { ...message };
|
|
61
68
|
if (!msg.from && this.defaultFrom) msg.from = this.defaultFrom;
|
|
62
69
|
if (!this.enabled) {
|
|
70
|
+
this.dryRunCount++;
|
|
63
71
|
log$2.info(`[dry-run] → ${formatRecipient(msg.to)} | ${msg.subject}`);
|
|
64
72
|
return {
|
|
65
73
|
messageId: "dry-run",
|
|
@@ -68,9 +76,11 @@ var MailerService = class {
|
|
|
68
76
|
}
|
|
69
77
|
try {
|
|
70
78
|
const result = await this.provider.send(msg);
|
|
79
|
+
this.sentCount++;
|
|
71
80
|
log$2.info(`Sent → ${formatRecipient(msg.to)} | ${msg.subject} [${result.messageId}]`);
|
|
72
81
|
return result;
|
|
73
82
|
} catch (err) {
|
|
83
|
+
this.failedCount++;
|
|
74
84
|
log$2.error({ err }, `Failed → ${formatRecipient(msg.to)} | ${msg.subject}`);
|
|
75
85
|
throw err;
|
|
76
86
|
}
|
|
@@ -116,30 +126,59 @@ const log$1 = Logger.for("MailerAdapter");
|
|
|
116
126
|
*
|
|
117
127
|
* bootstrap({
|
|
118
128
|
* adapters: [
|
|
119
|
-
*
|
|
129
|
+
* MailerAdapter({
|
|
120
130
|
* provider: new SmtpProvider({ host: 'smtp.gmail.com', port: 587, auth: { ... } }),
|
|
121
131
|
* defaultFrom: { name: 'My App', address: 'noreply@myapp.com' },
|
|
122
132
|
* }),
|
|
123
133
|
* ],
|
|
124
134
|
* })
|
|
135
|
+
*
|
|
136
|
+
* // Multiple providers via .scoped() — e.g. transactional + marketing pipelines:
|
|
137
|
+
* bootstrap({
|
|
138
|
+
* adapters: [
|
|
139
|
+
* MailerAdapter.scoped('transactional', { provider: new ResendProvider({ ... }) }),
|
|
140
|
+
* MailerAdapter.scoped('marketing', { provider: new SesProvider({ ... }) }),
|
|
141
|
+
* ],
|
|
142
|
+
* })
|
|
125
143
|
* ```
|
|
126
144
|
*/
|
|
127
|
-
|
|
128
|
-
name
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
const MailerAdapter = defineAdapter({
|
|
146
|
+
name: "MailerAdapter",
|
|
147
|
+
build: (options) => {
|
|
148
|
+
const mailer = new MailerService(options);
|
|
149
|
+
return {
|
|
150
|
+
introspect() {
|
|
151
|
+
return {
|
|
152
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
153
|
+
name: "MailerAdapter",
|
|
154
|
+
kind: "adapter",
|
|
155
|
+
state: {
|
|
156
|
+
provider: options.provider.name,
|
|
157
|
+
enabled: options.enabled !== false,
|
|
158
|
+
hasTemplateEngine: !!options.templateEngine
|
|
159
|
+
},
|
|
160
|
+
tokens: {
|
|
161
|
+
provides: ["kick/mailer/Service"],
|
|
162
|
+
requires: []
|
|
163
|
+
},
|
|
164
|
+
metrics: {
|
|
165
|
+
sent: mailer.sentCount,
|
|
166
|
+
failed: mailer.failedCount,
|
|
167
|
+
dryRun: mailer.dryRunCount
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
beforeStart({ container }) {
|
|
172
|
+
container.registerInstance(MAILER, mailer);
|
|
173
|
+
log$1.info(`Mail provider: ${options.provider.name}${options.enabled === false ? " (disabled)" : ""}`);
|
|
174
|
+
},
|
|
175
|
+
async shutdown() {
|
|
176
|
+
await mailer.shutdown();
|
|
177
|
+
log$1.info("Mailer shut down");
|
|
178
|
+
}
|
|
179
|
+
};
|
|
141
180
|
}
|
|
142
|
-
};
|
|
181
|
+
});
|
|
143
182
|
//#endregion
|
|
144
183
|
//#region src/providers/smtp.provider.ts
|
|
145
184
|
/**
|
|
@@ -164,7 +203,7 @@ var MailerAdapter = class {
|
|
|
164
203
|
* host: 'smtp.resend.com',
|
|
165
204
|
* port: 465,
|
|
166
205
|
* secure: true,
|
|
167
|
-
* auth: { user: 'resend', pass:
|
|
206
|
+
* auth: { user: 'resend', pass: getEnv('RESEND_API_KEY') },
|
|
168
207
|
* })
|
|
169
208
|
*
|
|
170
209
|
* // Mailpit (local dev)
|
|
@@ -237,7 +276,7 @@ let counter = ref(0);
|
|
|
237
276
|
*
|
|
238
277
|
* @example
|
|
239
278
|
* ```ts
|
|
240
|
-
*
|
|
279
|
+
* MailerAdapter({
|
|
241
280
|
* provider: new ConsoleProvider(),
|
|
242
281
|
* defaultFrom: 'dev@localhost',
|
|
243
282
|
* })
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["log","log"],"sources":["../src/mailer.service.ts","../src/adapter.ts","../src/providers/smtp.provider.ts","../src/providers/console.provider.ts"],"sourcesContent":["import { Logger } from '@forinda/kickjs'\nimport type {\n MailProvider,\n MailMessage,\n MailResult,\n MailRecipient,\n MailTemplateEngine,\n MailerOptions,\n} from './types'\n\nconst log = Logger.for('Mailer')\n\n/** DI token for resolving MailerService from the container */\nexport const MAILER = Symbol('MailerService')\n\n/**\n * Central mail service — send emails through any provider.\n *\n * @example\n * ```ts\n * @Service()\n * class UserService {\n * constructor(@Inject(MAILER) private mailer: MailerService) {}\n *\n * async sendWelcome(user: User) {\n * await this.mailer.send({\n * to: user.email,\n * subject: 'Welcome!',\n * html: '<h1>Welcome to our app</h1>',\n * })\n * }\n *\n * // Or with templates:\n * async sendInvoice(user: User, invoice: Invoice) {\n * await this.mailer.sendTemplate('invoice', {\n * to: user.email,\n * subject: `Invoice #${invoice.number}`,\n * }, { user, invoice })\n * }\n * }\n * ```\n */\nexport class MailerService {\n private provider: MailProvider\n private defaultFrom?: MailRecipient\n private templateEngine?: MailTemplateEngine\n private enabled: boolean\n\n constructor(options: MailerOptions) {\n this.provider = options.provider\n this.defaultFrom = options.defaultFrom\n this.templateEngine = options.templateEngine\n this.enabled = options.enabled ?? true\n }\n\n /**\n * Send an email message.\n * Applies defaultFrom if no from address is set.\n */\n async send(message: MailMessage): Promise<MailResult> {\n const msg = { ...message }\n\n // Apply default from\n if (!msg.from && this.defaultFrom) {\n msg.from = this.defaultFrom\n }\n\n if (!this.enabled) {\n log.info(`[dry-run] → ${formatRecipient(msg.to)} | ${msg.subject}`)\n return { messageId: 'dry-run', accepted: true }\n }\n\n try {\n const result = await this.provider.send(msg)\n log.info(`Sent → ${formatRecipient(msg.to)} | ${msg.subject} [${result.messageId}]`)\n return result\n } catch (err: any) {\n log.error({ err }, `Failed → ${formatRecipient(msg.to)} | ${msg.subject}`)\n throw err\n }\n }\n\n /**\n * Render a template and send the resulting HTML as an email.\n * Requires a templateEngine to be configured.\n *\n * @param template - Template name (resolved by the engine)\n * @param message - Mail message (html will be overwritten by the rendered template)\n * @param data - Template variables\n */\n async sendTemplate(\n template: string,\n message: Omit<MailMessage, 'html'>,\n data: Record<string, any>,\n ): Promise<MailResult> {\n if (!this.templateEngine) {\n throw new Error(\n 'MailerService: templateEngine is required for sendTemplate(). ' +\n 'Pass one in MailerOptions or use send() with raw HTML.',\n )\n }\n\n const html = await this.templateEngine.render(template, data)\n return this.send({ ...message, html })\n }\n\n /** Get the underlying provider (for advanced use) */\n getProvider(): MailProvider {\n return this.provider\n }\n\n /** Shutdown the provider */\n async shutdown(): Promise<void> {\n if (this.provider.shutdown) {\n await this.provider.shutdown()\n }\n }\n}\n\nfunction formatRecipient(to: MailRecipient | MailRecipient[]): string {\n if (Array.isArray(to)) {\n return to.map((r) => (typeof r === 'string' ? r : r.address)).join(', ')\n }\n return typeof to === 'string' ? to : to.address\n}\n","import { Logger, type AppAdapter, type AdapterContext } from '@forinda/kickjs'\nimport { MailerService, MAILER } from './mailer.service'\nimport type { MailerOptions } from './types'\n\nconst log = Logger.for('MailerAdapter')\n\n/**\n * Mailer adapter — registers MailerService in the DI container.\n *\n * @example\n * ```ts\n * import { MailerAdapter, SmtpProvider } from '@forinda/kickjs-mailer'\n *\n * bootstrap({\n * adapters: [\n * new MailerAdapter({\n * provider: new SmtpProvider({ host: 'smtp.gmail.com', port: 587, auth: { ... } }),\n * defaultFrom: { name: 'My App', address: 'noreply@myapp.com' },\n * }),\n * ],\n * })\n * ```\n */\nexport class MailerAdapter implements AppAdapter {\n name = 'MailerAdapter'\n private readonly mailer: MailerService\n\n constructor(private readonly options: MailerOptions) {\n this.mailer = new MailerService(options)\n }\n\n beforeStart({ container }: AdapterContext): void {\n container.registerInstance(MAILER, this.mailer)\n log.info(\n `Mail provider: ${this.options.provider.name}${this.options.enabled === false ? ' (disabled)' : ''}`,\n )\n }\n\n async shutdown(): Promise<void> {\n await this.mailer.shutdown()\n log.info('Mailer shut down')\n }\n}\n","import type { MailProvider, MailMessage, MailResult } from '../types'\n\nexport interface SmtpOptions {\n /** SMTP host (e.g. 'smtp.gmail.com', 'smtp.resend.com') */\n host: string\n /** SMTP port (default: 587) */\n port?: number\n /** Use TLS (default: true for port 465, false otherwise) */\n secure?: boolean\n /** Authentication credentials */\n auth?: {\n user: string\n pass: string\n }\n /** Connection timeout in ms (default: 10000) */\n connectionTimeout?: number\n}\n\n/**\n * SMTP mail provider using nodemailer.\n *\n * Requires `nodemailer` as a peer dependency:\n * ```bash\n * pnpm add nodemailer @types/nodemailer\n * ```\n *\n * @example\n * ```ts\n * // Gmail\n * new SmtpProvider({\n * host: 'smtp.gmail.com',\n * port: 587,\n * auth: { user: 'you@gmail.com', pass: 'app-password' },\n * })\n *\n * // Resend via SMTP\n * new SmtpProvider({\n * host: 'smtp.resend.com',\n * port: 465,\n * secure: true,\n * auth: { user: 'resend', pass: process.env.RESEND_API_KEY! },\n * })\n *\n * // Mailpit (local dev)\n * new SmtpProvider({ host: 'localhost', port: 1025 })\n * ```\n */\nexport class SmtpProvider implements MailProvider {\n name = 'smtp'\n private transporter: any\n\n constructor(private options: SmtpOptions) {}\n\n private async ensureTransporter(): Promise<void> {\n if (this.transporter) return\n try {\n const nodemailer: any = await import('nodemailer')\n const createTransport = nodemailer.createTransport ?? nodemailer.default?.createTransport\n this.transporter = createTransport({\n host: this.options.host,\n port: this.options.port ?? 587,\n secure: this.options.secure ?? this.options.port === 465,\n auth: this.options.auth,\n connectionTimeout: this.options.connectionTimeout ?? 10000,\n })\n } catch {\n throw new Error('SmtpProvider requires \"nodemailer\" package. Install: pnpm add nodemailer')\n }\n }\n\n async send(message: MailMessage): Promise<MailResult> {\n await this.ensureTransporter()\n\n const result = await this.transporter.sendMail({\n from: formatAddress(message.from),\n to: formatRecipients(message.to),\n cc: message.cc ? formatRecipients(message.cc) : undefined,\n bcc: message.bcc ? formatRecipients(message.bcc) : undefined,\n replyTo: message.replyTo ? formatAddress(message.replyTo) : undefined,\n subject: message.subject,\n text: message.text,\n html: message.html,\n attachments: message.attachments,\n headers: message.headers,\n })\n\n return {\n messageId: result.messageId,\n accepted: (result.accepted?.length ?? 0) > 0,\n raw: result,\n }\n }\n\n async shutdown(): Promise<void> {\n if (this.transporter) {\n this.transporter.close()\n }\n }\n}\n\nfunction formatAddress(addr: any): string | undefined {\n if (!addr) return undefined\n if (typeof addr === 'string') return addr\n return addr.name ? `\"${addr.name}\" <${addr.address}>` : addr.address\n}\n\nfunction formatRecipients(recipients: any): string {\n if (!recipients) return ''\n if (typeof recipients === 'string') return recipients\n if (Array.isArray(recipients)) {\n return recipients.map((r: any) => formatAddress(r)).join(', ')\n }\n return formatAddress(recipients) ?? ''\n}\n","import { Logger, ref } from '@forinda/kickjs'\nimport type { MailProvider, MailMessage, MailResult } from '../types'\n\nconst log = Logger.for('ConsoleMail')\n\nlet counter = ref(0)\n\n/**\n * Console mail provider — logs emails instead of sending them.\n * Perfect for development and testing.\n *\n * @example\n * ```ts\n * new MailerAdapter({\n * provider: new ConsoleProvider(),\n * defaultFrom: 'dev@localhost',\n * })\n * ```\n */\nexport class ConsoleProvider implements MailProvider {\n name = 'console'\n\n async send(message: MailMessage): Promise<MailResult> {\n const id = `console-${++counter.value}`\n const to = Array.isArray(message.to)\n ? message.to.map((r) => (typeof r === 'string' ? r : r.address)).join(', ')\n : typeof message.to === 'string'\n ? message.to\n : message.to.address\n\n log.info(`────────────────────────────────────────`)\n log.info(`From: ${formatAddr(message.from)}`)\n log.info(`To: ${to}`)\n if (message.cc) log.info(`CC: ${formatAddr(message.cc)}`)\n if (message.bcc) log.info(`BCC: ${formatAddr(message.bcc)}`)\n log.info(`Subject: ${message.subject}`)\n if (message.text)\n log.info(`Text: ${message.text.slice(0, 200)}${message.text.length > 200 ? '...' : ''}`)\n if (message.html)\n log.info(`HTML: ${message.html.slice(0, 200)}${message.html.length > 200 ? '...' : ''}`)\n if (message.attachments?.length) {\n log.info(`Attach: ${message.attachments.map((a) => a.filename).join(', ')}`)\n }\n log.info(`ID: ${id}`)\n log.info(`────────────────────────────────────────`)\n\n return { messageId: id, accepted: true }\n }\n}\n\nfunction formatAddr(addr: any): string {\n if (!addr) return '(none)'\n if (typeof addr === 'string') return addr\n if (Array.isArray(addr)) return addr.map(formatAddr).join(', ')\n return addr.name ? `\"${addr.name}\" <${addr.address}>` : addr.address\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["log","log"],"sources":["../src/mailer.service.ts","../src/adapter.ts","../src/providers/smtp.provider.ts","../src/providers/console.provider.ts"],"sourcesContent":["import { Logger, createToken } from '@forinda/kickjs'\nimport type {\n MailProvider,\n MailMessage,\n MailResult,\n MailRecipient,\n MailTemplateEngine,\n MailerOptions,\n} from './types'\n\nconst log = Logger.for('Mailer')\n\n/** DI token for resolving MailerService from the container. */\nexport const MAILER = createToken<MailerService>('kick/mailer/Service')\n\n/**\n * Central mail service — send emails through any provider.\n *\n * @example\n * ```ts\n * @Service()\n * class UserService {\n * constructor(@Inject(MAILER) private mailer: MailerService) {}\n *\n * async sendWelcome(user: User) {\n * await this.mailer.send({\n * to: user.email,\n * subject: 'Welcome!',\n * html: '<h1>Welcome to our app</h1>',\n * })\n * }\n *\n * // Or with templates:\n * async sendInvoice(user: User, invoice: Invoice) {\n * await this.mailer.sendTemplate('invoice', {\n * to: user.email,\n * subject: `Invoice #${invoice.number}`,\n * }, { user, invoice })\n * }\n * }\n * ```\n */\nexport class MailerService {\n private readonly provider: MailProvider\n private readonly defaultFrom?: MailRecipient\n private readonly templateEngine?: MailTemplateEngine\n private readonly enabled: boolean\n\n // Counters surfaced via the MailerAdapter's introspect() to DevTools\n // (architecture.md §23). Public + readonly externally so the adapter\n // wrapper reads them without coupling to internal field layout.\n /** Total messages successfully accepted by the provider. */\n public sentCount = 0\n /** Total messages that threw inside provider.send(). */\n public failedCount = 0\n /** Total messages skipped because `enabled = false` (dry-run mode). */\n public dryRunCount = 0\n\n constructor(options: MailerOptions) {\n this.provider = options.provider\n this.defaultFrom = options.defaultFrom\n this.templateEngine = options.templateEngine\n this.enabled = options.enabled ?? true\n }\n\n /**\n * Send an email message.\n * Applies defaultFrom if no from address is set.\n */\n async send(message: MailMessage): Promise<MailResult> {\n const msg = { ...message }\n\n // Apply default from\n if (!msg.from && this.defaultFrom) {\n msg.from = this.defaultFrom\n }\n\n if (!this.enabled) {\n this.dryRunCount++\n log.info(`[dry-run] → ${formatRecipient(msg.to)} | ${msg.subject}`)\n return { messageId: 'dry-run', accepted: true }\n }\n\n try {\n const result = await this.provider.send(msg)\n this.sentCount++\n log.info(`Sent → ${formatRecipient(msg.to)} | ${msg.subject} [${result.messageId}]`)\n return result\n } catch (err: any) {\n this.failedCount++\n log.error({ err }, `Failed → ${formatRecipient(msg.to)} | ${msg.subject}`)\n throw err\n }\n }\n\n /**\n * Render a template and send the resulting HTML as an email.\n * Requires a templateEngine to be configured.\n *\n * @param template - Template name (resolved by the engine)\n * @param message - Mail message (html will be overwritten by the rendered template)\n * @param data - Template variables\n */\n async sendTemplate(\n template: string,\n message: Omit<MailMessage, 'html'>,\n data: Record<string, any>,\n ): Promise<MailResult> {\n if (!this.templateEngine) {\n throw new Error(\n 'MailerService: templateEngine is required for sendTemplate(). ' +\n 'Pass one in MailerOptions or use send() with raw HTML.',\n )\n }\n\n const html = await this.templateEngine.render(template, data)\n return this.send({ ...message, html })\n }\n\n /** Get the underlying provider (for advanced use) */\n getProvider(): MailProvider {\n return this.provider\n }\n\n /** Shutdown the provider */\n async shutdown(): Promise<void> {\n if (this.provider.shutdown) {\n await this.provider.shutdown()\n }\n }\n}\n\nfunction formatRecipient(to: MailRecipient | MailRecipient[]): string {\n if (Array.isArray(to)) {\n return to.map((r) => (typeof r === 'string' ? r : r.address)).join(', ')\n }\n return typeof to === 'string' ? to : to.address\n}\n","import { Logger, defineAdapter } from '@forinda/kickjs'\nimport { PROTOCOL_VERSION, type IntrospectionSnapshot } from '@forinda/kickjs-devtools-kit'\nimport { MailerService, MAILER } from './mailer.service'\nimport type { MailerOptions } from './types'\n\nconst log = Logger.for('MailerAdapter')\n\n/**\n * Mailer adapter — registers MailerService in the DI container.\n *\n * @example\n * ```ts\n * import { MailerAdapter, SmtpProvider } from '@forinda/kickjs-mailer'\n *\n * bootstrap({\n * adapters: [\n * MailerAdapter({\n * provider: new SmtpProvider({ host: 'smtp.gmail.com', port: 587, auth: { ... } }),\n * defaultFrom: { name: 'My App', address: 'noreply@myapp.com' },\n * }),\n * ],\n * })\n *\n * // Multiple providers via .scoped() — e.g. transactional + marketing pipelines:\n * bootstrap({\n * adapters: [\n * MailerAdapter.scoped('transactional', { provider: new ResendProvider({ ... }) }),\n * MailerAdapter.scoped('marketing', { provider: new SesProvider({ ... }) }),\n * ],\n * })\n * ```\n */\nexport const MailerAdapter = defineAdapter<MailerOptions>({\n name: 'MailerAdapter',\n build: (options) => {\n const mailer = new MailerService(options)\n\n return {\n // ── DevTools introspection (architecture.md §23) ───────────────\n introspect(): IntrospectionSnapshot {\n return {\n protocolVersion: PROTOCOL_VERSION,\n name: 'MailerAdapter',\n kind: 'adapter',\n state: {\n provider: options.provider.name,\n enabled: options.enabled !== false,\n hasTemplateEngine: !!options.templateEngine,\n },\n tokens: { provides: ['kick/mailer/Service'], requires: [] },\n metrics: {\n sent: mailer.sentCount,\n failed: mailer.failedCount,\n dryRun: mailer.dryRunCount,\n },\n }\n },\n\n beforeStart({ container }) {\n container.registerInstance(MAILER, mailer)\n log.info(\n `Mail provider: ${options.provider.name}${options.enabled === false ? ' (disabled)' : ''}`,\n )\n },\n\n async shutdown() {\n await mailer.shutdown()\n log.info('Mailer shut down')\n },\n }\n },\n})\n","import type { MailProvider, MailMessage, MailResult } from '../types'\n\nexport interface SmtpOptions {\n /** SMTP host (e.g. 'smtp.gmail.com', 'smtp.resend.com') */\n host: string\n /** SMTP port (default: 587) */\n port?: number\n /** Use TLS (default: true for port 465, false otherwise) */\n secure?: boolean\n /** Authentication credentials */\n auth?: {\n user: string\n pass: string\n }\n /** Connection timeout in ms (default: 10000) */\n connectionTimeout?: number\n}\n\n/**\n * SMTP mail provider using nodemailer.\n *\n * Requires `nodemailer` as a peer dependency:\n * ```bash\n * pnpm add nodemailer @types/nodemailer\n * ```\n *\n * @example\n * ```ts\n * // Gmail\n * new SmtpProvider({\n * host: 'smtp.gmail.com',\n * port: 587,\n * auth: { user: 'you@gmail.com', pass: 'app-password' },\n * })\n *\n * // Resend via SMTP\n * new SmtpProvider({\n * host: 'smtp.resend.com',\n * port: 465,\n * secure: true,\n * auth: { user: 'resend', pass: getEnv('RESEND_API_KEY') },\n * })\n *\n * // Mailpit (local dev)\n * new SmtpProvider({ host: 'localhost', port: 1025 })\n * ```\n */\nexport class SmtpProvider implements MailProvider {\n name = 'smtp'\n private transporter: any\n\n constructor(private options: SmtpOptions) {}\n\n private async ensureTransporter(): Promise<void> {\n if (this.transporter) return\n try {\n const nodemailer: any = await import('nodemailer')\n const createTransport = nodemailer.createTransport ?? nodemailer.default?.createTransport\n this.transporter = createTransport({\n host: this.options.host,\n port: this.options.port ?? 587,\n secure: this.options.secure ?? this.options.port === 465,\n auth: this.options.auth,\n connectionTimeout: this.options.connectionTimeout ?? 10000,\n })\n } catch {\n throw new Error('SmtpProvider requires \"nodemailer\" package. Install: pnpm add nodemailer')\n }\n }\n\n async send(message: MailMessage): Promise<MailResult> {\n await this.ensureTransporter()\n\n const result = await this.transporter.sendMail({\n from: formatAddress(message.from),\n to: formatRecipients(message.to),\n cc: message.cc ? formatRecipients(message.cc) : undefined,\n bcc: message.bcc ? formatRecipients(message.bcc) : undefined,\n replyTo: message.replyTo ? formatAddress(message.replyTo) : undefined,\n subject: message.subject,\n text: message.text,\n html: message.html,\n attachments: message.attachments,\n headers: message.headers,\n })\n\n return {\n messageId: result.messageId,\n accepted: (result.accepted?.length ?? 0) > 0,\n raw: result,\n }\n }\n\n async shutdown(): Promise<void> {\n if (this.transporter) {\n this.transporter.close()\n }\n }\n}\n\nfunction formatAddress(addr: any): string | undefined {\n if (!addr) return undefined\n if (typeof addr === 'string') return addr\n return addr.name ? `\"${addr.name}\" <${addr.address}>` : addr.address\n}\n\nfunction formatRecipients(recipients: any): string {\n if (!recipients) return ''\n if (typeof recipients === 'string') return recipients\n if (Array.isArray(recipients)) {\n return recipients.map((r: any) => formatAddress(r)).join(', ')\n }\n return formatAddress(recipients) ?? ''\n}\n","import { Logger, ref } from '@forinda/kickjs'\nimport type { MailProvider, MailMessage, MailResult } from '../types'\n\nconst log = Logger.for('ConsoleMail')\n\nlet counter = ref(0)\n\n/**\n * Console mail provider — logs emails instead of sending them.\n * Perfect for development and testing.\n *\n * @example\n * ```ts\n * MailerAdapter({\n * provider: new ConsoleProvider(),\n * defaultFrom: 'dev@localhost',\n * })\n * ```\n */\nexport class ConsoleProvider implements MailProvider {\n name = 'console'\n\n async send(message: MailMessage): Promise<MailResult> {\n const id = `console-${++counter.value}`\n const to = Array.isArray(message.to)\n ? message.to.map((r) => (typeof r === 'string' ? r : r.address)).join(', ')\n : typeof message.to === 'string'\n ? message.to\n : message.to.address\n\n log.info(`────────────────────────────────────────`)\n log.info(`From: ${formatAddr(message.from)}`)\n log.info(`To: ${to}`)\n if (message.cc) log.info(`CC: ${formatAddr(message.cc)}`)\n if (message.bcc) log.info(`BCC: ${formatAddr(message.bcc)}`)\n log.info(`Subject: ${message.subject}`)\n if (message.text)\n log.info(`Text: ${message.text.slice(0, 200)}${message.text.length > 200 ? '...' : ''}`)\n if (message.html)\n log.info(`HTML: ${message.html.slice(0, 200)}${message.html.length > 200 ? '...' : ''}`)\n if (message.attachments?.length) {\n log.info(`Attach: ${message.attachments.map((a) => a.filename).join(', ')}`)\n }\n log.info(`ID: ${id}`)\n log.info(`────────────────────────────────────────`)\n\n return { messageId: id, accepted: true }\n }\n}\n\nfunction formatAddr(addr: any): string {\n if (!addr) return '(none)'\n if (typeof addr === 'string') return addr\n if (Array.isArray(addr)) return addr.map(formatAddr).join(', ')\n return addr.name ? `\"${addr.name}\" <${addr.address}>` : addr.address\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forinda/kickjs-mailer",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Pluggable email sending for KickJS — nodemailer, Resend, SES, and custom providers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kickjs",
|
|
@@ -23,13 +23,10 @@
|
|
|
23
23
|
"@forinda/kickjs",
|
|
24
24
|
"@forinda/kickjs-auth",
|
|
25
25
|
"@forinda/kickjs-cli",
|
|
26
|
-
"@forinda/kickjs-config",
|
|
27
|
-
"@forinda/kickjs-core",
|
|
28
26
|
"@forinda/kickjs-cron",
|
|
29
27
|
"@forinda/kickjs-devtools",
|
|
30
28
|
"@forinda/kickjs-drizzle",
|
|
31
29
|
"@forinda/kickjs-graphql",
|
|
32
|
-
"@forinda/kickjs-http",
|
|
33
30
|
"@forinda/kickjs-mailer",
|
|
34
31
|
"@forinda/kickjs-multi-tenant",
|
|
35
32
|
"@forinda/kickjs-notifications",
|
|
@@ -65,13 +62,12 @@
|
|
|
65
62
|
"output": [
|
|
66
63
|
"dist/**"
|
|
67
64
|
],
|
|
68
|
-
"dependencies": [
|
|
69
|
-
"../core:build"
|
|
70
|
-
]
|
|
65
|
+
"dependencies": []
|
|
71
66
|
}
|
|
72
67
|
},
|
|
73
68
|
"dependencies": {
|
|
74
|
-
"reflect-metadata": "^0.2.2"
|
|
69
|
+
"reflect-metadata": "^0.2.2",
|
|
70
|
+
"@forinda/kickjs-devtools-kit": "4.0.0"
|
|
75
71
|
},
|
|
76
72
|
"peerDependencies": {
|
|
77
73
|
"nodemailer": ">=7.0.11",
|
|
@@ -83,12 +79,12 @@
|
|
|
83
79
|
}
|
|
84
80
|
},
|
|
85
81
|
"devDependencies": {
|
|
86
|
-
"@types/node": "^25.
|
|
82
|
+
"@types/node": "^25.6.0",
|
|
87
83
|
"@types/nodemailer": "^7.0.11",
|
|
88
84
|
"nodemailer": "^8.0.4",
|
|
89
|
-
"typescript": "^
|
|
90
|
-
"vitest": "^4.1.
|
|
91
|
-
"@forinda/kickjs": "
|
|
85
|
+
"typescript": "^6.0.3",
|
|
86
|
+
"vitest": "^4.1.5",
|
|
87
|
+
"@forinda/kickjs": "4.0.0"
|
|
92
88
|
},
|
|
93
89
|
"publishConfig": {
|
|
94
90
|
"access": "public"
|