@nxgt/mail-config 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.
- package/LICENSE +21 -0
- package/README.md +239 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +173 -0
- package/dist/index.js.map +11 -0
- package/dist/layer.d.ts +10 -0
- package/dist/layer.d.ts.map +1 -0
- package/docs/README.md +14 -0
- package/docs/guide/config.md +305 -0
- package/docs/guide/plugins.md +280 -0
- package/docs/guide/production.md +168 -0
- package/docs/roadmap.md +76 -0
- package/docs/troubleshooting.md +497 -0
- package/package.json +58 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# The project config
|
|
2
|
+
|
|
3
|
+
This page is for writing a project's `maizzle.config.ts` with
|
|
4
|
+
`defineMailConfig`: which layer wins, how two layers merge, how the build
|
|
5
|
+
events of several plugins run, and what is refused.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// maizzle.config.ts
|
|
9
|
+
import { defineMailConfig } from '@nxgt/mail-config';
|
|
10
|
+
import { alpha, beta } from './plugins';
|
|
11
|
+
|
|
12
|
+
export default defineMailConfig({
|
|
13
|
+
plugins: [alpha, beta],
|
|
14
|
+
vue: { globalProperties: { greeting: 'from the project' } },
|
|
15
|
+
afterTransform: ({ html }) => `${html}<!-- project -->`,
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`maizzle serve` and `maizzle build` load that file as they load any Maizzle
|
|
20
|
+
config, and `defineMailConfig` runs each time the file is loaded, answering a
|
|
21
|
+
plain Maizzle config. That can be more than once: above 50 templates Maizzle 6
|
|
22
|
+
builds in parallel, and each worker loads the file again. Keep the call free
|
|
23
|
+
of side effects. The per-template events (`beforeRender`, `afterRender`,
|
|
24
|
+
`afterTransform`) then run in the workers; `beforeCreate` and `afterBuild` run
|
|
25
|
+
only on the main thread, once per build.
|
|
26
|
+
|
|
27
|
+
## The signature
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import type { MaizzleConfig } from '@maizzle/framework';
|
|
31
|
+
|
|
32
|
+
interface MailPlugin extends MaizzleConfig {
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly plugins?: never; // a plugin cannot bring others
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface MailConfig extends MaizzleConfig {
|
|
38
|
+
readonly plugins?: readonly MailPlugin[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function defineMailConfig(config?: MailConfig): MaizzleConfig;
|
|
42
|
+
|
|
43
|
+
const baseConfig: Readonly<MaizzleConfig>; // { plaintext: true }, frozen
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`config` is your whole Maizzle config plus `plugins`. The answer is a new
|
|
47
|
+
object: neither `config` nor any plugin is changed, and neither `plugins` nor a
|
|
48
|
+
plugin's `name` is passed on to Maizzle.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { defineMailConfig } from '@nxgt/mail-config';
|
|
52
|
+
|
|
53
|
+
defineMailConfig(); // { plaintext: true }
|
|
54
|
+
defineMailConfig({ plugins: [{ name: 'a', root: 'src' }] }); // { plaintext: true, root: 'src' }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## The layers
|
|
58
|
+
|
|
59
|
+
Three layers, lowest first; each key of a later layer wins over the same key
|
|
60
|
+
below it:
|
|
61
|
+
|
|
62
|
+
1. `baseConfig` — `{ plaintext: true }`, a plain-text part next to each HTML
|
|
63
|
+
file. It is frozen: override it in a layer, never by assigning to it. `dist/`, `public/` copied as static files, and CSS inlined and purged
|
|
64
|
+
are already Maizzle's defaults, so the base does not repeat them.
|
|
65
|
+
2. Each plugin, in the order of `plugins`.
|
|
66
|
+
3. Everything in `config` but `plugins` — your project.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { defineMailConfig } from '@nxgt/mail-config';
|
|
70
|
+
|
|
71
|
+
const config = defineMailConfig({
|
|
72
|
+
plugins: [
|
|
73
|
+
{ name: 'a', output: { path: 'a', extension: 'htm' }, css: { purge: false } },
|
|
74
|
+
{ name: 'b', output: { path: 'b' } },
|
|
75
|
+
],
|
|
76
|
+
output: { path: 'project' },
|
|
77
|
+
});
|
|
78
|
+
// {
|
|
79
|
+
// plaintext: true,
|
|
80
|
+
// output: { path: 'project', extension: 'htm' }, ← the project's path, a's extension
|
|
81
|
+
// css: { purge: false },
|
|
82
|
+
// }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
A plugin overrides the base, and the project overrides a plugin — even to turn
|
|
86
|
+
the plain text back off:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
defineMailConfig({ plugins: [{ name: 'a', plaintext: false }] }).plaintext; // false
|
|
90
|
+
defineMailConfig({
|
|
91
|
+
plugins: [{ name: 'a', plaintext: false }],
|
|
92
|
+
plaintext: { extension: 'text' },
|
|
93
|
+
}).plaintext; // { extension: 'text' }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## How two layers merge
|
|
97
|
+
|
|
98
|
+
The rule is Maizzle's own — the one its config loader uses to put your file
|
|
99
|
+
over its defaults — with three lists as the exception.
|
|
100
|
+
|
|
101
|
+
| What | Rule |
|
|
102
|
+
| --- | --- |
|
|
103
|
+
| An object | Merged key by key. The later layer's key wins; a key only an earlier layer sets stays |
|
|
104
|
+
| An array | **Replaced** by the later layer's |
|
|
105
|
+
| `components.source` | **Joined**, in layer order. A single entry (not in an array) counts as one |
|
|
106
|
+
| `vite.plugins` | **Joined**, in layer order |
|
|
107
|
+
| `vue.plugins` | **Joined**, in layer order. When any layer gives a factory, the result is a factory |
|
|
108
|
+
| A build event | **Chained** — [below](#build-events) |
|
|
109
|
+
|
|
110
|
+
### An array replaces
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const config = defineMailConfig({
|
|
114
|
+
plugins: [{ name: 'a', static: { source: ['a/**'], destination: 'a' } }],
|
|
115
|
+
static: { source: ['project/**'] },
|
|
116
|
+
});
|
|
117
|
+
config.static; // { source: ['project/**'], destination: 'a' }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The same holds against Maizzle's defaults, which sit under every layer: a
|
|
121
|
+
plugin that sets `content` replaces `emails/**/*.{vue,md}`, and one that sets
|
|
122
|
+
`static.source` replaces `public/**/*.*`.
|
|
123
|
+
|
|
124
|
+
### Three lists are joined
|
|
125
|
+
|
|
126
|
+
Under Maizzle's rule, two plugins that each bring components would keep only
|
|
127
|
+
the last one's. These three are added to instead:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const viteA = { name: 'vite-a' };
|
|
131
|
+
const viteB = { name: 'vite-b' };
|
|
132
|
+
|
|
133
|
+
const config = defineMailConfig({
|
|
134
|
+
plugins: [
|
|
135
|
+
{
|
|
136
|
+
name: 'a',
|
|
137
|
+
components: { source: { path: '/abs/a', prefix: 'A' } },
|
|
138
|
+
vite: { plugins: [viteA], base: '/a' },
|
|
139
|
+
},
|
|
140
|
+
{ name: 'b', vite: { plugins: [viteB] } },
|
|
141
|
+
],
|
|
142
|
+
components: { source: ['components-extra'] },
|
|
143
|
+
});
|
|
144
|
+
config.components; // { source: [{ path: '/abs/a', prefix: 'A' }, 'components-extra'] }
|
|
145
|
+
config.vite; // { base: '/a', plugins: [viteA, viteB] }
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
A `null` in one of the three sets nothing, as in Maizzle's own merge — it
|
|
149
|
+
neither empties the list nor ends up in it:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const config = defineMailConfig({
|
|
153
|
+
plugins: [{ name: 'a', components: { source: ['a'] } }],
|
|
154
|
+
components: { source: null as never },
|
|
155
|
+
});
|
|
156
|
+
config.components; // { source: ['a'] }
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Your own `components/` folder does not need to be in `components.source`:
|
|
160
|
+
Maizzle always reads it, whatever the list holds, so a component there is
|
|
161
|
+
found whatever the plugins bring.
|
|
162
|
+
|
|
163
|
+
`vue.plugins` may be a list or a factory, `() => Plugin[]`, which Maizzle calls
|
|
164
|
+
for each render so a stateful Vue plugin starts fresh. When every layer gives a
|
|
165
|
+
list, the lists are joined; when any gives a factory, the result is a factory
|
|
166
|
+
that calls each layer's factory and joins what they answer, in layer order:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
const one = { install() {} };
|
|
170
|
+
const two = { install() {} };
|
|
171
|
+
|
|
172
|
+
const config = defineMailConfig({
|
|
173
|
+
plugins: [{ name: 'a', vue: { plugins: () => [one] } }],
|
|
174
|
+
vue: { plugins: [two], globalProperties: { x: 1 } },
|
|
175
|
+
});
|
|
176
|
+
const plugins = config.vue?.plugins as () => unknown[];
|
|
177
|
+
plugins(); // [one, two] — a's factory called again on every call
|
|
178
|
+
config.vue?.globalProperties; // { x: 1 }
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Build events
|
|
182
|
+
|
|
183
|
+
Maizzle keeps **one** function per build event: under a plain merge, the last
|
|
184
|
+
layer's `beforeRender` would replace every other. `defineMailConfig` hands
|
|
185
|
+
Maizzle one function per event that runs every layer's handler, in layer
|
|
186
|
+
order — the base, each plugin in order, then the project — each awaited before
|
|
187
|
+
the next. An event only one layer sets is handed to Maizzle as it is.
|
|
188
|
+
|
|
189
|
+
Maizzle fires them in this order:
|
|
190
|
+
|
|
191
|
+
| Event | When | Parameters | What a returned string does |
|
|
192
|
+
| --- | --- | --- | --- |
|
|
193
|
+
| `beforeCreate` | Once, before any template | `{ config }` | Nothing |
|
|
194
|
+
| `beforeRender` | Before each template renders | `{ config, template }` | Replaces `template.source`; the next handler reads the new one |
|
|
195
|
+
| `afterRender` | After each render, before the transformers | `{ config, template, html }` | Becomes the `html` the next handler receives |
|
|
196
|
+
| `afterTransform` | After the transformers, on each template | `{ config, template, html }` | Becomes the `html` the next handler receives |
|
|
197
|
+
| `afterBuild` | Once, after every template | `{ config, files }` | Nothing |
|
|
198
|
+
|
|
199
|
+
A handler that returns nothing (or anything but a string) leaves the source or
|
|
200
|
+
the HTML as it was.
|
|
201
|
+
|
|
202
|
+
### `beforeRender`
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { defineMailConfig } from '@nxgt/mail-config';
|
|
206
|
+
|
|
207
|
+
export default defineMailConfig({
|
|
208
|
+
plugins: [
|
|
209
|
+
{ name: 'a', beforeRender: ({ template }) => `${template.source} a` },
|
|
210
|
+
{ name: 'b', beforeRender: ({ template }) => { console.log(template.source); } }, // 'source a'
|
|
211
|
+
],
|
|
212
|
+
beforeRender: async ({ template }) => `${template.source} project`,
|
|
213
|
+
});
|
|
214
|
+
// a template whose source is 'source' renders 'source a project'
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### `afterRender` and `afterTransform`
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
export default defineMailConfig({
|
|
221
|
+
plugins: [
|
|
222
|
+
{ name: 'a', afterTransform: ({ html }) => `${html}a` },
|
|
223
|
+
{ name: 'b', afterTransform: () => undefined }, // keeps what a answered
|
|
224
|
+
],
|
|
225
|
+
afterTransform: async ({ html }) => `${html}p`,
|
|
226
|
+
});
|
|
227
|
+
// '<p>' comes out as '<p>ap'
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Each handler receives the `html` the previous one answered; the `html` Maizzle
|
|
231
|
+
passed in is not changed.
|
|
232
|
+
|
|
233
|
+
### `beforeCreate` and `afterBuild`
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
export default defineMailConfig({
|
|
237
|
+
plugins: [
|
|
238
|
+
{ name: 'a', afterBuild: async () => { /* runs first */ } },
|
|
239
|
+
{ name: 'b', afterBuild: async () => { /* runs once a's has settled */ } },
|
|
240
|
+
],
|
|
241
|
+
afterBuild: async ({ files }) => { /* runs last, with every file written */ },
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### A throw stops the chain
|
|
246
|
+
|
|
247
|
+
A handler that throws, or rejects, stops there: the handlers after it do not
|
|
248
|
+
run, and the error reaches Maizzle unchanged, which fails the build with it.
|
|
249
|
+
Nothing is caught or logged on the way.
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
export default defineMailConfig({
|
|
253
|
+
plugins: [
|
|
254
|
+
{
|
|
255
|
+
name: 'a',
|
|
256
|
+
beforeRender: () => {
|
|
257
|
+
throw new Error('catalogue broken');
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
beforeRender: () => {
|
|
262
|
+
/* never runs */
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Errors
|
|
268
|
+
|
|
269
|
+
A mistake in how the config is wired is a bare `TypeError`, thrown by
|
|
270
|
+
`defineMailConfig` each time `maizzle.config.ts` is loaded — the first time
|
|
271
|
+
before any template is built, so the build stops there. A message names the
|
|
272
|
+
plugin by its `name`, or by its index when it has none. Several are also
|
|
273
|
+
refused at compile time — a plugin without a `name`, `plugins` that is not a
|
|
274
|
+
list, a build event that is not a function, a plugin that lists plugins; the
|
|
275
|
+
count is in the [README](../../README.md#type-safety-counted).
|
|
276
|
+
|
|
277
|
+
| Message | Cause |
|
|
278
|
+
| --- | --- |
|
|
279
|
+
| `defineMailConfig: config must be an object, as { plugins, ...maizzleConfig }` | `config` is `null`, an array, or not an object |
|
|
280
|
+
| `defineMailConfig: plugins must be an array` | `plugins` is set to something else |
|
|
281
|
+
| `defineMailConfig: plugins[0] must be a plugin object, as { name, ...config } — was it called?` | An entry is not an object — usually a plugin factory listed without calling it: `plugins: [brand]` for `plugins: [brand()]` |
|
|
282
|
+
| `defineMailConfig: plugins[1] has no name — a plugin is { name, ...config }` | An entry has no `name`, or a blank one |
|
|
283
|
+
| `defineMailConfig: plugin "a" lists plugins — a plugin cannot bring others; list them in the project` | A plugin sets `plugins`: plugins do not nest |
|
|
284
|
+
| `defineMailConfig: two plugins are named "i18n" — is one listed twice?` | Two entries share a `name` |
|
|
285
|
+
| `defineMailConfig: plugin "a": afterBuild must be a function` | A plugin sets a build event to something other than a function |
|
|
286
|
+
| `defineMailConfig: beforeRender must be a function` | The project sets a build event to something other than a function |
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
import { defineMailConfig } from '@nxgt/mail-config';
|
|
290
|
+
import { brand } from './plugins/brand'; // a factory: brand(options) answers the plugin
|
|
291
|
+
|
|
292
|
+
export default defineMailConfig({
|
|
293
|
+
plugins: [brand], // TypeError: defineMailConfig: plugins[0] must be a plugin object, … — was it called?
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Each message says where the problem is, never a value from your config. A
|
|
298
|
+
plugin checked by `defineMailPlugin` in its own package is refused there, with
|
|
299
|
+
`defineMailPlugin: plugin …` messages — see
|
|
300
|
+
[Writing a plugin](plugins.md#errors).
|
|
301
|
+
|
|
302
|
+
## See also
|
|
303
|
+
|
|
304
|
+
- [Writing a plugin](plugins.md) — what goes in a plugin, and `defineMailPlugin`.
|
|
305
|
+
- [Production](production.md) — the same layering, for `maizzle.config.production.ts`.
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# Writing a plugin
|
|
2
|
+
|
|
3
|
+
This page is for shipping part of a Maizzle config in a package — components,
|
|
4
|
+
global properties, build hooks — so a project adds it with one line in
|
|
5
|
+
`plugins`.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// brand.ts in a package, say `acme-mail-brand`, next to its components/ folder
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { defineMailPlugin } from '@nxgt/mail-config';
|
|
11
|
+
|
|
12
|
+
export const brand = defineMailPlugin({
|
|
13
|
+
name: 'brand',
|
|
14
|
+
components: {
|
|
15
|
+
source: [{ path: fileURLToPath(new URL('./components', import.meta.url)), prefix: 'Brand' }],
|
|
16
|
+
},
|
|
17
|
+
vue: { globalProperties: { company: 'Example Inc.' } },
|
|
18
|
+
afterTransform: ({ html }) => `${html}<!-- brand -->`,
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// maizzle.config.ts, in the project
|
|
24
|
+
import { defineMailConfig } from '@nxgt/mail-config';
|
|
25
|
+
import { brand } from 'acme-mail-brand';
|
|
26
|
+
|
|
27
|
+
export default defineMailConfig({ plugins: [brand] });
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```vue
|
|
31
|
+
<!-- emails/welcome.vue -->
|
|
32
|
+
<template>
|
|
33
|
+
<Html>
|
|
34
|
+
<Body>
|
|
35
|
+
<Container>
|
|
36
|
+
<Text>Welcome to {{ company }}.</Text>
|
|
37
|
+
<BrandFooter />
|
|
38
|
+
</Container>
|
|
39
|
+
</Body>
|
|
40
|
+
</Html>
|
|
41
|
+
</template>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`<BrandFooter>` is the package's `components/footer.vue`; `{{ company }}` is
|
|
45
|
+
the global property; every built file ends with `<!-- brand -->`.
|
|
46
|
+
|
|
47
|
+
## The signature
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import type { MaizzleConfig } from '@maizzle/framework';
|
|
51
|
+
|
|
52
|
+
interface MailPlugin extends MaizzleConfig {
|
|
53
|
+
readonly name: string;
|
|
54
|
+
readonly plugins?: never; // a plugin cannot bring others
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function defineMailPlugin(plugin: MailPlugin): MailPlugin;
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
A plugin is any Maizzle config key, plus a `name`. `defineMailPlugin` answers
|
|
61
|
+
the object it is given, unchanged; it runs the checks `defineMailConfig` would
|
|
62
|
+
run, so a mistake throws where the plugin is written — in the package, when it
|
|
63
|
+
loads, with a `defineMailPlugin: plugin …` message — and not in each project
|
|
64
|
+
that lists it. See [Errors](#errors). It also types the export as
|
|
65
|
+
`MailPlugin`, so the package's declarations name it.
|
|
66
|
+
|
|
67
|
+
A plain object `{ name, ...config }` in `plugins` works as well:
|
|
68
|
+
`defineMailPlugin` is for a plugin written in one place and used in another.
|
|
69
|
+
|
|
70
|
+
## `name`
|
|
71
|
+
|
|
72
|
+
Names the plugin in an error, and must be unique in a project: two plugins with
|
|
73
|
+
the same `name` are refused, since that is usually one plugin listed twice. It
|
|
74
|
+
is not passed to Maizzle.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
defineMailPlugin({ name: 'brand', plaintext: false }); // answers the same object
|
|
78
|
+
defineMailPlugin({ output: {} } as never);
|
|
79
|
+
// TypeError: defineMailPlugin: plugin has no name — a plugin is { name, ...config }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Components, under a prefix
|
|
83
|
+
|
|
84
|
+
Point `components.source` at a folder **inside the package**, as an absolute
|
|
85
|
+
path, with a `prefix`:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { fileURLToPath } from 'node:url';
|
|
89
|
+
import { defineMailPlugin } from '@nxgt/mail-config';
|
|
90
|
+
|
|
91
|
+
const here = (path: string) => fileURLToPath(new URL(path, import.meta.url));
|
|
92
|
+
|
|
93
|
+
export const brand = defineMailPlugin({
|
|
94
|
+
name: 'brand',
|
|
95
|
+
components: { source: [{ path: here('./components'), prefix: 'Brand' }] },
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- **Absolute.** Maizzle resolves a relative `components.source` against the
|
|
100
|
+
directory `maizzle` runs in — the project, where `./components` is the
|
|
101
|
+
project's own folder, not the package's. `new URL(…, import.meta.url)` is
|
|
102
|
+
relative to the plugin's file wherever the package is installed.
|
|
103
|
+
- **Prefixed.** `prefix: 'Brand'` makes `footer.vue` `<BrandFooter>`, so the
|
|
104
|
+
package never shadows Maizzle's own components (`<Button>`) or the project's.
|
|
105
|
+
- **Joined, not replaced.** Every plugin's `components.source` is kept, in the
|
|
106
|
+
order of `plugins`, then the project's — see
|
|
107
|
+
[The project config](config.md#three-lists-are-joined).
|
|
108
|
+
- **Shipped.** The folder must be in the package's `files`, or it is not in
|
|
109
|
+
the tarball:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"files": ["dist", "components", "README.md", "package.json", "LICENSE"]
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
When the plugin is compiled to `dist/`, `import.meta.url` is the built file's:
|
|
118
|
+
point the path at where the folder sits relative to `dist/index.js`
|
|
119
|
+
(`'../components'`).
|
|
120
|
+
|
|
121
|
+
## Global properties
|
|
122
|
+
|
|
123
|
+
`vue.globalProperties` makes a value available in every template, with no
|
|
124
|
+
import. It is an object, so it merges key by key: a later plugin's key wins
|
|
125
|
+
over an earlier one's, the project's over every plugin's, and a key only one
|
|
126
|
+
layer sets stays.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { defineMailConfig, defineMailPlugin } from '@nxgt/mail-config';
|
|
130
|
+
|
|
131
|
+
const alpha = defineMailPlugin({
|
|
132
|
+
name: 'alpha',
|
|
133
|
+
vue: { globalProperties: { greeting: 'from alpha', origin: 'alpha' } },
|
|
134
|
+
});
|
|
135
|
+
const beta = defineMailPlugin({
|
|
136
|
+
name: 'beta',
|
|
137
|
+
vue: { globalProperties: { greeting: 'from beta' } },
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
export default defineMailConfig({
|
|
141
|
+
plugins: [alpha, beta],
|
|
142
|
+
vue: { globalProperties: { greeting: 'from the project' } },
|
|
143
|
+
});
|
|
144
|
+
// {{ greeting }} / {{ origin }} renders 'from the project / alpha'
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Hooks
|
|
148
|
+
|
|
149
|
+
A plugin sets any of Maizzle's five build events — `beforeCreate`,
|
|
150
|
+
`beforeRender`, `afterRender`, `afterTransform`, `afterBuild` — with Maizzle's
|
|
151
|
+
own parameter types. It never sees the other layers' handlers: each runs in
|
|
152
|
+
turn, and a string it returns is what the next one reads. The table of events
|
|
153
|
+
is in [The project config](config.md#build-events).
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { defineMailPlugin } from '@nxgt/mail-config';
|
|
157
|
+
|
|
158
|
+
export const tracking = defineMailPlugin({
|
|
159
|
+
name: 'tracking',
|
|
160
|
+
// replaces the template's source for the handlers after it, and for the render
|
|
161
|
+
beforeRender: ({ template }) => template.source.replaceAll('[[year]]', String(new Date().getFullYear())),
|
|
162
|
+
// the html the next handler receives, and in the end the file written
|
|
163
|
+
afterTransform: ({ html }) => html.replace('</body>', '<!-- sent by Example Inc. --></body>'),
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
A hook that finds the project wrong **throws**: the handlers after it do not
|
|
168
|
+
run, and Maizzle fails the build with the error. Do not log and return.
|
|
169
|
+
|
|
170
|
+
## The order of `plugins` matters
|
|
171
|
+
|
|
172
|
+
Plugins are layered in the order listed, and their hooks run in that order.
|
|
173
|
+
Two plugins that each rewrite what the other wrote give a different result the
|
|
174
|
+
other way round:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { defineMailConfig, defineMailPlugin } from '@nxgt/mail-config';
|
|
178
|
+
|
|
179
|
+
/** Rewrites `[[mark]]` to `[[alpha]]`, which only beta knows how to finish. */
|
|
180
|
+
const alpha = defineMailPlugin({
|
|
181
|
+
name: 'alpha',
|
|
182
|
+
beforeRender: ({ template }) => template.source.replace('[[mark]]', '[[alpha]]'),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
/** Finishes what alpha started. */
|
|
186
|
+
const beta = defineMailPlugin({
|
|
187
|
+
name: 'beta',
|
|
188
|
+
beforeRender: ({ template }) => template.source.replace('[[alpha]]', 'alpha then beta'),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
export default defineMailConfig({ plugins: [alpha, beta] }); // [[mark]] → 'alpha then beta'
|
|
192
|
+
// plugins: [beta, alpha] leaves '[[alpha]]' in the e-mail
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
A plugin that depends on another's output says so in its README, and names
|
|
196
|
+
which one comes first. The same order decides which plugin's key wins: the
|
|
197
|
+
later one.
|
|
198
|
+
|
|
199
|
+
## A plugin with options
|
|
200
|
+
|
|
201
|
+
A package usually exports a function that takes options and answers the
|
|
202
|
+
plugin. Check the options there, and throw a bare `TypeError` for a wrong one —
|
|
203
|
+
a wiring mistake, raised when the config loads, never in the middle of a build:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { fileURLToPath } from 'node:url';
|
|
207
|
+
import { defineMailPlugin, type MailPlugin } from '@nxgt/mail-config';
|
|
208
|
+
|
|
209
|
+
export interface BrandOptions {
|
|
210
|
+
/** Shown in every template as {{ company }}. */
|
|
211
|
+
readonly company: string;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function brand(options: BrandOptions): MailPlugin {
|
|
215
|
+
if (typeof options?.company !== 'string' || options.company.trim() === '') {
|
|
216
|
+
throw new TypeError('brand: company must be a non-empty string');
|
|
217
|
+
}
|
|
218
|
+
return defineMailPlugin({
|
|
219
|
+
name: 'brand',
|
|
220
|
+
components: {
|
|
221
|
+
source: [{ path: fileURLToPath(new URL('../components', import.meta.url)), prefix: 'Brand' }],
|
|
222
|
+
},
|
|
223
|
+
vue: { globalProperties: { company: options.company } },
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
// maizzle.config.ts
|
|
230
|
+
export default defineMailConfig({ plugins: [brand({ company: 'Example Inc.' })] });
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Listing the factory without calling it — `plugins: [brand]` — is refused by
|
|
234
|
+
`defineMailConfig` with `plugins[0] must be a plugin object, … — was it
|
|
235
|
+
called?`.
|
|
236
|
+
|
|
237
|
+
## What a plugin cannot do
|
|
238
|
+
|
|
239
|
+
- **Bring other plugins.** `MailPlugin` declares `plugins?: never`, so a
|
|
240
|
+
plugin that sets `plugins` is a type error, and a `TypeError` at load time
|
|
241
|
+
(`plugin "a" lists plugins — a plugin cannot bring others; list them in the
|
|
242
|
+
project`). A project lists every plugin it uses, so their order is visible
|
|
243
|
+
in one place.
|
|
244
|
+
- **Win over the project.** The project's config is always the top layer.
|
|
245
|
+
- **Add to an array other than the three joined lists.** A plugin that sets
|
|
246
|
+
`content` or `static.source` replaces Maizzle's default for it, and is
|
|
247
|
+
replaced by a later layer that sets it.
|
|
248
|
+
|
|
249
|
+
## Errors
|
|
250
|
+
|
|
251
|
+
`defineMailPlugin` throws a bare `TypeError` when the module that calls it
|
|
252
|
+
loads. Its messages name the plugin by its `name`, or `plugin` when it has
|
|
253
|
+
none:
|
|
254
|
+
|
|
255
|
+
| Message | Cause |
|
|
256
|
+
| --- | --- |
|
|
257
|
+
| `defineMailPlugin: plugin must be a plugin object, as { name, ...config } — was it called?` | The argument is not an object — `null`, an array, a function |
|
|
258
|
+
| `defineMailPlugin: plugin has no name — a plugin is { name, ...config }` | No `name`, or a blank one |
|
|
259
|
+
| `defineMailPlugin: plugin "a" lists plugins — a plugin cannot bring others; list them in the project` | The plugin sets `plugins` |
|
|
260
|
+
| `defineMailPlugin: plugin "a": beforeRender must be a function` | A build event set to something other than a function |
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
import { defineMailPlugin } from '@nxgt/mail-config';
|
|
264
|
+
|
|
265
|
+
export const broken = defineMailPlugin({
|
|
266
|
+
name: 'a',
|
|
267
|
+
beforeRender: 'x' as never,
|
|
268
|
+
}); // TypeError: defineMailPlugin: plugin "a": beforeRender must be a function
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
The same mistakes in a plain object listed in `plugins` are refused by
|
|
272
|
+
`defineMailConfig`, as `defineMailConfig: plugins[1] …` — see
|
|
273
|
+
[The project config](config.md#errors). Two plugins with the same `name` can
|
|
274
|
+
only be seen there, in the project.
|
|
275
|
+
|
|
276
|
+
## See also
|
|
277
|
+
|
|
278
|
+
- [The project config](config.md) — the layers, the merge rules, the chained
|
|
279
|
+
events and every error.
|
|
280
|
+
- [Production](production.md) — a hook that only runs in the production build.
|