@nxgt/mail-presets 0.1.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.
@@ -0,0 +1,365 @@
1
+ # Wiring the presets
2
+
3
+ This page is for adding the presets to a project: what `presets()` answers,
4
+ how `@nxgt/mail-i18n` builds it beside your own templates, and how your
5
+ project replaces a template, overrides a message, or adds a locale.
6
+
7
+ ```ts
8
+ // maizzle.config.ts
9
+ import { defineMailConfig } from '@nxgt/mail-config';
10
+ import { i18n } from '@nxgt/mail-i18n';
11
+ import { presets } from '@nxgt/mail-presets';
12
+ import { ui, uiCatalogues } from '@nxgt/mail-ui';
13
+
14
+ const mails = presets({ only: ['verify-email', 'reset-password'] });
15
+
16
+ export default defineMailConfig({
17
+ plugins: [
18
+ ui({ brand: { name: 'Acme', url: 'https://acme.example' } }),
19
+ i18n({
20
+ locales: ['en', 'fr'],
21
+ catalogues: [uiCatalogues, mails.catalogues],
22
+ templates: [mails.templates],
23
+ }),
24
+ ],
25
+ });
26
+ ```
27
+
28
+ ```json
29
+ // locales/en.json, and the same in locales/fr.json — nothing to override yet
30
+ {}
31
+ ```
32
+
33
+ `maizzle build` writes:
34
+
35
+ ```text
36
+ dist/
37
+ en/reset-password.html en/reset-password.txt
38
+ en/verify-email.html en/verify-email.txt
39
+ fr/reset-password.html fr/reset-password.txt
40
+ fr/verify-email.html fr/verify-email.txt
41
+ mail-manifest.json
42
+ ```
43
+
44
+ The header and the footer show `Acme`, the button is in `ui()`'s theme, and
45
+ `{{ name }}` and `{{ link }}` wait for the values the sender fills.
46
+
47
+ ## The signature
48
+
49
+ ```ts
50
+ import type { Catalogues, TemplateSource } from '@nxgt/mail-i18n';
51
+
52
+ const PRESETS: readonly [
53
+ 'verify-email',
54
+ 'reset-password',
55
+ 'password-changed',
56
+ 'email-changed',
57
+ 'sign-in-code',
58
+ 'magic-link',
59
+ 'new-sign-in',
60
+ 'welcome',
61
+ 'invitation',
62
+ ];
63
+
64
+ type PresetName = (typeof PRESETS)[number];
65
+
66
+ interface PresetsOptions {
67
+ /** The presets to build, at least one. Default every one. */
68
+ readonly only?: readonly [PresetName, ...PresetName[]];
69
+ }
70
+
71
+ interface Presets {
72
+ /** For i18n({ templates }). */
73
+ readonly templates: TemplateSource;
74
+ /** For i18n({ catalogues }): the messages of the presets kept, and the shared presets.*. */
75
+ readonly catalogues: Catalogues;
76
+ }
77
+
78
+ function presets(options?: PresetsOptions): Presets;
79
+ ```
80
+
81
+ `presets()` reads nothing and writes nothing: it answers where the templates
82
+ are and which messages go with them. The build is `@nxgt/mail-i18n`'s, with
83
+ your `ui()` — your brand, your theme, your components.
84
+
85
+ ## Options
86
+
87
+ | Option | Type | Default | Effect |
88
+ | --- | --- | --- | --- |
89
+ | `only` | `readonly [PresetName, ...PresetName[]]` | every preset | The presets to build, at least one. The others are neither built nor in the manifest, and their messages are left out of `catalogues` |
90
+
91
+ `only: []` does not compile: leave `only` out for every preset. The options
92
+ are an object even for one preset — `presets(['welcome'])` does not compile,
93
+ and throws when the config loads.
94
+
95
+ ## What it answers
96
+
97
+ With no option, the whole folder and every message:
98
+
99
+ ```ts
100
+ import { presetCatalogues, presets, TEMPLATES_DIR } from '@nxgt/mail-presets';
101
+
102
+ const all = presets();
103
+ // all.templates → { dir: TEMPLATES_DIR }
104
+ // all.catalogues → presetCatalogues, { en: {…}, fr: {…} }
105
+ ```
106
+
107
+ `presetCatalogues` is frozen, every group in it: read it, never change it.
108
+ Your `locales/<locale>.json` is where a message changes — see
109
+ [Overriding a message](#overriding-a-message).
110
+
111
+ ```ts
112
+ import { presetCatalogues } from '@nxgt/mail-presets';
113
+
114
+ Object.isFrozen(presetCatalogues.en?.verifyEmail); // true — and its type is readonly
115
+ ```
116
+
117
+ With `only`, the folder with the e-mails to keep, and only their messages
118
+ with the shared `presets.*` group:
119
+
120
+ ```ts
121
+ const one = presets({ only: ['sign-in-code'] });
122
+ // one.templates → { dir: TEMPLATES_DIR, emails: ['sign-in-code'] }
123
+ // Object.keys(one.catalogues.en) → ['presets', 'signInCode']
124
+ ```
125
+
126
+ | Field | What `i18n()` does with it |
127
+ | --- | --- |
128
+ | `templates.dir` | `TEMPLATES_DIR`, the absolute path of the package's `emails/`. Its templates are built once per locale, like yours; a name your own `emails/` also has is taken from yours |
129
+ | `templates.emails` | Present with `only`: the templates of the folder to build, and no other |
130
+ | `catalogues` | Merged under your `locales/<locale>.json`, key by key, in the order `catalogues` lists it |
131
+
132
+ Pass `mails.templates` whole: `templates: [mails.templates.dir]` does not
133
+ compile, and `i18n()` would refuse it with a `TypeError` if it did.
134
+
135
+ The rest of the exports serve a project that wants to look inside:
136
+
137
+ ```ts
138
+ import { PRESETS, type PresetName, TEMPLATES_DIR } from '@nxgt/mail-presets';
139
+
140
+ const wanted: PresetName[] = PRESETS.filter((name) => name !== 'invitation');
141
+ const source = `${TEMPLATES_DIR}/verify-email.vue`; // to copy it into your emails/
142
+ ```
143
+
144
+ ## Your templates beside them
145
+
146
+ Your `emails/` is built as before. Its templates and the presets end up in
147
+ the same `dist/` and the same manifest:
148
+
149
+ ```vue
150
+ <!-- emails/order-shipped.vue -->
151
+ <template>
152
+ <NxLayout>
153
+ <NxTypography>{{ t('orderShipped.body', { order: placeholder('order') }) }}</NxTypography>
154
+ </NxLayout>
155
+ </template>
156
+ ```
157
+
158
+ ```json
159
+ // locales/en.json
160
+ { "orderShipped": { "subject": "Order {order} is on its way", "body": "Order {order} has shipped." } }
161
+ ```
162
+
163
+ The presets' messages are under their own keys (`verifyEmail.*`,
164
+ `presets.*`), so yours do not collide with them unless you mean them to.
165
+
166
+ ## Replacing a template
167
+
168
+ A template in your `emails/` with the name of a preset is built instead of
169
+ the package's. Your folder is read first, and a name found there is never
170
+ taken from the package:
171
+
172
+ ```vue
173
+ <!-- emails/welcome.vue -->
174
+ <template>
175
+ <NxLayout :preheader="t('welcome.preheader')">
176
+ <NxTypography variant="headline-small">{{ t('welcome.title', { brand: brand.name }) }}</NxTypography>
177
+ <NxTypography>{{ t('common.greeting', { name: placeholder('name') }) }}</NxTypography>
178
+ <NxTypography>{{ t('welcome.body') }}</NxTypography>
179
+ <NxButton :href="placeholder('link')">{{ t('welcome.action') }}</NxButton>
180
+ <NxTypography variant="caption">{{ t('welcome.help') }}</NxTypography>
181
+ </NxLayout>
182
+ </template>
183
+ ```
184
+
185
+ ```json
186
+ // locales/en.json
187
+ { "welcome": { "help": "Questions? Reply to this e-mail." } }
188
+ ```
189
+
190
+ ```json
191
+ // locales/fr.json
192
+ { "welcome": { "help": "Une question ? Répondez à cet e-mail." } }
193
+ ```
194
+
195
+ It uses the preset's `welcome.*` messages and adds one. Its placeholders are
196
+ whatever it writes: the manifest records yours, not the package's. To start
197
+ from the package's template, copy `${TEMPLATES_DIR}/welcome.vue`.
198
+
199
+ A replaced preset still needs its messages when your template calls them:
200
+ keep it in `only` (or leave `only` out), or write every key it uses in your
201
+ catalogues.
202
+
203
+ ## Overriding a message
204
+
205
+ Your `locales/<locale>.json` goes over the package's messages, key by key, in
206
+ each locale:
207
+
208
+ ```json
209
+ // locales/en.json
210
+ { "verifyEmail": { "action": "Yes, this is my address" } }
211
+ ```
212
+
213
+ ```json
214
+ // locales/fr.json — no override: the French button stays "Confirmer mon adresse"
215
+ {}
216
+ ```
217
+
218
+ The English button says `Yes, this is my address`; every other message,
219
+ English or French, stays the package's. An override keeps the arguments
220
+ the template passes: in `en`, the fallback locale, `verifyEmail.body` keeps
221
+ `{brand}`, or the build fails with
222
+ `i18n: en: verify-email passes {brand} to verifyEmail.body, which does not use it`.
223
+ In `fr` it may leave `{brand}` out, and never use an argument `en` does not
224
+ declare. To change what a template passes, replace the template. The keys of
225
+ every preset are in [The e-mails](emails.md), and the merge rules in
226
+ `@nxgt/mail-i18n`'s
227
+ [Catalogues](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-i18n/docs/guide/catalogues.md#catalogues-from-a-package).
228
+
229
+ ## `ui()` and `uiCatalogues` are required
230
+
231
+ The templates are made of `@nxgt/mail-ui`'s components (`<NxLayout>`,
232
+ `<NxButton>`, `<NxCode>`…) and call its shared messages: `common.greeting`,
233
+ `common.footer.ignore`, and `common.footer.why`, which `<NxLayout>` writes in
234
+ every footer. So:
235
+
236
+ - `ui({ brand })` is in `plugins`, or no component renders. The templates
237
+ are installed under `node_modules`, where Maizzle resolves no tag: `ui()`
238
+ resolves them — your `components/` first, then its `Nx*` components, then
239
+ Maizzle's built-ins (see its
240
+ [Components from a package](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-ui/docs/guide/plugin.md#components-from-a-package)).
241
+ Without it, every preset renders empty and the build fails:
242
+
243
+ ```text
244
+ Error: i18n: en/verify-email.html is empty — a tag of its template resolved to no component; list the plugin that brings it, as ui()
245
+ ```
246
+
247
+ - `uiCatalogues` is in `catalogues`, or your catalogues hold the `common`
248
+ keys, or the build fails:
249
+
250
+ ```text
251
+ Error: i18n: en: verify-email calls t('common.footer.why'), which is not a key of the catalogues
252
+ ```
253
+
254
+ Without `mails.catalogues`, it fails on the first preset message instead:
255
+
256
+ ```text
257
+ Error: i18n: en: verify-email calls t('verifyEmail.preheader'), which is not a key of the catalogues
258
+ ```
259
+
260
+ ## Another locale
261
+
262
+ The presets are written in `en` and `fr`. A project that builds another
263
+ locale writes, in its catalogue for it, every key the fallback locale has:
264
+ the three `common` keys, the two `presets` keys, and the group of each preset
265
+ it builds. Otherwise the build fails on the first one missing:
266
+
267
+ ```text
268
+ Error: i18n: de: presets.linkFallback is missing — en, the fallback locale, has it
269
+ ```
270
+
271
+ With `presets({ only: ['sign-in-code'] })` and `locales: ['en', 'fr', 'de']`:
272
+
273
+ ```json
274
+ // locales/de.json
275
+ {
276
+ "common": {
277
+ "greeting": "Hallo {name},",
278
+ "footer": {
279
+ "why": "Sie erhalten diese E-Mail, weil Sie ein Konto bei {brand} haben.",
280
+ "ignore": "Wenn Sie dies nicht angefordert haben, können Sie diese E-Mail ignorieren."
281
+ }
282
+ },
283
+ "presets": {
284
+ "linkFallback": "Wenn die Schaltfläche nicht funktioniert, öffnen Sie diesen Link:",
285
+ "notYou": "Wenn Sie das nicht waren, sichern Sie jetzt Ihr Konto."
286
+ },
287
+ "signInCode": {
288
+ "subject": "Ihr Anmeldecode: {code}",
289
+ "preheader": "Geben Sie diesen Code ein, um sich anzumelden.",
290
+ "title": "Ihr Anmeldecode",
291
+ "body": "Geben Sie diesen Code ein, um sich bei {brand} anzumelden. Er gilt einmal.",
292
+ "ignore": "Wenn Sie sich nicht anmelden wollten, können Sie diese E-Mail ignorieren."
293
+ }
294
+ }
295
+ ```
296
+
297
+ The manifest then has `"de": "Ihr Anmeldecode: {{ code }}"` for its subject.
298
+ `presetCatalogues.en` is the list of keys to translate.
299
+
300
+ ## Checking what your build sends
301
+
302
+ The manifest is the contract between the build and the code that sends. A
303
+ spec in your project keeps it from drifting when you replace a template or
304
+ upgrade the package:
305
+
306
+ ```ts
307
+ // mail.spec.ts
308
+ import { expect, test } from 'bun:test';
309
+ import type { Manifest } from '@nxgt/mail-i18n';
310
+ import built from './dist/mail-manifest.json';
311
+
312
+ const manifest = built as Manifest;
313
+
314
+ test('the verification e-mail takes a name and a link', () => {
315
+ expect(manifest.emails['verify-email']?.variables).toEqual(['link', 'name']);
316
+ expect(manifest.emails['verify-email']?.urlVariables).toEqual(['link']);
317
+ });
318
+
319
+ test('the verification e-mail is built in every locale', () => {
320
+ expect(Object.keys(manifest.emails['verify-email']?.files ?? {})).toEqual(['en', 'fr']);
321
+ });
322
+ ```
323
+
324
+ Run it after `maizzle build`. Each preset's placeholders are in
325
+ [The e-mails](emails.md).
326
+
327
+ ## Errors
328
+
329
+ `presets()` throws a bare `TypeError` when `maizzle.config.ts` loads:
330
+
331
+ | Message | Cause |
332
+ | --- | --- |
333
+ | `presets: options must be an object, as { only: ['verify-email'] }` | `presets(null)`, `presets(['welcome'])` — a list rather than `{ only: [...] }` — or anything that is not an object |
334
+ | `presets: only must list at least one preset, as ['verify-email']` | `only: []`, or `only: 'welcome'` |
335
+ | `presets: only holds something that is not a preset — name one of verify-email, reset-password, password-changed, email-changed, sign-in-code, magic-link, new-sign-in, welcome, invitation` | `only: ['sign-in']` |
336
+ | `presets: only holds the same preset twice` | `only: ['welcome', 'welcome']` |
337
+
338
+ ```ts
339
+ presets({ only: [] });
340
+ // TypeError: presets: only must list at least one preset, as ['verify-email']
341
+ ```
342
+
343
+ Four more come from `i18n()`'s `templates` option, when the config loads —
344
+ see `@nxgt/mail-i18n`'s
345
+ [Templates from a package](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-i18n/docs/guide/templates.md#templates-from-a-package):
346
+
347
+ | Message | When |
348
+ | --- | --- |
349
+ | `i18n: templates must be a list of template folders, as [{ dir: '/abs/path/emails' }] — emails, when given, names at least one, each once` | A `TypeError` when the config loads: `templates: mails.templates` without the list, a relative `dir`, or an empty `emails` |
350
+ | `i18n: templates[0] has no template sign-in.vue — name one of its e-mails` | A plain `Error` when the config loads: an `emails` list written by hand, naming a template the folder does not have |
351
+ | `i18n: templates[0] holds no template — is /…/emails the folder of a package's e-mails?` | A plain `Error` when the config loads: a `dir` written by hand that is missing or has no template — use `TEMPLATES_DIR` |
352
+ | `i18n: templates[0] and templates[1] both have welcome.vue — keep one with emails: [...], or write the project's own in its folder` | A plain `Error` when the config loads: another package ships an e-mail of the same name as a preset. Keep one with `only`, or write your own `emails/welcome.vue` |
353
+
354
+ Every other failure is the build's, from the catalogues or the templates, and
355
+ is in `@nxgt/mail-i18n`'s
356
+ [Troubleshooting](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-i18n/docs/troubleshooting.md).
357
+
358
+ ## See also
359
+
360
+ - [The e-mails](emails.md) — each preset's placeholders, subject and messages.
361
+ - `@nxgt/mail-ui`'s
362
+ [plugin](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-ui/docs/guide/plugin.md)
363
+ — the brand and the theme the presets are built with.
364
+ - [Samples](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-presets/samples/README.md)
365
+ — every preset, built.
@@ -0,0 +1,63 @@
1
+ # Roadmap
2
+
3
+ Where `@nxgt/mail-presets` is heading. A direction, not a commitment: there
4
+ are no dates here, and the version something shipped in is the only number.
5
+
6
+ ## Now
7
+
8
+ Nothing between releases.
9
+
10
+ ## Next
11
+
12
+ Nothing yet.
13
+
14
+ ## Later
15
+
16
+ Nothing yet. A request is welcome as an
17
+ [issue](https://github.com/softistx/nxgt-mail/issues).
18
+
19
+ ## Not planned
20
+
21
+ - **E-mails in a brand of ours** — a preset carries no brand and no colours
22
+ of its own: your project builds it with your `ui({ brand, theme })`. The
23
+ samples use the brand `Acme` only to be looked at.
24
+ - **Built HTML to send as is** — `samples/` lives in the repository and is
25
+ not in the package. Sending the samples would send someone else's brand; a
26
+ preset is built by your project, with `maizzle build`.
27
+ - **Options to patch a preset** — a preset is changed the way any template is:
28
+ your template of the same name replaces it, and your catalogue overrides its
29
+ messages. There is no per-preset option to learn.
30
+
31
+ ## Shipped
32
+
33
+ The last ten, newest first, each with the version it came in. Everything
34
+ before is in the [CHANGELOG](../CHANGELOG.md).
35
+
36
+ - **Nine ready e-mails, v0.1.0** — `verify-email`, `reset-password`,
37
+ `password-changed` and `email-changed` for accounts; `sign-in-code` and
38
+ `magic-link` for passwordless sign-in; `new-sign-in` for security; `welcome`
39
+ and `invitation` for the lifecycle. Each is a template of `@nxgt/mail-ui`
40
+ components with its messages in `en` and `fr`, and leaves the values only
41
+ known at send time (`{{ name }}`, `{{ link }}`, …) as placeholders.
42
+ - **Presets for the i18n plugin, v0.1.0** — `presets({ only })` answers
43
+ `{ templates, catalogues }` for `i18n({ templates, catalogues })`, so the
44
+ presets are built by your own Maizzle project, with your
45
+ `ui({ brand, theme })`, in every locale you list. `only` keeps the presets
46
+ you name and their messages; a preset that does not exist, or one named
47
+ twice, is refused.
48
+ - **Overriding a preset, v0.1.0** — a template of the same name in your project's
49
+ `emails/` replaces a preset, and your `locales/<locale>.json` overrides any
50
+ of its messages key by key.
51
+ - **Samples, v0.1.0** — every preset built in `en` and `fr` with the brand `Acme` and
52
+ the default theme, in the repository's
53
+ [`samples/`](https://github.com/softistx/nxgt-mail/tree/develop/packages/mail-presets/samples),
54
+ checked against a fresh build, so you can see an e-mail before installing
55
+ anything.
56
+ - **A renderer that sends them, v0.1.0** — `createMailRenderer` from
57
+ `@nxgt/mail/renderer` renders the presets' build at send time, each value
58
+ filled and escaped; the build spec renders every preset with it.
59
+ - **A starter that uses one, with v0.1.0** — the official Maizzle starter with
60
+ `presets({ only: ['sign-in-code'] })` next to its own e-mail, built,
61
+ rendered in `en` and `fr` and served in CI:
62
+ [`examples/starter`](https://github.com/softistx/nxgt-mail/tree/develop/examples/starter).
63
+ In the repository; its README says how to start your own from npm.