@casomoltd/tooling 0.34.2 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/buttondown/engine.mjs +42 -11
- package/package.json +1 -1
package/buttondown/engine.mjs
CHANGED
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
* },
|
|
28
28
|
* };
|
|
29
29
|
*
|
|
30
|
-
* `newsletter.json` holds the newsletter's short fields
|
|
30
|
+
* `newsletter.json` holds the newsletter's short fields (its `image` is
|
|
31
|
+
* also every draft's default share image), `tags.json` its
|
|
31
32
|
* tags, and each file in `fields/` is the field it is named after: a
|
|
32
33
|
* stylesheet is read verbatim, and an `.mjs` file's default export is the
|
|
33
34
|
* field's value.
|
|
@@ -180,15 +181,7 @@ export async function tokenReader(file) {
|
|
|
180
181
|
|
|
181
182
|
/** Every field the newsletter is set to: newsletter.json, then fields/. */
|
|
182
183
|
export async function buildPayload(settings) {
|
|
183
|
-
const payload =
|
|
184
|
-
await readFile(path.join(settings.dir, "newsletter.json"), "utf8"),
|
|
185
|
-
);
|
|
186
|
-
for (const field of IMAGE_FIELDS) {
|
|
187
|
-
if (payload[field] && !URL.canParse(payload[field])) {
|
|
188
|
-
throw new Error(`${field} must be an absolute URL, got `
|
|
189
|
-
+ JSON.stringify(payload[field]));
|
|
190
|
-
}
|
|
191
|
-
}
|
|
184
|
+
const payload = await readNewsletter(settings);
|
|
192
185
|
const identity = IDENTITY.filter((key) => key in payload);
|
|
193
186
|
if (identity.length) {
|
|
194
187
|
throw new Error(
|
|
@@ -570,7 +563,8 @@ export async function apply(settings, api, log = console.log) {
|
|
|
570
563
|
* A draft from a markdown file whose first line, after any front matter,
|
|
571
564
|
* is `# Subject`. Front matter carries the email's id once it exists, so a
|
|
572
565
|
* re-run replaces that draft rather than creating a second; `slug` and
|
|
573
|
-
* `description` are passed through
|
|
566
|
+
* `description` are passed through, and `image` (the share image) defaults
|
|
567
|
+
* to the newsletter's own. An email already sent is refused:
|
|
574
568
|
* replacing it would rewrite the published archive.
|
|
575
569
|
*/
|
|
576
570
|
export async function draft(settings, api, file, log = console.log) {
|
|
@@ -591,6 +585,10 @@ export async function draft(settings, api, file, log = console.log) {
|
|
|
591
585
|
throw new Error("front matter has an empty `id:`; delete the line to "
|
|
592
586
|
+ "create a draft");
|
|
593
587
|
}
|
|
588
|
+
if ("image" in fm && !fm.image) {
|
|
589
|
+
throw new Error("front matter has an empty `image:`; delete the line "
|
|
590
|
+
+ "to use the newsletter's share image");
|
|
591
|
+
}
|
|
594
592
|
// The marker says the body is markdown (Buttondown's `plaintext` mode).
|
|
595
593
|
// Without it Buttondown guesses, and a draft once stored as rich text
|
|
596
594
|
// takes the next PATCH as rich text too.
|
|
@@ -601,6 +599,15 @@ export async function draft(settings, api, file, log = console.log) {
|
|
|
601
599
|
...Object.fromEntries(["slug", "description"]
|
|
602
600
|
.filter((k) => k in fm).map((k) => [k, fm[k]])),
|
|
603
601
|
};
|
|
602
|
+
// Left unset, Buttondown takes the post's first picture as its share
|
|
603
|
+
// image, so every post would preview differently; the newsletter's own
|
|
604
|
+
// share image is the default, and `image:` in the front matter wins.
|
|
605
|
+
const image = "image" in fm
|
|
606
|
+
? fm.image : (await readNewsletter(settings)).image;
|
|
607
|
+
if (image) {
|
|
608
|
+
assertAbsoluteUrl("image", image);
|
|
609
|
+
body.image = image;
|
|
610
|
+
}
|
|
604
611
|
if (fm.id) {
|
|
605
612
|
const current = await api.call("GET", `/emails/${fm.id}`);
|
|
606
613
|
if (!current.ok) throw new Error(`GET email: ${detail(current)}`);
|
|
@@ -686,6 +693,30 @@ export function stripComments(text) {
|
|
|
686
693
|
}
|
|
687
694
|
}
|
|
688
695
|
|
|
696
|
+
/** Throws unless an image field holds an absolute URL, since Buttondown
|
|
697
|
+
* stores a relative one as given and the image then never loads. */
|
|
698
|
+
function assertAbsoluteUrl(field, value) {
|
|
699
|
+
if (!URL.canParse(value)) {
|
|
700
|
+
throw new Error(`${field} must be an absolute URL, got `
|
|
701
|
+
+ JSON.stringify(value));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** newsletter.json, parsed and with its image fields checked. */
|
|
706
|
+
async function readNewsletter(settings) {
|
|
707
|
+
const file = path.join(settings.dir, "newsletter.json");
|
|
708
|
+
let payload;
|
|
709
|
+
try {
|
|
710
|
+
payload = JSON.parse(await readFile(file, "utf8"));
|
|
711
|
+
} catch (error) {
|
|
712
|
+
throw new Error(`${file}: ${error.message}`, {cause: error});
|
|
713
|
+
}
|
|
714
|
+
for (const field of IMAGE_FIELDS) {
|
|
715
|
+
if (payload[field]) assertAbsoluteUrl(field, payload[field]);
|
|
716
|
+
}
|
|
717
|
+
return payload;
|
|
718
|
+
}
|
|
719
|
+
|
|
689
720
|
/** Why a call failed: Buttondown's `detail`, else its whole body, which
|
|
690
721
|
* names the fields it refused, else the status. */
|
|
691
722
|
function detail(res) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@casomoltd/tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "Casomo's shared dev config: ESLint, Prettier, commitlint, knip & jscpd presets and dev CLIs (git hooks, readability, screenshots, design specs)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|