@nxgt/mail 0.5.0 → 0.5.1
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 +10 -1
- package/dist/renderer.d.ts +8 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +16 -4
- package/dist/renderer.js.map +3 -3
- package/docs/README.md +1 -1
- package/docs/guide/rendering.md +47 -2
- package/docs/roadmap.md +13 -13
- package/docs/troubleshooting.md +48 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ import without extensions, so `nodenext` is not supported.
|
|
|
42
42
|
| Import | What it holds |
|
|
43
43
|
| --- | --- |
|
|
44
44
|
| `@nxgt/mail` | The port (`Mailer`, `MailMessage`, `Rendered`, `SentMail`, `Address`, `MailAttachment`), the errors (`MailError`, `MailFailure`, `MailRefused`), `createMemoryMailer`, `pickLocale` and `parseAcceptLanguage`, `listUnsubscribe` with `ListUnsubscribeOptions` and `ListUnsubscribeHeaders`, and what a transport calls first: `checkMessage`, `recipientsOf`, `addressOf`. No Node built-in: it runs anywhere |
|
|
45
|
-
| `@nxgt/mail/renderer` | The renderer: `createMailRenderer`, `MailRenderer`, `MailRendererOptions`, `RenderOptions`, `MailVariables`,
|
|
45
|
+
| `@nxgt/mail/renderer` | The renderer: `createMailRenderer`, `MailRenderer`, `MailRendererOptions`, `RenderOptions`, `MailVariables`, the types that type it with a build's `MailEmails` (`MailEmailsOf`, `AnyMailEmails`, `RenderArguments`), and `MANIFEST_FORMAT`, the newest manifest format it reads. Reads the build with `node:fs` |
|
|
46
46
|
| `@nxgt/mail/conformance` | **For transport authors**: `describeMailer`, its cases as data, `runMailerCase`, the messages they send (`sampleMessage`, `sampleAttachment`), and the memory mailer's harness as a worked example |
|
|
47
47
|
|
|
48
48
|
## Usage
|
|
@@ -88,6 +88,15 @@ variable missing or unknown, and a number for a URL variable, as
|
|
|
88
88
|
[Rendering](docs/guide/rendering.md) for the options, typing the renderer,
|
|
89
89
|
the locale chosen through `getLanguage`, and every error.
|
|
90
90
|
|
|
91
|
+
A renderer reads every manifest format up to its `MANIFEST_FORMAT`, within
|
|
92
|
+
0.x: a build from any earlier `@nxgt/mail-i18n` 0.x keeps working with a newer
|
|
93
|
+
`@nxgt/mail`, so a package that ships a prebuilt format-1 build can peer
|
|
94
|
+
`@nxgt/mail` `>=0.1.0 <1` — the lower bound is the first `@nxgt/mail` that
|
|
95
|
+
reads the build's format. A build in a newer format fails at start-up with
|
|
96
|
+
`… is manifest format 2, newer than this @nxgt/mail reads (1) — upgrade
|
|
97
|
+
@nxgt/mail`. See
|
|
98
|
+
[Rendering — which builds it reads](docs/guide/rendering.md#which-builds-it-reads--manifest_format).
|
|
99
|
+
|
|
91
100
|
### Sending — the port and `MailMessage`
|
|
92
101
|
|
|
93
102
|
A `MailMessage` is a rendered e-mail — `subject`, `html`, `text` — plus its
|
package/dist/renderer.d.ts
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { type WantedLocales } from './locale';
|
|
7
7
|
import type { Rendered } from './types';
|
|
8
|
+
/**
|
|
9
|
+
* The newest manifest format this renderer reads. It reads every format up
|
|
10
|
+
* to this one, within 0.x: a build from any earlier `@nxgt/mail-i18n` 0.x
|
|
11
|
+
* keeps working. A manifest without `formatVersion` is format 1, as
|
|
12
|
+
* `@nxgt/mail-i18n` 0.1 and 0.2 wrote it. Copied in
|
|
13
|
+
* packages/mail-i18n/src/manifest.ts: change both.
|
|
14
|
+
*/
|
|
15
|
+
export declare const MANIFEST_FORMAT = 1;
|
|
8
16
|
/**
|
|
9
17
|
* A value only known at send time. A number is written as `String(n)`; any
|
|
10
18
|
* other type is refused, so an object never renders as `[object Object]`.
|
package/dist/renderer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AAC1D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAKxC;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC;AAEtE;;;GAGG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI;IAAE,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,aAAa;CAAE,CAAC;AAEzE,6EAA6E;AAC7E,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;AAEpE;;;;;GAKG;AACH,MAAM,MAAM,eAAe,CAAC,CAAC,IAC5B,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,SAAS,CAAC,GACtC,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa,CAAC,GACxC,CAAC,SAAS,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa,CAAC,CAAC;AAE5C,MAAM,WAAW,mBAAmB;IACnC,6EAA6E;IAC7E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,aAAa,CAAC;IAC3C,oEAAoE;IACpE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,aAAa;IAC7B,oFAAoF;IACpF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,YAAY,CAAC,CAAC,CAAC,GAAG,aAAa;IACtE,iFAAiF;IACjF,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,EAAE,CAAC;IAC/C,gCAAgC;IAChC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC;;;;;OAKG;IACH,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,EAChC,KAAK,EAAE,CAAC,EACR,GAAG,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAC5B,QAAQ,CAAC;CACZ;
|
|
1
|
+
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AAC1D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAKxC;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,IAAI,CAAC;AAEjC;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC;AAEtE;;;GAGG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI;IAAE,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,aAAa;CAAE,CAAC;AAEzE,6EAA6E;AAC7E,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;AAEpE;;;;;GAKG;AACH,MAAM,MAAM,eAAe,CAAC,CAAC,IAC5B,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,SAAS,CAAC,GACtC,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa,CAAC,GACxC,CAAC,SAAS,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa,CAAC,CAAC;AAE5C,MAAM,WAAW,mBAAmB;IACnC,6EAA6E;IAC7E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,aAAa,CAAC;IAC3C,oEAAoE;IACpE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,aAAa;IAC7B,oFAAoF;IACpF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,YAAY,CAAC,CAAC,CAAC,GAAG,aAAa;IACtE,iFAAiF;IACjF,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,EAAE,CAAC;IAC/C,gCAAgC;IAChC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC;;;;;OAKG;IACH,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,EAChC,KAAK,EAAE,CAAC,EACR,GAAG,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAC5B,QAAQ,CAAC;CACZ;AAgPD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,YAAY,CAAC,CAAC,CAAC,GAAG,aAAa,EAC3E,OAAO,EAAE,mBAAmB,GAC1B,YAAY,CAAC,CAAC,CAAC,CAmDjB"}
|
package/dist/renderer.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { readFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
var MANIFEST_FILE = "mail-manifest.json";
|
|
12
|
+
var MANIFEST_FORMAT = 1;
|
|
12
13
|
var PLACEHOLDER = /\{\{\s*([a-z][a-zA-Z0-9]*)\s*\}\}/g;
|
|
13
14
|
var LINE_BREAKS = /[\r\n\v\f\u0085\u2028\u2029]+/g;
|
|
14
15
|
var SAFE_URL = /^(?:https?:\/\/|mailto:)/i;
|
|
@@ -57,8 +58,18 @@ function readManifest(dir) {
|
|
|
57
58
|
}
|
|
58
59
|
throw error;
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
const notManifest = () => new Error(`createMailRenderer: ${file} is not a manifest of @nxgt/mail-i18n — build with its i18n() plugin`);
|
|
62
|
+
if (!isObject(manifest))
|
|
63
|
+
throw notManifest();
|
|
64
|
+
const format = "formatVersion" in manifest ? manifest.formatVersion : 1;
|
|
65
|
+
if (typeof format !== "number" || !Number.isSafeInteger(format) || format < 1) {
|
|
66
|
+
throw notManifest();
|
|
67
|
+
}
|
|
68
|
+
if (format > MANIFEST_FORMAT) {
|
|
69
|
+
throw new Error(`createMailRenderer: ${file} is manifest format ${format}, newer than this @nxgt/mail reads (${MANIFEST_FORMAT}) — upgrade @nxgt/mail`);
|
|
70
|
+
}
|
|
71
|
+
if (!isStringList(manifest.locales) || manifest.locales.length === 0 || typeof manifest.fallbackLocale !== "string" || !isObject(manifest.emails)) {
|
|
72
|
+
throw notManifest();
|
|
62
73
|
}
|
|
63
74
|
return {
|
|
64
75
|
locales: manifest.locales,
|
|
@@ -67,7 +78,7 @@ function readManifest(dir) {
|
|
|
67
78
|
};
|
|
68
79
|
}
|
|
69
80
|
function readEmail(dir, name, entry, locales) {
|
|
70
|
-
const broken = () => new Error(`createMailRenderer: ${MANIFEST_FILE} describes ${name} in a shape
|
|
81
|
+
const broken = () => new Error(`createMailRenderer: ${MANIFEST_FILE} describes ${name} in a shape its format does not have — it was changed after the build; run maizzle build again`);
|
|
71
82
|
if (!isObject(entry) || !isStringList(entry.variables) || !isStringList(entry.urlVariables) || !isObject(entry.subject) || !isObject(entry.files)) {
|
|
72
83
|
throw broken();
|
|
73
84
|
}
|
|
@@ -162,8 +173,9 @@ function createMailRenderer(options) {
|
|
|
162
173
|
return renderer;
|
|
163
174
|
}
|
|
164
175
|
export {
|
|
176
|
+
MANIFEST_FORMAT,
|
|
165
177
|
createMailRenderer
|
|
166
178
|
};
|
|
167
179
|
|
|
168
|
-
//# debugId=
|
|
180
|
+
//# debugId=3F107BDB6620800264756E2164756E21
|
|
169
181
|
//# sourceMappingURL=renderer.js.map
|
package/dist/renderer.js.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/renderer.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * `@nxgt/mail/renderer` — fills the files `maizzle build` wrote with\n * `@nxgt/mail-i18n`. Its own entry because it reads them with `node:fs`: the\n * main entry, which every transport imports, stays free of Node built-ins.\n */\n\nimport { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { MailRefused } from './errors';\nimport { pickLocale, type WantedLocales } from './locale';\nimport type { Rendered } from './types';\n\n/** The manifest's name in the build's output folder, as `@nxgt/mail-i18n` writes it. */\nconst MANIFEST_FILE = 'mail-manifest.json';\n\n/**\n * A value only known at send time. A number is written as `String(n)`; any\n * other type is refused, so an object never renders as `[object Object]`.\n */\nexport type MailVariables = Readonly<Record<string, string | number>>;\n\n/**\n * The e-mails of a build, each with the variables it takes: the `MailEmails`\n * that `@nxgt/mail-i18n` writes in `generated/mail.ts`.\n */\nexport type MailEmailsOf<E> = { readonly [K in keyof E]: MailVariables };\n\n/** What any build takes, when the renderer is not given its `MailEmails`. */\nexport type AnyMailEmails = Readonly<Record<string, MailVariables>>;\n\n/**\n * `render`'s arguments after the e-mail's name: its variables, which may be\n * left out when it takes none, and the options. An e-mail without variables\n * is `Readonly<Record<string, never>>`, as `@nxgt/mail-i18n`'s\n * `rendererTypes()` writes it: change both together.\n */\nexport type RenderArguments<V> =\n\tReadonly<Record<string, never>> extends V\n\t\t? [variables?: V, options?: RenderOptions]\n\t\t: [variables: V, options?: RenderOptions];\n\nexport interface MailRendererOptions {\n\t/** The build's output folder — where `mail-manifest.json` is — as `dist`. */\n\treadonly dir: string;\n\t/**\n\t * The locales wanted, most wanted first, asked at each render — as\n\t * `@nxgt/i18n`'s language provider: `() => user.locale`, or a Hono\n\t * handler's `() => c.get('language')`. Picked with `pickLocale`. Default:\n\t * none, so the fallback locale.\n\t */\n\treadonly getLanguage?: () => WantedLocales;\n\t/** The locale when none wanted is built. Default the manifest's. */\n\treadonly fallbackLocale?: string;\n}\n\nexport interface RenderOptions {\n\t/** Render in this locale, one the build wrote, rather than asking `getLanguage`. */\n\treadonly locale?: string;\n}\n\n/**\n * Given the build's `MailEmails`, an unknown e-mail, a missing or unknown\n * variable, or a number for a URL is a compile error; without it, any name\n * and any variables compile, and the same mistakes throw.\n */\nexport interface MailRenderer<E extends MailEmailsOf<E> = AnyMailEmails> {\n\t/** The e-mails of the build, sorted, as `['reset-password', 'verify-email']`. */\n\treadonly emails: readonly (keyof E & string)[];\n\t/** The locales of the build. */\n\treadonly locales: readonly string[];\n\t/**\n\t * `email` in the wanted locale, every `{{ variable }}` filled: escaped in\n\t * `html`, as is in `text` and the subject. A missing or unknown variable,\n\t * an unknown e-mail or locale **throws** an `Error`; a URL variable that\n\t * is not an `http:`, `https:` or `mailto:` URL throws `MailRefused`.\n\t */\n\trender<N extends keyof E & string>(\n\t\temail: N,\n\t\t...rest: RenderArguments<E[N]>\n\t): Rendered;\n}\n\n/** One e-mail in one locale, read. */\ninterface Parts {\n\treadonly subject: string;\n\treadonly html: string;\n\treadonly text: string;\n}\n\ninterface Email {\n\treadonly variables: ReadonlySet<string>;\n\treadonly urlVariables: ReadonlySet<string>;\n\treadonly locales: ReadonlyMap<string, Parts>;\n}\n\n/**\n * `{{ name }}` — what `@nxgt/mail-i18n`'s `placeholder('name')` writes. A\n * copy of `PLACEHOLDER` in `packages/mail-i18n/src/manifest.ts`, as is the\n * manifest's shape below: change both, or a declared variable goes unfilled.\n */\nconst PLACEHOLDER = /\\{\\{\\s*([a-z][a-zA-Z0-9]*)\\s*\\}\\}/g;\n\n/** A line break a header would split on: each run becomes one space. */\nconst LINE_BREAKS = /[\\r\\n\\v\\f\\u0085\\u2028\\u2029]+/g;\n\n/** What a URL variable may hold: a scheme a mail client cannot run. */\nconst SAFE_URL = /^(?:https?:\\/\\/|mailto:)/i;\n\n/** Whitespace, a control, a quote or a bracket: what no URL holds as is. */\nconst hasUnsafeUrlChar = (value: string) =>\n\t[...value].some((char) => {\n\t\tconst code = char.codePointAt(0) ?? 0;\n\t\treturn code < 0x21 || code === 0x7f || /[\\s\"'<>`]/.test(char);\n\t});\n\nconst HTML_ESCAPES: Readonly<Record<string, string>> = {\n\t'&': '&',\n\t'<': '<',\n\t'>': '>',\n\t'\"': '"',\n\t\"'\": ''',\n};\n\nconst escapeHtml = (value: string) =>\n\tvalue.replace(/[&<>\"']/g, (char) => HTML_ESCAPES[char] ?? char);\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n\ttypeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst isStringList = (value: unknown): value is string[] =>\n\tArray.isArray(value) && value.every((item) => typeof item === 'string');\n\nfunction checkOptions(options: MailRendererOptions): void {\n\tif (!isObject(options)) {\n\t\tthrow new TypeError(\n\t\t\t\"createMailRenderer: options must be an object, as { dir: 'dist' }\",\n\t\t);\n\t}\n\tif (typeof options.dir !== 'string' || options.dir.trim() === '') {\n\t\tthrow new TypeError(\n\t\t\t'createMailRenderer: dir must be the folder maizzle build wrote, as dist',\n\t\t);\n\t}\n\tif (\n\t\toptions.getLanguage !== undefined &&\n\t\ttypeof options.getLanguage !== 'function'\n\t) {\n\t\tthrow new TypeError(\n\t\t\t'createMailRenderer: getLanguage must be a function that answers the wanted locales, as () => user.locale',\n\t\t);\n\t}\n}\n\n/** Reads `dir/path`; a missing file **throws**, naming it. */\nfunction readBuilt(dir: string, path: string): string {\n\ttry {\n\t\treturn readFileSync(join(dir, path), 'utf8');\n\t} catch (error) {\n\t\tthrow new Error(\n\t\t\t`createMailRenderer: ${join(dir, path)} cannot be read — run maizzle build, and deploy its output folder`,\n\t\t\t{ cause: error },\n\t\t);\n\t}\n}\n\n/** The manifest, its shape checked: a broken one **throws**. */\nfunction readManifest(dir: string) {\n\tconst file = join(dir, MANIFEST_FILE);\n\tlet manifest: unknown;\n\ttry {\n\t\tmanifest = JSON.parse(readBuilt(dir, MANIFEST_FILE));\n\t} catch (error) {\n\t\tif (error instanceof SyntaxError) {\n\t\t\tthrow new Error(`createMailRenderer: ${file} is not valid JSON`, {\n\t\t\t\tcause: error,\n\t\t\t});\n\t\t}\n\t\tthrow error;\n\t}\n\tif (\n\t\t!isObject(manifest) ||\n\t\t!isStringList(manifest.locales) ||\n\t\tmanifest.locales.length === 0 ||\n\t\ttypeof manifest.fallbackLocale !== 'string' ||\n\t\t!isObject(manifest.emails)\n\t) {\n\t\tthrow new Error(\n\t\t\t`createMailRenderer: ${file} is not a manifest of @nxgt/mail-i18n — build with its i18n() plugin`,\n\t\t);\n\t}\n\treturn {\n\t\tlocales: manifest.locales,\n\t\tfallbackLocale: manifest.fallbackLocale,\n\t\temails: manifest.emails,\n\t};\n}\n\n/** One e-mail of the manifest, with its files read in every locale. */\nfunction readEmail(\n\tdir: string,\n\tname: string,\n\tentry: unknown,\n\tlocales: readonly string[],\n): Email {\n\tconst broken = () =>\n\t\tnew Error(\n\t\t\t`createMailRenderer: ${MANIFEST_FILE} describes ${name} in a shape this version does not read — rebuild with the same version of @nxgt/mail-i18n`,\n\t\t);\n\tif (\n\t\t!isObject(entry) ||\n\t\t!isStringList(entry.variables) ||\n\t\t!isStringList(entry.urlVariables) ||\n\t\t!isObject(entry.subject) ||\n\t\t!isObject(entry.files)\n\t) {\n\t\tthrow broken();\n\t}\n\tconst parts = new Map<string, Parts>();\n\tfor (const locale of locales) {\n\t\tconst subject = entry.subject[locale];\n\t\tconst files = entry.files[locale];\n\t\tif (\n\t\t\ttypeof subject !== 'string' ||\n\t\t\t!isObject(files) ||\n\t\t\ttypeof files.html !== 'string'\n\t\t) {\n\t\t\tthrow broken();\n\t\t}\n\t\tif (typeof files.text !== 'string') {\n\t\t\tthrow new Error(\n\t\t\t\t`createMailRenderer: ${name} has no text part in ${locale} — keep Maizzle's plaintext on, as @nxgt/mail-config sets it`,\n\t\t\t);\n\t\t}\n\t\tparts.set(locale, {\n\t\t\tsubject,\n\t\t\thtml: readBuilt(dir, files.html),\n\t\t\ttext: readBuilt(dir, files.text),\n\t\t});\n\t}\n\treturn {\n\t\tvariables: new Set(entry.variables),\n\t\turlVariables: new Set(entry.urlVariables),\n\t\tlocales: parts,\n\t};\n}\n\n/** Each variable of `email` as a string, checked against what it declares. */\nfunction checkVariables(\n\tname: string,\n\temail: Email,\n\tvariables: unknown,\n): Map<string, string> {\n\tif (!isObject(variables)) {\n\t\tthrow new TypeError(\n\t\t\t`render: the variables of ${name} must be an object, as { name: 'Ada' }`,\n\t\t);\n\t}\n\tconst values = new Map<string, string>();\n\tfor (const [key, value] of Object.entries(variables)) {\n\t\tif (!email.variables.has(key)) {\n\t\t\tthrow new Error(\n\t\t\t\t`render: ${name} has no variable ${key} — it takes ${[...email.variables].join(', ') || 'none'}`,\n\t\t\t);\n\t\t}\n\t\tif (typeof value === 'number' && Number.isFinite(value)) {\n\t\t\tvalues.set(key, String(value));\n\t\t} else if (typeof value === 'string') {\n\t\t\tvalues.set(key, value);\n\t\t} else {\n\t\t\tthrow new TypeError(\n\t\t\t\t`render: ${name}: ${key} must be a string or a finite number`,\n\t\t\t);\n\t\t}\n\t}\n\tfor (const key of email.variables) {\n\t\tconst value = values.get(key);\n\t\tif (value === undefined) {\n\t\t\tthrow new Error(`render: ${name} needs the variable ${key}`);\n\t\t}\n\t\tif (\n\t\t\temail.urlVariables.has(key) &&\n\t\t\t(!SAFE_URL.test(value) || hasUnsafeUrlChar(value) || !URL.canParse(value))\n\t\t) {\n\t\t\tthrow new MailRefused(\n\t\t\t\t`render: ${name}: ${key} must be an http:, https: or mailto: URL`,\n\t\t\t);\n\t\t}\n\t}\n\treturn values;\n}\n\n/** `source` with each placeholder replaced, in one pass: a value is never read again. */\nconst fill = (\n\tsource: string,\n\tvalues: ReadonlyMap<string, string>,\n\twrite: (value: string) => string,\n) =>\n\tsource.replace(PLACEHOLDER, (mark, key: string) => {\n\t\tconst value = values.get(key);\n\t\treturn value === undefined ? mark : write(value);\n\t});\n\n/**\n * The run-time renderer: the files `maizzle build` wrote with\n * `@nxgt/mail-i18n`, filled with the values of one send.\n *\n * ```ts\n * import type { MailEmails } from './generated/mail';\n *\n * const mails = createMailRenderer<MailEmails>({ dir: 'dist', getLanguage: () => user.locale });\n * await mailer.send({ to: user.email, ...mails.render('verify-email', { name, link }) });\n * ```\n *\n * Reads the manifest and every file once, here: a missing or broken build\n * **throws** when the renderer is created, not at the first send. A wrong\n * option is a `TypeError`.\n */\nexport function createMailRenderer<E extends MailEmailsOf<E> = AnyMailEmails>(\n\toptions: MailRendererOptions,\n): MailRenderer<E> {\n\tcheckOptions(options);\n\tconst { dir } = options;\n\tconst manifest = readManifest(dir);\n\tconst fallback = options.fallbackLocale ?? manifest.fallbackLocale;\n\tif (!manifest.locales.includes(fallback)) {\n\t\tthrow new TypeError(\n\t\t\t`createMailRenderer: fallbackLocale must be one of the build's locales, ${manifest.locales.join(', ')}`,\n\t\t);\n\t}\n\tconst emails = new Map<string, Email>();\n\tfor (const [name, entry] of Object.entries(manifest.emails)) {\n\t\temails.set(name, readEmail(dir, name, entry, manifest.locales));\n\t}\n\tconst getLanguage = options.getLanguage;\n\n\tconst renderer: MailRenderer = Object.freeze({\n\t\temails: Object.freeze([...emails.keys()].sort()),\n\t\tlocales: Object.freeze([...manifest.locales]),\n\t\trender(\n\t\t\tname: string,\n\t\t\tvariables: MailVariables = {},\n\t\t\trenderOptions: RenderOptions = {},\n\t\t): Rendered {\n\t\t\tconst email = emails.get(name);\n\t\t\tif (email === undefined) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`render: ${String(name)} is not an e-mail of the build — one of ${[...emails.keys()].sort().join(', ')}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst locale =\n\t\t\t\trenderOptions.locale ??\n\t\t\t\tpickLocale(getLanguage?.(), manifest.locales, fallback);\n\t\t\tconst parts = email.locales.get(locale);\n\t\t\tif (parts === undefined) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`render: the locale asked for is not one of the build's, ${manifest.locales.join(', ')}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst values = checkVariables(name, email, variables);\n\t\t\treturn {\n\t\t\t\tsubject: fill(parts.subject, values, (value) => value)\n\t\t\t\t\t.replace(LINE_BREAKS, ' ')\n\t\t\t\t\t.trim(),\n\t\t\t\thtml: fill(parts.html, values, escapeHtml),\n\t\t\t\ttext: fill(parts.text, values, (value) => value),\n\t\t\t};\n\t\t},\n\t});\n\t// The types only narrow what the same checks refuse at run time.\n\treturn renderer as unknown as MailRenderer<E>;\n}\n"
|
|
5
|
+
"/**\n * `@nxgt/mail/renderer` — fills the files `maizzle build` wrote with\n * `@nxgt/mail-i18n`. Its own entry because it reads them with `node:fs`: the\n * main entry, which every transport imports, stays free of Node built-ins.\n */\n\nimport { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { MailRefused } from './errors';\nimport { pickLocale, type WantedLocales } from './locale';\nimport type { Rendered } from './types';\n\n/** The manifest's name in the build's output folder, as `@nxgt/mail-i18n` writes it. */\nconst MANIFEST_FILE = 'mail-manifest.json';\n\n/**\n * The newest manifest format this renderer reads. It reads every format up\n * to this one, within 0.x: a build from any earlier `@nxgt/mail-i18n` 0.x\n * keeps working. A manifest without `formatVersion` is format 1, as\n * `@nxgt/mail-i18n` 0.1 and 0.2 wrote it. Copied in\n * packages/mail-i18n/src/manifest.ts: change both.\n */\nexport const MANIFEST_FORMAT = 1;\n\n/**\n * A value only known at send time. A number is written as `String(n)`; any\n * other type is refused, so an object never renders as `[object Object]`.\n */\nexport type MailVariables = Readonly<Record<string, string | number>>;\n\n/**\n * The e-mails of a build, each with the variables it takes: the `MailEmails`\n * that `@nxgt/mail-i18n` writes in `generated/mail.ts`.\n */\nexport type MailEmailsOf<E> = { readonly [K in keyof E]: MailVariables };\n\n/** What any build takes, when the renderer is not given its `MailEmails`. */\nexport type AnyMailEmails = Readonly<Record<string, MailVariables>>;\n\n/**\n * `render`'s arguments after the e-mail's name: its variables, which may be\n * left out when it takes none, and the options. An e-mail without variables\n * is `Readonly<Record<string, never>>`, as `@nxgt/mail-i18n`'s\n * `rendererTypes()` writes it: change both together.\n */\nexport type RenderArguments<V> =\n\tReadonly<Record<string, never>> extends V\n\t\t? [variables?: V, options?: RenderOptions]\n\t\t: [variables: V, options?: RenderOptions];\n\nexport interface MailRendererOptions {\n\t/** The build's output folder — where `mail-manifest.json` is — as `dist`. */\n\treadonly dir: string;\n\t/**\n\t * The locales wanted, most wanted first, asked at each render — as\n\t * `@nxgt/i18n`'s language provider: `() => user.locale`, or a Hono\n\t * handler's `() => c.get('language')`. Picked with `pickLocale`. Default:\n\t * none, so the fallback locale.\n\t */\n\treadonly getLanguage?: () => WantedLocales;\n\t/** The locale when none wanted is built. Default the manifest's. */\n\treadonly fallbackLocale?: string;\n}\n\nexport interface RenderOptions {\n\t/** Render in this locale, one the build wrote, rather than asking `getLanguage`. */\n\treadonly locale?: string;\n}\n\n/**\n * Given the build's `MailEmails`, an unknown e-mail, a missing or unknown\n * variable, or a number for a URL is a compile error; without it, any name\n * and any variables compile, and the same mistakes throw.\n */\nexport interface MailRenderer<E extends MailEmailsOf<E> = AnyMailEmails> {\n\t/** The e-mails of the build, sorted, as `['reset-password', 'verify-email']`. */\n\treadonly emails: readonly (keyof E & string)[];\n\t/** The locales of the build. */\n\treadonly locales: readonly string[];\n\t/**\n\t * `email` in the wanted locale, every `{{ variable }}` filled: escaped in\n\t * `html`, as is in `text` and the subject. A missing or unknown variable,\n\t * an unknown e-mail or locale **throws** an `Error`; a URL variable that\n\t * is not an `http:`, `https:` or `mailto:` URL throws `MailRefused`.\n\t */\n\trender<N extends keyof E & string>(\n\t\temail: N,\n\t\t...rest: RenderArguments<E[N]>\n\t): Rendered;\n}\n\n/** One e-mail in one locale, read. */\ninterface Parts {\n\treadonly subject: string;\n\treadonly html: string;\n\treadonly text: string;\n}\n\ninterface Email {\n\treadonly variables: ReadonlySet<string>;\n\treadonly urlVariables: ReadonlySet<string>;\n\treadonly locales: ReadonlyMap<string, Parts>;\n}\n\n/**\n * `{{ name }}` — what `@nxgt/mail-i18n`'s `placeholder('name')` writes. A\n * copy of `PLACEHOLDER` in `packages/mail-i18n/src/manifest.ts`, as is the\n * manifest's shape below: change both, or a declared variable goes unfilled.\n */\nconst PLACEHOLDER = /\\{\\{\\s*([a-z][a-zA-Z0-9]*)\\s*\\}\\}/g;\n\n/** A line break a header would split on: each run becomes one space. */\nconst LINE_BREAKS = /[\\r\\n\\v\\f\\u0085\\u2028\\u2029]+/g;\n\n/** What a URL variable may hold: a scheme a mail client cannot run. */\nconst SAFE_URL = /^(?:https?:\\/\\/|mailto:)/i;\n\n/** Whitespace, a control, a quote or a bracket: what no URL holds as is. */\nconst hasUnsafeUrlChar = (value: string) =>\n\t[...value].some((char) => {\n\t\tconst code = char.codePointAt(0) ?? 0;\n\t\treturn code < 0x21 || code === 0x7f || /[\\s\"'<>`]/.test(char);\n\t});\n\nconst HTML_ESCAPES: Readonly<Record<string, string>> = {\n\t'&': '&',\n\t'<': '<',\n\t'>': '>',\n\t'\"': '"',\n\t\"'\": ''',\n};\n\nconst escapeHtml = (value: string) =>\n\tvalue.replace(/[&<>\"']/g, (char) => HTML_ESCAPES[char] ?? char);\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n\ttypeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst isStringList = (value: unknown): value is string[] =>\n\tArray.isArray(value) && value.every((item) => typeof item === 'string');\n\nfunction checkOptions(options: MailRendererOptions): void {\n\tif (!isObject(options)) {\n\t\tthrow new TypeError(\n\t\t\t\"createMailRenderer: options must be an object, as { dir: 'dist' }\",\n\t\t);\n\t}\n\tif (typeof options.dir !== 'string' || options.dir.trim() === '') {\n\t\tthrow new TypeError(\n\t\t\t'createMailRenderer: dir must be the folder maizzle build wrote, as dist',\n\t\t);\n\t}\n\tif (\n\t\toptions.getLanguage !== undefined &&\n\t\ttypeof options.getLanguage !== 'function'\n\t) {\n\t\tthrow new TypeError(\n\t\t\t'createMailRenderer: getLanguage must be a function that answers the wanted locales, as () => user.locale',\n\t\t);\n\t}\n}\n\n/** Reads `dir/path`; a missing file **throws**, naming it. */\nfunction readBuilt(dir: string, path: string): string {\n\ttry {\n\t\treturn readFileSync(join(dir, path), 'utf8');\n\t} catch (error) {\n\t\tthrow new Error(\n\t\t\t`createMailRenderer: ${join(dir, path)} cannot be read — run maizzle build, and deploy its output folder`,\n\t\t\t{ cause: error },\n\t\t);\n\t}\n}\n\n/** The manifest, its shape checked: a broken one **throws**. */\nfunction readManifest(dir: string) {\n\tconst file = join(dir, MANIFEST_FILE);\n\tlet manifest: unknown;\n\ttry {\n\t\tmanifest = JSON.parse(readBuilt(dir, MANIFEST_FILE));\n\t} catch (error) {\n\t\tif (error instanceof SyntaxError) {\n\t\t\tthrow new Error(`createMailRenderer: ${file} is not valid JSON`, {\n\t\t\t\tcause: error,\n\t\t\t});\n\t\t}\n\t\tthrow error;\n\t}\n\tconst notManifest = () =>\n\t\tnew Error(\n\t\t\t`createMailRenderer: ${file} is not a manifest of @nxgt/mail-i18n — build with its i18n() plugin`,\n\t\t);\n\tif (!isObject(manifest)) throw notManifest();\n\t// The format first, before any field it may change: a newer format is\n\t// refused as newer, whatever its shape. Absent: format 1, written before\n\t// the field was.\n\tconst format = 'formatVersion' in manifest ? manifest.formatVersion : 1;\n\tif (\n\t\ttypeof format !== 'number' ||\n\t\t!Number.isSafeInteger(format) ||\n\t\tformat < 1\n\t) {\n\t\tthrow notManifest();\n\t}\n\tif (format > MANIFEST_FORMAT) {\n\t\tthrow new Error(\n\t\t\t`createMailRenderer: ${file} is manifest format ${format}, newer than this @nxgt/mail reads (${MANIFEST_FORMAT}) — upgrade @nxgt/mail`,\n\t\t);\n\t}\n\tif (\n\t\t!isStringList(manifest.locales) ||\n\t\tmanifest.locales.length === 0 ||\n\t\ttypeof manifest.fallbackLocale !== 'string' ||\n\t\t!isObject(manifest.emails)\n\t) {\n\t\tthrow notManifest();\n\t}\n\treturn {\n\t\tlocales: manifest.locales,\n\t\tfallbackLocale: manifest.fallbackLocale,\n\t\temails: manifest.emails,\n\t};\n}\n\n/** One e-mail of the manifest, with its files read in every locale. */\nfunction readEmail(\n\tdir: string,\n\tname: string,\n\tentry: unknown,\n\tlocales: readonly string[],\n): Email {\n\tconst broken = () =>\n\t\tnew Error(\n\t\t\t`createMailRenderer: ${MANIFEST_FILE} describes ${name} in a shape its format does not have — it was changed after the build; run maizzle build again`,\n\t\t);\n\tif (\n\t\t!isObject(entry) ||\n\t\t!isStringList(entry.variables) ||\n\t\t!isStringList(entry.urlVariables) ||\n\t\t!isObject(entry.subject) ||\n\t\t!isObject(entry.files)\n\t) {\n\t\tthrow broken();\n\t}\n\tconst parts = new Map<string, Parts>();\n\tfor (const locale of locales) {\n\t\tconst subject = entry.subject[locale];\n\t\tconst files = entry.files[locale];\n\t\tif (\n\t\t\ttypeof subject !== 'string' ||\n\t\t\t!isObject(files) ||\n\t\t\ttypeof files.html !== 'string'\n\t\t) {\n\t\t\tthrow broken();\n\t\t}\n\t\tif (typeof files.text !== 'string') {\n\t\t\tthrow new Error(\n\t\t\t\t`createMailRenderer: ${name} has no text part in ${locale} — keep Maizzle's plaintext on, as @nxgt/mail-config sets it`,\n\t\t\t);\n\t\t}\n\t\tparts.set(locale, {\n\t\t\tsubject,\n\t\t\thtml: readBuilt(dir, files.html),\n\t\t\ttext: readBuilt(dir, files.text),\n\t\t});\n\t}\n\treturn {\n\t\tvariables: new Set(entry.variables),\n\t\turlVariables: new Set(entry.urlVariables),\n\t\tlocales: parts,\n\t};\n}\n\n/** Each variable of `email` as a string, checked against what it declares. */\nfunction checkVariables(\n\tname: string,\n\temail: Email,\n\tvariables: unknown,\n): Map<string, string> {\n\tif (!isObject(variables)) {\n\t\tthrow new TypeError(\n\t\t\t`render: the variables of ${name} must be an object, as { name: 'Ada' }`,\n\t\t);\n\t}\n\tconst values = new Map<string, string>();\n\tfor (const [key, value] of Object.entries(variables)) {\n\t\tif (!email.variables.has(key)) {\n\t\t\tthrow new Error(\n\t\t\t\t`render: ${name} has no variable ${key} — it takes ${[...email.variables].join(', ') || 'none'}`,\n\t\t\t);\n\t\t}\n\t\tif (typeof value === 'number' && Number.isFinite(value)) {\n\t\t\tvalues.set(key, String(value));\n\t\t} else if (typeof value === 'string') {\n\t\t\tvalues.set(key, value);\n\t\t} else {\n\t\t\tthrow new TypeError(\n\t\t\t\t`render: ${name}: ${key} must be a string or a finite number`,\n\t\t\t);\n\t\t}\n\t}\n\tfor (const key of email.variables) {\n\t\tconst value = values.get(key);\n\t\tif (value === undefined) {\n\t\t\tthrow new Error(`render: ${name} needs the variable ${key}`);\n\t\t}\n\t\tif (\n\t\t\temail.urlVariables.has(key) &&\n\t\t\t(!SAFE_URL.test(value) || hasUnsafeUrlChar(value) || !URL.canParse(value))\n\t\t) {\n\t\t\tthrow new MailRefused(\n\t\t\t\t`render: ${name}: ${key} must be an http:, https: or mailto: URL`,\n\t\t\t);\n\t\t}\n\t}\n\treturn values;\n}\n\n/** `source` with each placeholder replaced, in one pass: a value is never read again. */\nconst fill = (\n\tsource: string,\n\tvalues: ReadonlyMap<string, string>,\n\twrite: (value: string) => string,\n) =>\n\tsource.replace(PLACEHOLDER, (mark, key: string) => {\n\t\tconst value = values.get(key);\n\t\treturn value === undefined ? mark : write(value);\n\t});\n\n/**\n * The run-time renderer: the files `maizzle build` wrote with\n * `@nxgt/mail-i18n`, filled with the values of one send.\n *\n * ```ts\n * import type { MailEmails } from './generated/mail';\n *\n * const mails = createMailRenderer<MailEmails>({ dir: 'dist', getLanguage: () => user.locale });\n * await mailer.send({ to: user.email, ...mails.render('verify-email', { name, link }) });\n * ```\n *\n * Reads the manifest and every file once, here: a missing or broken build\n * **throws** when the renderer is created, not at the first send. A wrong\n * option is a `TypeError`.\n */\nexport function createMailRenderer<E extends MailEmailsOf<E> = AnyMailEmails>(\n\toptions: MailRendererOptions,\n): MailRenderer<E> {\n\tcheckOptions(options);\n\tconst { dir } = options;\n\tconst manifest = readManifest(dir);\n\tconst fallback = options.fallbackLocale ?? manifest.fallbackLocale;\n\tif (!manifest.locales.includes(fallback)) {\n\t\tthrow new TypeError(\n\t\t\t`createMailRenderer: fallbackLocale must be one of the build's locales, ${manifest.locales.join(', ')}`,\n\t\t);\n\t}\n\tconst emails = new Map<string, Email>();\n\tfor (const [name, entry] of Object.entries(manifest.emails)) {\n\t\temails.set(name, readEmail(dir, name, entry, manifest.locales));\n\t}\n\tconst getLanguage = options.getLanguage;\n\n\tconst renderer: MailRenderer = Object.freeze({\n\t\temails: Object.freeze([...emails.keys()].sort()),\n\t\tlocales: Object.freeze([...manifest.locales]),\n\t\trender(\n\t\t\tname: string,\n\t\t\tvariables: MailVariables = {},\n\t\t\trenderOptions: RenderOptions = {},\n\t\t): Rendered {\n\t\t\tconst email = emails.get(name);\n\t\t\tif (email === undefined) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`render: ${String(name)} is not an e-mail of the build — one of ${[...emails.keys()].sort().join(', ')}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst locale =\n\t\t\t\trenderOptions.locale ??\n\t\t\t\tpickLocale(getLanguage?.(), manifest.locales, fallback);\n\t\t\tconst parts = email.locales.get(locale);\n\t\t\tif (parts === undefined) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`render: the locale asked for is not one of the build's, ${manifest.locales.join(', ')}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst values = checkVariables(name, email, variables);\n\t\t\treturn {\n\t\t\t\tsubject: fill(parts.subject, values, (value) => value)\n\t\t\t\t\t.replace(LINE_BREAKS, ' ')\n\t\t\t\t\t.trim(),\n\t\t\t\thtml: fill(parts.html, values, escapeHtml),\n\t\t\t\ttext: fill(parts.text, values, (value) => value),\n\t\t\t};\n\t\t},\n\t});\n\t// The types only narrow what the same checks refuse at run time.\n\treturn renderer as unknown as MailRenderer<E>;\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;;;;;AAMA;AACA;AAMA,IAAM,gBAAgB;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "3F107BDB6620800264756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/docs/README.md
CHANGED
|
@@ -7,7 +7,7 @@ mailer, transport, hand-over, refusal, failure — are defined once, in the
|
|
|
7
7
|
|
|
8
8
|
| Page | Read it when |
|
|
9
9
|
| --- | --- |
|
|
10
|
-
| [Rendering](guide/rendering.md) | You are turning a Maizzle build of `@nxgt/mail-i18n` into an e-mail with `createMailRenderer`: its options, typing it with the build's `MailEmails`, choosing the locale, escaping, URL variables, deploying the build, and every error |
|
|
10
|
+
| [Rendering](guide/rendering.md) | You are turning a Maizzle build of `@nxgt/mail-i18n` into an e-mail with `createMailRenderer`: its options, which builds it reads (`MANIFEST_FORMAT`), typing it with the build's `MailEmails`, choosing the locale, escaping, URL variables, deploying the build, and every error |
|
|
11
11
|
| [Sending](guide/sending.md) | You are calling `mailer.send`: the `MailMessage` shape, addresses, headers, one-click unsubscribe with `listUnsubscribe` (the headers, DKIM, the endpoint), attachments, the idempotency key, what `send` answers, and turning `MailFailure` and `MailRefused` into a response |
|
|
12
12
|
| [Testing](guide/testing.md) | You are testing code that sends e-mail with `createMemoryMailer`: reading the outbox and its attachments, making a send fail, counting attempts, a retry under an idempotency key |
|
|
13
13
|
| [Locales](guide/locales.md) | You are choosing the locale an e-mail is rendered in, with `pickLocale` and `parseAcceptLanguage` |
|
package/docs/guide/rendering.md
CHANGED
|
@@ -49,6 +49,50 @@ Nothing is evaluated at send time: no template engine and no Maizzle run in
|
|
|
49
49
|
your server. `render` replaces each `{{ name }}` the build left in place, and
|
|
50
50
|
nothing else.
|
|
51
51
|
|
|
52
|
+
### Which builds it reads — `MANIFEST_FORMAT`
|
|
53
|
+
|
|
54
|
+
The manifest carries its format, `formatVersion`, and the renderer exports the
|
|
55
|
+
newest format it reads:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { MANIFEST_FORMAT } from '@nxgt/mail/renderer';
|
|
59
|
+
|
|
60
|
+
MANIFEST_FORMAT; // 1 — the newest manifest format this @nxgt/mail reads
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const MANIFEST_FORMAT = 1;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The promise, within 0.x:
|
|
68
|
+
|
|
69
|
+
- **A renderer reads every format up to its own.** A build from any earlier
|
|
70
|
+
`@nxgt/mail-i18n` 0.x keeps working with a newer `@nxgt/mail`: a package
|
|
71
|
+
that ships a prebuilt format-1 build can peer `@nxgt/mail` `>=0.1.0 <1`.
|
|
72
|
+
The peer's lower bound is the first `@nxgt/mail` that reads the build's
|
|
73
|
+
format.
|
|
74
|
+
- **A manifest without `formatVersion` is format 1**, as `@nxgt/mail-i18n`
|
|
75
|
+
0.1 and 0.2 wrote it.
|
|
76
|
+
- **The format changes only when the manifest's shape does.** A new
|
|
77
|
+
`@nxgt/mail-i18n` that writes the same shape writes the same format.
|
|
78
|
+
|
|
79
|
+
So upgrade `@nxgt/mail` no later than `@nxgt/mail-i18n`. From 0.5.1, a
|
|
80
|
+
renderer refuses a newer format when it starts, before reading any other
|
|
81
|
+
field. 0.1.0 to 0.5.0 know no format and read only format 1: a build in a
|
|
82
|
+
later format must peer at least the first `@nxgt/mail` that reads it.
|
|
83
|
+
|
|
84
|
+
| The manifest's `formatVersion` | At start-up |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| absent | Read as format 1 |
|
|
87
|
+
| `1` to `MANIFEST_FORMAT` | Read |
|
|
88
|
+
| an integer above `MANIFEST_FORMAT` | `Error`: `createMailRenderer: dist/mail-manifest.json is manifest format 2, newer than this @nxgt/mail reads (1) — upgrade @nxgt/mail` |
|
|
89
|
+
| anything else — `0`, `1.5`, `'1'`, `null` | `Error`: `createMailRenderer: dist/mail-manifest.json is not a manifest of @nxgt/mail-i18n — build with its i18n() plugin` |
|
|
90
|
+
|
|
91
|
+
`@nxgt/mail` 0.5.0 and earlier do not export `MANIFEST_FORMAT` and ignore
|
|
92
|
+
`formatVersion`; they read format 1. A package that ships its build checks the
|
|
93
|
+
format it wrote against the renderers it supports when it builds: see
|
|
94
|
+
[the manifest guide — shipping a build in a package](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-i18n/docs/guide/manifest.md#shipping-a-build-in-a-package).
|
|
95
|
+
|
|
52
96
|
## `createMailRenderer`
|
|
53
97
|
|
|
54
98
|
```ts
|
|
@@ -402,9 +446,10 @@ read. The paths are `dir` joined with the file, as you passed `dir`.
|
|
|
402
446
|
| `createMailRenderer: getLanguage must be a function that answers the wanted locales, as () => user.locale` | `TypeError` | `getLanguage` given as a locale rather than a function |
|
|
403
447
|
| `createMailRenderer: dist/mail-manifest.json cannot be read — run maizzle build, and deploy its output folder` | `Error` | No build at `dir`: not built, not deployed, or `dir` read from another working directory |
|
|
404
448
|
| `createMailRenderer: dist/mail-manifest.json is not valid JSON` | `Error` | The manifest was cut or edited |
|
|
405
|
-
| `createMailRenderer: dist/mail-manifest.json is not a manifest of @nxgt/mail-i18n — build with its i18n() plugin` | `Error` | A JSON file without `locales`, `fallbackLocale` and `emails` |
|
|
449
|
+
| `createMailRenderer: dist/mail-manifest.json is not a manifest of @nxgt/mail-i18n — build with its i18n() plugin` | `Error` | A JSON file without `locales`, `fallbackLocale` and `emails`, or with a `formatVersion` that is not a positive integer |
|
|
450
|
+
| `createMailRenderer: dist/mail-manifest.json is manifest format 2, newer than this @nxgt/mail reads (1) — upgrade @nxgt/mail` | `Error` | A build from a newer `@nxgt/mail-i18n`, whose manifest format this renderer predates — see [which builds it reads](#which-builds-it-reads--manifest_format) |
|
|
406
451
|
| `createMailRenderer: fallbackLocale must be one of the build's locales, en, fr` | `TypeError` | `fallbackLocale` names a locale the build does not have |
|
|
407
|
-
| `createMailRenderer: mail-manifest.json describes verify-email in a shape
|
|
452
|
+
| `createMailRenderer: mail-manifest.json describes verify-email in a shape its format does not have — it was changed after the build; run maizzle build again` | `Error` | An entry of the manifest lacks a field, or a locale: every format has them, so the file was edited, merged or cut after `maizzle build` wrote it |
|
|
408
453
|
| `createMailRenderer: verify-email has no text part in fr — keep Maizzle's plaintext on, as @nxgt/mail-config sets it` | `Error` | The project turned Maizzle's `plaintext` off; every e-mail sent has a text part |
|
|
409
454
|
| `createMailRenderer: dist/fr/verify-email.txt cannot be read — run maizzle build, and deploy its output folder` | `Error` | A file the manifest lists is gone: only part of the build was deployed |
|
|
410
455
|
|
package/docs/roadmap.md
CHANGED
|
@@ -6,13 +6,12 @@ the only number.
|
|
|
6
6
|
|
|
7
7
|
## Now
|
|
8
8
|
|
|
9
|
-
- **
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
is refused. Built, not yet published.
|
|
9
|
+
- **A build read by any later renderer** — within 0.x, `createMailRenderer`
|
|
10
|
+
reads every manifest format up to its own (`MANIFEST_FORMAT`, exported from
|
|
11
|
+
`@nxgt/mail/renderer`), so a build from any earlier `@nxgt/mail-i18n` 0.x
|
|
12
|
+
keeps working, and a package that ships a prebuilt format-1 build can peer
|
|
13
|
+
`@nxgt/mail` `>=0.1.0 <1`. A newer format is refused at start-up. Built,
|
|
14
|
+
not yet published.
|
|
16
15
|
|
|
17
16
|
## Next
|
|
18
17
|
|
|
@@ -69,6 +68,13 @@ Nothing yet.
|
|
|
69
68
|
The last ten, newest first, each with the version it came in. Everything
|
|
70
69
|
before is in the [CHANGELOG](../CHANGELOG.md).
|
|
71
70
|
|
|
71
|
+
- **The idempotency key in the conformance suite, and the unsubscribe URL as
|
|
72
|
+
written, v0.5.0** — a fourteenth case, `send.idempotencyKey`, delivers a
|
|
73
|
+
message with a fresh key and expects it never refused for it, the key in
|
|
74
|
+
none of its recipients, subject, HTML or text. `listUnsubscribe` writes
|
|
75
|
+
`new URL(url).href` and checks it as well as what it was given: a `%` that
|
|
76
|
+
starts no escape, or a host escape decoded into a refused character, is a
|
|
77
|
+
`MailRefused`. SMTP and Resend move their peer to `^0.5.0`.
|
|
72
78
|
- **One-click unsubscribe, v0.4.0** — `listUnsubscribe({ url, mailto? })`
|
|
73
79
|
answers RFC 8058's `List-Unsubscribe` and `List-Unsubscribe-Post` headers,
|
|
74
80
|
to spread into a message's `headers`, so Gmail and Yahoo offer their
|
|
@@ -125,9 +131,3 @@ before is in the [CHANGELOG](../CHANGELOG.md).
|
|
|
125
131
|
call written out. The
|
|
126
132
|
type parameter is optional; untyped, the renderer is unchanged, and the
|
|
127
133
|
run-time checks hold either way.
|
|
128
|
-
- **Two transports, `@nxgt/mail-smtp` and `@nxgt/mail-resend` v0.1.0** —
|
|
129
|
-
SMTP on the `nodemailer` you install, and Resend over `fetch` with no SDK,
|
|
130
|
-
each passing the conformance suite — against a local SMTP server, and a
|
|
131
|
-
local server answering as Resend does — and throwing `@nxgt/mail`'s errors.
|
|
132
|
-
See [the SMTP roadmap](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-smtp/docs/roadmap.md)
|
|
133
|
-
and [the Resend roadmap](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-resend/docs/roadmap.md).
|
package/docs/troubleshooting.md
CHANGED
|
@@ -91,7 +91,8 @@ How the messages are shaped:
|
|
|
91
91
|
- [`createMailRenderer: <dir>/<locale>/<email>.html cannot be read — run maizzle build, and deploy its output folder`](#createmailrenderer-dirlocaleemailhtml-cannot-be-read--run-maizzle-build-and-deploy-its-output-folder)
|
|
92
92
|
- [`createMailRenderer: <dir>/mail-manifest.json is not valid JSON`](#createmailrenderer-dirmail-manifestjson-is-not-valid-json)
|
|
93
93
|
- [`createMailRenderer: <dir>/mail-manifest.json is not a manifest of @nxgt/mail-i18n — build with its i18n() plugin`](#createmailrenderer-dirmail-manifestjson-is-not-a-manifest-of-nxgtmail-i18n--build-with-its-i18n-plugin)
|
|
94
|
-
- [`createMailRenderer: mail-manifest.json
|
|
94
|
+
- [`createMailRenderer: <dir>/mail-manifest.json is manifest format <format>, newer than this @nxgt/mail reads (<newest>) — upgrade @nxgt/mail`](#createmailrenderer-dirmail-manifestjson-is-manifest-format-format-newer-than-this-nxgtmail-reads-newest--upgrade-nxgtmail)
|
|
95
|
+
- [`createMailRenderer: mail-manifest.json describes <email> in a shape its format does not have — it was changed after the build; run maizzle build again`](#createmailrenderer-mail-manifestjson-describes-email-in-a-shape-its-format-does-not-have--it-was-changed-after-the-build-run-maizzle-build-again)
|
|
95
96
|
- [`createMailRenderer: <email> has no text part in <locale> — keep Maizzle's plaintext on, as @nxgt/mail-config sets it`](#createmailrenderer-email-has-no-text-part-in-locale--keep-maizzles-plaintext-on-as-nxgtmail-config-sets-it)
|
|
96
97
|
- [`Could not resolve "node:fs"`, or `No such module "node:fs"`, on an edge runtime](#could-not-resolve-nodefs-or-no-such-module-nodefs-on-an-edge-runtime)
|
|
97
98
|
|
|
@@ -1226,7 +1227,8 @@ An `Error`.
|
|
|
1226
1227
|
|
|
1227
1228
|
**When:** start-up, when `dir` points at a folder whose `mail-manifest.json`
|
|
1228
1229
|
has no `locales` list (or an empty one), no `fallbackLocale` or no `emails`
|
|
1229
|
-
object
|
|
1230
|
+
object, or a `formatVersion` that is not a positive integer (`0`, `1.5`,
|
|
1231
|
+
`"1"`, `null`): typically a `mail-manifest.json` written by something else.
|
|
1230
1232
|
**Why:** the renderer reads only the manifest the `i18n()` plugin of
|
|
1231
1233
|
`@nxgt/mail-i18n` writes. A Maizzle build without that plugin writes no
|
|
1232
1234
|
manifest at all, and fails with
|
|
@@ -1244,19 +1246,51 @@ export default defineMailConfig({
|
|
|
1244
1246
|
});
|
|
1245
1247
|
```
|
|
1246
1248
|
|
|
1247
|
-
### `createMailRenderer: mail-manifest.json
|
|
1249
|
+
### `createMailRenderer: <dir>/mail-manifest.json is manifest format <format>, newer than this @nxgt/mail reads (<newest>) — upgrade @nxgt/mail`
|
|
1250
|
+
|
|
1251
|
+
An `Error`, as `… is manifest format 2, newer than this @nxgt/mail reads (1)
|
|
1252
|
+
— upgrade @nxgt/mail`.
|
|
1253
|
+
|
|
1254
|
+
**When:** start-up, when the build was made with a `@nxgt/mail-i18n` that
|
|
1255
|
+
writes a newer manifest format than the installed `@nxgt/mail` reads: the
|
|
1256
|
+
build tool was upgraded and the server was not, or a package that ships a
|
|
1257
|
+
prebuilt build needs a newer `@nxgt/mail` than the one installed.
|
|
1258
|
+
**Why:** a renderer reads every manifest format up to its `MANIFEST_FORMAT`,
|
|
1259
|
+
and refuses a newer one rather than misread it. The format only changes when
|
|
1260
|
+
the manifest's shape does.
|
|
1261
|
+
**Fix:** upgrade `@nxgt/mail` in the server that renders, to a version whose
|
|
1262
|
+
`MANIFEST_FORMAT` is at least the format in the message:
|
|
1263
|
+
|
|
1264
|
+
```sh
|
|
1265
|
+
bun add @nxgt/mail@latest
|
|
1266
|
+
```
|
|
1267
|
+
|
|
1268
|
+
```ts
|
|
1269
|
+
import { MANIFEST_FORMAT } from '@nxgt/mail/renderer';
|
|
1270
|
+
|
|
1271
|
+
MANIFEST_FORMAT; // must be >= the manifest's formatVersion
|
|
1272
|
+
```
|
|
1273
|
+
|
|
1274
|
+
A package that ships its build states the lowest `@nxgt/mail` it needs in its
|
|
1275
|
+
peer range; see
|
|
1276
|
+
[Rendering — which builds it reads](guide/rendering.md#which-builds-it-reads--manifest_format).
|
|
1277
|
+
|
|
1278
|
+
### `createMailRenderer: mail-manifest.json describes <email> in a shape its format does not have — it was changed after the build; run maizzle build again`
|
|
1248
1279
|
|
|
1249
1280
|
An `Error`.
|
|
1250
1281
|
|
|
1251
1282
|
**When:** start-up, when an e-mail's entry in the manifest lacks
|
|
1252
1283
|
`variables`, `urlVariables`, `subject` or `files`, or has no subject or HTML
|
|
1253
1284
|
file for one of the build's locales.
|
|
1254
|
-
**Why:**
|
|
1255
|
-
`@nxgt/mail`
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1285
|
+
**Why:** every manifest format has those fields, for every e-mail and every
|
|
1286
|
+
locale, and `@nxgt/mail-i18n` writes them all. An entry without one was
|
|
1287
|
+
changed after `maizzle build` wrote it: a hand edit, a merge conflict resolved
|
|
1288
|
+
in a committed build, a script that rewrote the file, a copy cut short. It is
|
|
1289
|
+
not a version mismatch: within 0.x, a renderer reads every format up to its
|
|
1290
|
+
own, so a build from an earlier `@nxgt/mail-i18n` 0.x keeps working.
|
|
1291
|
+
**Fix:** run `maizzle build` again, and deploy its output without editing
|
|
1292
|
+
`mail-manifest.json`. If the build is fresh and untouched, it is a
|
|
1293
|
+
[bug in this package](#a-bug-in-nxgtmail-itself).
|
|
1260
1294
|
|
|
1261
1295
|
### `createMailRenderer: <email> has no text part in <locale> — keep Maizzle's plaintext on, as @nxgt/mail-config sets it`
|
|
1262
1296
|
|
|
@@ -1268,8 +1302,11 @@ comes after `@nxgt/mail-config`'s base.
|
|
|
1268
1302
|
**Why:** every e-mail is sent with a text part — `Rendered` and `MailMessage`
|
|
1269
1303
|
require one — and the renderer only fills it; it does not derive it from the
|
|
1270
1304
|
HTML at send time.
|
|
1271
|
-
**Fix:** remove the `plaintext: false
|
|
1272
|
-
`
|
|
1305
|
+
**Fix:** remove the `plaintext: false`, then run `maizzle build` again.
|
|
1306
|
+
`defineMailConfig`'s base turns the text part on, laid out in paragraphs.
|
|
1307
|
+
Leave `plaintext` out rather than writing `plaintext: true`: `true` replaces
|
|
1308
|
+
the base's options, and the text part runs onto one line again — see
|
|
1309
|
+
[@nxgt/mail-config's troubleshooting](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-config/docs/troubleshooting.md#the-plain-text-part-is-one-long-line-again).
|
|
1273
1310
|
|
|
1274
1311
|
### `Could not resolve "node:fs"`, or `No such module "node:fs"`, on an edge runtime
|
|
1275
1312
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxgt/mail",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "The run-time side of transactional e-mail: the renderer that fills a Maizzle build made with @nxgt/mail-i18n, the Mailer a transport implements, its errors, a memory transport and locale selection. No dependency.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|