@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 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`, and the types that type it with a build's `MailEmails` (`MailEmailsOf`, `AnyMailEmails`, `RenderArguments`). Reads the build with `node:fs` |
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
@@ -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]`.
@@ -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;AA8ND;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,YAAY,CAAC,CAAC,CAAC,GAAG,aAAa,EAC3E,OAAO,EAAE,mBAAmB,GAC1B,YAAY,CAAC,CAAC,CAAC,CAmDjB"}
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
- if (!isObject(manifest) || !isStringList(manifest.locales) || manifest.locales.length === 0 || typeof manifest.fallbackLocale !== "string" || !isObject(manifest.emails)) {
61
- throw new Error(`createMailRenderer: ${file} is not a manifest of @nxgt/mail-i18n — build with its i18n() plugin`);
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 this version does not read — rebuild with the same version of @nxgt/mail-i18n`);
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=74229904C7DC09F264756E2164756E21
180
+ //# debugId=3F107BDB6620800264756E2164756E21
169
181
  //# sourceMappingURL=renderer.js.map
@@ -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'&': '&amp;',\n\t'<': '&lt;',\n\t'>': '&gt;',\n\t'\"': '&quot;',\n\t\"'\": '&#39;',\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'&': '&amp;',\n\t'<': '&lt;',\n\t'>': '&gt;',\n\t'\"': '&quot;',\n\t\"'\": '&#39;',\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": "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",
8
- "debugId": "74229904C7DC09F264756E2164756E21",
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` |
@@ -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 this version does not read — rebuild with the same version of @nxgt/mail-i18n` | `Error` | An entry of the manifest lacks a field, or a locale — usually a build made with another version |
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
- - **The conformance suite checks the idempotency key** — a fourteenth case,
10
- `send.idempotencyKey`: a message with a key is delivered, never refused
11
- for it, and the key is written nowhere in the e-mail. Built, not yet
12
- published.
13
- - **`listUnsubscribe` writes the URL a parser reads** — `new URL(url).href`,
14
- so the value written is the value checked, and a `%` that starts no escape
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).
@@ -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 describes <email> in a shape this version does not read — rebuild with the same version of @nxgt/mail-i18n`](#createmailrenderer-mail-manifestjson-describes-email-in-a-shape-this-version-does-not-read--rebuild-with-the-same-version-of-nxgtmail-i18n)
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: typically a `mail-manifest.json` written by something else.
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 describes <email> in a shape this version does not read — rebuild with the same version of @nxgt/mail-i18n`
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:** the build and the server use versions of `@nxgt/mail-i18n` and
1255
- `@nxgt/mail` that do not agree on the manifest: a build committed or cached
1256
- from an older version, read by a newer server, or the reverse.
1257
- **Fix:** upgrade `@nxgt/mail-i18n` and `@nxgt/mail` together, then run
1258
- `maizzle build` again and deploy its output. If both are current and the
1259
- build is fresh, it is a [bug in this package](#a-bug-in-nxgtmail-itself).
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`. `defineMailConfig` sets
1272
- `plaintext: true`; keep it, then run `maizzle build` again.
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.0",
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",