@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.
@@ -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.