shipseal 0.0.7 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +1441 -101
- package/dist/cli.js.map +1 -1
- package/dist/studio-client.js +242 -0
- package/dist/studio-client.js.map +1 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { basename, dirname, extname, isAbsolute, join, relative } from "node:path";
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { cac } from "cac";
|
|
5
|
-
import { appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import { appendFile, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
-
import { createHash } from "node:crypto";
|
|
8
7
|
import { execFile } from "node:child_process";
|
|
9
8
|
import { promisify } from "node:util";
|
|
10
9
|
import { fileURLToPath } from "node:url";
|
|
@@ -14,6 +13,8 @@ import { fromJsx } from "takumi-js/helpers/jsx";
|
|
|
14
13
|
import { createInterface } from "node:readline/promises";
|
|
15
14
|
import { stdin, stdout } from "node:process";
|
|
16
15
|
import { inflateSync } from "node:zlib";
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
import { createServer } from "node:http";
|
|
17
18
|
|
|
18
19
|
//#region src/core/errors.ts
|
|
19
20
|
var ShipsealError = class extends Error {
|
|
@@ -62,7 +63,11 @@ const brandSchema = z.object({
|
|
|
62
63
|
}),
|
|
63
64
|
radius: z.number().nonnegative(),
|
|
64
65
|
theme: z.enum(["dark", "light"]),
|
|
65
|
-
style: z.
|
|
66
|
+
style: z.enum([
|
|
67
|
+
"minimal",
|
|
68
|
+
"editorial",
|
|
69
|
+
"terminal"
|
|
70
|
+
]),
|
|
66
71
|
tokens: z.string().nullable()
|
|
67
72
|
});
|
|
68
73
|
const DEFAULT_BRAND_COLORS = {
|
|
@@ -163,6 +168,12 @@ const FORMATS = {
|
|
|
163
168
|
safeZone: LANDSCAPE_SAFE_ZONE
|
|
164
169
|
}
|
|
165
170
|
};
|
|
171
|
+
const V1_FORMAT_IDS = [
|
|
172
|
+
"og",
|
|
173
|
+
"github-social",
|
|
174
|
+
"x",
|
|
175
|
+
"linkedin"
|
|
176
|
+
];
|
|
166
177
|
|
|
167
178
|
//#endregion
|
|
168
179
|
//#region src/config/schema.ts
|
|
@@ -175,7 +186,19 @@ const configSchema = z.object({
|
|
|
175
186
|
templates: z.array(z.string()).optional(),
|
|
176
187
|
maxHighlights: z.number().int().positive().optional(),
|
|
177
188
|
changelogPath: z.string().optional(),
|
|
178
|
-
snippet: z.string().nullable().optional()
|
|
189
|
+
snippet: z.string().nullable().optional(),
|
|
190
|
+
announce: z.enum([
|
|
191
|
+
"major",
|
|
192
|
+
"minor",
|
|
193
|
+
"patch"
|
|
194
|
+
]).optional(),
|
|
195
|
+
headline: z.string().nullable().optional(),
|
|
196
|
+
subheadline: z.string().nullable().optional(),
|
|
197
|
+
story: z.object({
|
|
198
|
+
upgrade: z.string().max(4e3).optional(),
|
|
199
|
+
before: z.string().optional(),
|
|
200
|
+
after: z.string().optional()
|
|
201
|
+
}).refine((value) => Boolean(value.before) === Boolean(value.after), { message: "Supply both story.before and story.after screenshot paths, or neither." }).optional()
|
|
179
202
|
}).optional(),
|
|
180
203
|
milestones: z.object({
|
|
181
204
|
stars: z.array(z.number().int().positive()).optional(),
|
|
@@ -213,6 +236,7 @@ const DEFAULT_CONFIG = {
|
|
|
213
236
|
],
|
|
214
237
|
maxHighlights: 4,
|
|
215
238
|
changelogPath: "CHANGELOG.md",
|
|
239
|
+
announce: "patch",
|
|
216
240
|
snippet: null
|
|
217
241
|
},
|
|
218
242
|
milestones: {
|
|
@@ -301,18 +325,16 @@ const COPY_LIMITS = {
|
|
|
301
325
|
|
|
302
326
|
//#endregion
|
|
303
327
|
//#region src/copy/deterministic.ts
|
|
304
|
-
function deterministicCopy(facts, maxHighlights = 4) {
|
|
328
|
+
function deterministicCopy(facts, maxHighlights = 4, displayName) {
|
|
305
329
|
const name = facts.project.name.value;
|
|
306
330
|
const version = facts.release?.version.value;
|
|
307
331
|
const features = (facts.release?.features ?? []).map((item) => item.value);
|
|
308
332
|
const fixes = (facts.release?.fixes ?? []).map((item) => item.value);
|
|
309
333
|
const breaking = (facts.release?.breaking ?? []).map((item) => item.value);
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
].map((line) => cleanLine(firstSentence(line))).find((line) => line.length <= HEADLINE_MAX_CHARS) ?? (version === void 0 ? name : `${name} ${version}`);
|
|
315
|
-
const tagline = facts.project.tagline?.value;
|
|
334
|
+
const headlineSource = [...breaking, ...features].map((line) => cleanLine(firstSentence(line))).find((line) => line.length <= HEADLINE_MAX_CHARS);
|
|
335
|
+
const title = displayName ?? name;
|
|
336
|
+
const headline = facts.release?.headline?.value ?? headlineSource ?? (version === void 0 ? title : `${title} ${version}`);
|
|
337
|
+
const tagline = facts.release?.subheadline?.value ?? facts.project.tagline?.value;
|
|
316
338
|
const subheadline = tagline !== void 0 && tagline.length > 0 ? cleanLine(tagline) : cleanLine(features[1] ?? fixes[1] ?? fixes[0] ?? `What's new in ${version ?? name}`);
|
|
317
339
|
const highlights = [];
|
|
318
340
|
const addHighlight = (line, prefix = "") => {
|
|
@@ -396,7 +418,7 @@ function cleanLine(text) {
|
|
|
396
418
|
}
|
|
397
419
|
function ctaFor(facts) {
|
|
398
420
|
const npm = facts.project.npmPackage?.value;
|
|
399
|
-
if (npm !== void 0 && npm.length > 0) return `npm i ${npm}`;
|
|
421
|
+
if (npm !== void 0 && npm.length > 0) return facts.project.cli?.value === true ? `npx ${npm}@latest` : `npm i ${npm}`;
|
|
400
422
|
const url = facts.project.url?.value ?? facts.project.repo?.value;
|
|
401
423
|
if (url === void 0) return facts.project.name.value;
|
|
402
424
|
return url.replace(/^https?:\/\//, "");
|
|
@@ -551,7 +573,9 @@ const jsxs = jsx;
|
|
|
551
573
|
|
|
552
574
|
//#endregion
|
|
553
575
|
//#region src/templates/code-card.tsx
|
|
554
|
-
|
|
576
|
+
/** Advance width of a monospace glyph as a fraction of the font size. */
|
|
577
|
+
const MONO_ADVANCE = .6;
|
|
578
|
+
const propsSchema$5 = z.object({
|
|
555
579
|
headline: z.string(),
|
|
556
580
|
headingFamily: z.string(),
|
|
557
581
|
monoFamily: z.string(),
|
|
@@ -567,7 +591,7 @@ const codeCard = {
|
|
|
567
591
|
id: "code-card",
|
|
568
592
|
events: ["release"],
|
|
569
593
|
formats: ["x", "linkedin"],
|
|
570
|
-
propsSchema: propsSchema$
|
|
594
|
+
propsSchema: propsSchema$5,
|
|
571
595
|
slots(format) {
|
|
572
596
|
return { headline: {
|
|
573
597
|
maxLines: 1,
|
|
@@ -578,10 +602,10 @@ const codeCard = {
|
|
|
578
602
|
} };
|
|
579
603
|
},
|
|
580
604
|
slotText(raw) {
|
|
581
|
-
return { headline: propsSchema$
|
|
605
|
+
return { headline: propsSchema$5.parse(raw).headline };
|
|
582
606
|
},
|
|
583
607
|
slotFont(raw, slot) {
|
|
584
|
-
const props = propsSchema$
|
|
608
|
+
const props = propsSchema$5.parse(raw);
|
|
585
609
|
if (slot === "headline") return {
|
|
586
610
|
family: props.headingFamily,
|
|
587
611
|
weight: props.headingWeight,
|
|
@@ -615,10 +639,19 @@ const codeCard = {
|
|
|
615
639
|
};
|
|
616
640
|
},
|
|
617
641
|
render(raw, ctx) {
|
|
618
|
-
const props = propsSchema$
|
|
642
|
+
const props = propsSchema$5.parse(raw);
|
|
619
643
|
const colors = themeColors(ctx.brand, ctx.theme);
|
|
620
644
|
const pad = ctx.format.safeZone;
|
|
621
645
|
const headline = ctx.fitted.headline;
|
|
646
|
+
const boxPad = 28;
|
|
647
|
+
const lineHeight = 1.35;
|
|
648
|
+
const lines = props.lines.length === 0 ? 1 : props.lines.length;
|
|
649
|
+
const longest = Math.max(1, ...props.lines.map((line) => line.reduce((total, token) => total + token.text.length, 0)));
|
|
650
|
+
const headlineHeight = (headline?.fontSize ?? 36) * 1.25;
|
|
651
|
+
const attributionHeight = ctx.attribution ? 49.2 : 0;
|
|
652
|
+
const innerWidth = ctx.format.width - 2 * pad - 56;
|
|
653
|
+
const innerHeight = ctx.format.height - 2 * pad - headlineHeight - 24 - attributionHeight - 56;
|
|
654
|
+
const codeSize = Math.max(18, Math.min(44, Math.floor(Math.min(innerWidth / (longest * MONO_ADVANCE), innerHeight / (lines * lineHeight)))));
|
|
622
655
|
return /* @__PURE__ */ jsxs("div", {
|
|
623
656
|
style: {
|
|
624
657
|
display: "flex",
|
|
@@ -644,29 +677,36 @@ const codeCard = {
|
|
|
644
677
|
display: "flex",
|
|
645
678
|
flexDirection: "column",
|
|
646
679
|
flexGrow: 1,
|
|
647
|
-
|
|
648
|
-
borderRadius: props.radius,
|
|
649
|
-
padding: 28,
|
|
650
|
-
gap: 0
|
|
680
|
+
justifyContent: "center"
|
|
651
681
|
},
|
|
652
|
-
children:
|
|
682
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
653
683
|
style: {
|
|
654
684
|
display: "flex",
|
|
655
|
-
flexDirection: "
|
|
656
|
-
|
|
685
|
+
flexDirection: "column",
|
|
686
|
+
backgroundColor: colors.card,
|
|
687
|
+
borderRadius: props.radius,
|
|
688
|
+
padding: boxPad,
|
|
689
|
+
gap: 0
|
|
657
690
|
},
|
|
658
|
-
children:
|
|
691
|
+
children: props.lines.map((line, lineIndex) => /* @__PURE__ */ jsx("div", {
|
|
659
692
|
style: {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
color: token.color,
|
|
664
|
-
whiteSpace: "pre",
|
|
665
|
-
lineHeight: 1.35
|
|
693
|
+
display: "flex",
|
|
694
|
+
flexDirection: "row",
|
|
695
|
+
flexWrap: "nowrap"
|
|
666
696
|
},
|
|
667
|
-
children: token
|
|
668
|
-
|
|
669
|
-
|
|
697
|
+
children: line.map((token, tokenIndex) => /* @__PURE__ */ jsx("div", {
|
|
698
|
+
style: {
|
|
699
|
+
fontFamily: props.monoFamily,
|
|
700
|
+
fontWeight: props.monoWeight,
|
|
701
|
+
fontSize: codeSize,
|
|
702
|
+
color: token.color,
|
|
703
|
+
whiteSpace: "pre",
|
|
704
|
+
lineHeight
|
|
705
|
+
},
|
|
706
|
+
children: token.text
|
|
707
|
+
}, `${String(lineIndex)}-${String(tokenIndex)}`))
|
|
708
|
+
}, String(lineIndex)))
|
|
709
|
+
})
|
|
670
710
|
}),
|
|
671
711
|
ctx.attribution ? /* @__PURE__ */ jsx("div", {
|
|
672
712
|
style: {
|
|
@@ -735,7 +775,7 @@ const rowSchema = z.object({
|
|
|
735
775
|
changeText: z.string(),
|
|
736
776
|
regression: z.boolean()
|
|
737
777
|
});
|
|
738
|
-
const propsSchema$
|
|
778
|
+
const propsSchema$4 = z.object({
|
|
739
779
|
title: z.string(),
|
|
740
780
|
note: z.string(),
|
|
741
781
|
rows: z.array(rowSchema),
|
|
@@ -749,7 +789,7 @@ const bench = {
|
|
|
749
789
|
id: "bench",
|
|
750
790
|
events: ["bench"],
|
|
751
791
|
formats: ["x", "linkedin"],
|
|
752
|
-
propsSchema: propsSchema$
|
|
792
|
+
propsSchema: propsSchema$4,
|
|
753
793
|
slots(format) {
|
|
754
794
|
const width = format.width - 2 * format.safeZone;
|
|
755
795
|
const slots = { title: {
|
|
@@ -769,7 +809,7 @@ const bench = {
|
|
|
769
809
|
return slots;
|
|
770
810
|
},
|
|
771
811
|
slotText(raw) {
|
|
772
|
-
const props = propsSchema$
|
|
812
|
+
const props = propsSchema$4.parse(raw);
|
|
773
813
|
const text = { title: props.title };
|
|
774
814
|
props.rows.forEach((row, index) => {
|
|
775
815
|
text[`change-${String(index)}`] = row.changeText;
|
|
@@ -777,7 +817,7 @@ const bench = {
|
|
|
777
817
|
return text;
|
|
778
818
|
},
|
|
779
819
|
slotFont(raw, slot) {
|
|
780
|
-
const props = propsSchema$
|
|
820
|
+
const props = propsSchema$4.parse(raw);
|
|
781
821
|
if (slot === "title") return {
|
|
782
822
|
family: props.headingFamily,
|
|
783
823
|
weight: props.headingWeight,
|
|
@@ -820,7 +860,7 @@ const bench = {
|
|
|
820
860
|
};
|
|
821
861
|
},
|
|
822
862
|
render(raw, ctx) {
|
|
823
|
-
const props = propsSchema$
|
|
863
|
+
const props = propsSchema$4.parse(raw);
|
|
824
864
|
const colors = themeColors(ctx.brand, ctx.theme);
|
|
825
865
|
const pad = ctx.format.safeZone;
|
|
826
866
|
const title = ctx.fitted.title;
|
|
@@ -952,7 +992,7 @@ function formatMetric(value) {
|
|
|
952
992
|
|
|
953
993
|
//#endregion
|
|
954
994
|
//#region src/templates/milestone.tsx
|
|
955
|
-
const propsSchema$
|
|
995
|
+
const propsSchema$3 = z.object({
|
|
956
996
|
name: z.string(),
|
|
957
997
|
numberText: z.string(),
|
|
958
998
|
metricLabel: z.string(),
|
|
@@ -972,7 +1012,7 @@ const milestone = {
|
|
|
972
1012
|
"x",
|
|
973
1013
|
"linkedin"
|
|
974
1014
|
],
|
|
975
|
-
propsSchema: propsSchema$
|
|
1015
|
+
propsSchema: propsSchema$3,
|
|
976
1016
|
slots(format) {
|
|
977
1017
|
const width = format.width - 2 * format.safeZone;
|
|
978
1018
|
return {
|
|
@@ -993,14 +1033,14 @@ const milestone = {
|
|
|
993
1033
|
};
|
|
994
1034
|
},
|
|
995
1035
|
slotText(raw) {
|
|
996
|
-
const props = propsSchema$
|
|
1036
|
+
const props = propsSchema$3.parse(raw);
|
|
997
1037
|
return {
|
|
998
1038
|
number: props.numberText,
|
|
999
1039
|
thankYou: props.thankYou
|
|
1000
1040
|
};
|
|
1001
1041
|
},
|
|
1002
1042
|
slotFont(raw, slot) {
|
|
1003
|
-
const props = propsSchema$
|
|
1043
|
+
const props = propsSchema$3.parse(raw);
|
|
1004
1044
|
if (slot === "number") return {
|
|
1005
1045
|
family: props.headingFamily,
|
|
1006
1046
|
weight: props.headingWeight,
|
|
@@ -1044,7 +1084,7 @@ const milestone = {
|
|
|
1044
1084
|
};
|
|
1045
1085
|
},
|
|
1046
1086
|
render(raw, ctx) {
|
|
1047
|
-
const props = propsSchema$
|
|
1087
|
+
const props = propsSchema$3.parse(raw);
|
|
1048
1088
|
const colors = themeColors(ctx.brand, ctx.theme);
|
|
1049
1089
|
const pad = ctx.format.safeZone;
|
|
1050
1090
|
const number = ctx.fitted.number;
|
|
@@ -1138,9 +1178,39 @@ const milestone = {
|
|
|
1138
1178
|
}
|
|
1139
1179
|
};
|
|
1140
1180
|
|
|
1181
|
+
//#endregion
|
|
1182
|
+
//#region src/templates/primitives/seal.ts
|
|
1183
|
+
/**
|
|
1184
|
+
* The core promise, made visible on the card.
|
|
1185
|
+
*
|
|
1186
|
+
* "sealed · 7 commits · 2026-09-11" turns "every number has a source" from a claim on the
|
|
1187
|
+
* website into a mark in the feed. Every segment is a fact, so rule 1 holds: a missing fact
|
|
1188
|
+
* drops its segment and is reported, it never becomes a placeholder or an estimate.
|
|
1189
|
+
*/
|
|
1190
|
+
function sealStamp(facts) {
|
|
1191
|
+
const segments = ["sealed"];
|
|
1192
|
+
const missing = [];
|
|
1193
|
+
const commits = facts.release?.commitCount?.value;
|
|
1194
|
+
if (commits === void 0) missing.push({
|
|
1195
|
+
fact: "release.commitCount",
|
|
1196
|
+
effect: "seal stamp omits the commit count"
|
|
1197
|
+
});
|
|
1198
|
+
else segments.push(`${formatCount$1(commits)} ${commits === 1 ? "commit" : "commits"}`);
|
|
1199
|
+
const date = facts.release?.date?.value;
|
|
1200
|
+
if (date === void 0) missing.push({
|
|
1201
|
+
fact: "release.date",
|
|
1202
|
+
effect: "seal stamp omits the date"
|
|
1203
|
+
});
|
|
1204
|
+
else segments.push(date);
|
|
1205
|
+
return {
|
|
1206
|
+
segments,
|
|
1207
|
+
missing
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1141
1211
|
//#endregion
|
|
1142
1212
|
//#region src/templates/release-hero.tsx
|
|
1143
|
-
const propsSchema$
|
|
1213
|
+
const propsSchema$2 = z.object({
|
|
1144
1214
|
name: z.string(),
|
|
1145
1215
|
version: z.string(),
|
|
1146
1216
|
headline: z.string(),
|
|
@@ -1152,7 +1222,8 @@ const propsSchema$1 = z.object({
|
|
|
1152
1222
|
bodyWeight: z.number(),
|
|
1153
1223
|
radius: z.number(),
|
|
1154
1224
|
showCta: z.boolean(),
|
|
1155
|
-
showLogo: z.boolean()
|
|
1225
|
+
showLogo: z.boolean(),
|
|
1226
|
+
seal: z.array(z.string())
|
|
1156
1227
|
});
|
|
1157
1228
|
const releaseHero = {
|
|
1158
1229
|
id: "release-hero",
|
|
@@ -1163,7 +1234,7 @@ const releaseHero = {
|
|
|
1163
1234
|
"x",
|
|
1164
1235
|
"linkedin"
|
|
1165
1236
|
],
|
|
1166
|
-
propsSchema: propsSchema$
|
|
1237
|
+
propsSchema: propsSchema$2,
|
|
1167
1238
|
slots(format) {
|
|
1168
1239
|
const width = format.width - 2 * format.safeZone;
|
|
1169
1240
|
return {
|
|
@@ -1191,7 +1262,7 @@ const releaseHero = {
|
|
|
1191
1262
|
};
|
|
1192
1263
|
},
|
|
1193
1264
|
slotText(raw) {
|
|
1194
|
-
const props = propsSchema$
|
|
1265
|
+
const props = propsSchema$2.parse(raw);
|
|
1195
1266
|
return {
|
|
1196
1267
|
headline: props.headline,
|
|
1197
1268
|
subheadline: props.subheadline,
|
|
@@ -1199,7 +1270,7 @@ const releaseHero = {
|
|
|
1199
1270
|
};
|
|
1200
1271
|
},
|
|
1201
1272
|
slotFont(raw, slot) {
|
|
1202
|
-
const props = propsSchema$
|
|
1273
|
+
const props = propsSchema$2.parse(raw);
|
|
1203
1274
|
if (slot === "headline") return {
|
|
1204
1275
|
family: props.headingFamily,
|
|
1205
1276
|
weight: props.headingWeight,
|
|
@@ -1222,6 +1293,8 @@ const releaseHero = {
|
|
|
1222
1293
|
fact: "brand.logo",
|
|
1223
1294
|
effect: "logo hidden"
|
|
1224
1295
|
});
|
|
1296
|
+
const seal = sealStamp(facts);
|
|
1297
|
+
missing.push(...seal.missing);
|
|
1225
1298
|
const version = facts.release?.version.value ?? "";
|
|
1226
1299
|
return {
|
|
1227
1300
|
props: {
|
|
@@ -1236,13 +1309,14 @@ const releaseHero = {
|
|
|
1236
1309
|
bodyWeight: brand.fonts.body.weight,
|
|
1237
1310
|
radius: brand.radius,
|
|
1238
1311
|
showCta: copy.cta.length > 0,
|
|
1312
|
+
seal: seal.segments,
|
|
1239
1313
|
showLogo
|
|
1240
1314
|
},
|
|
1241
1315
|
missing
|
|
1242
1316
|
};
|
|
1243
1317
|
},
|
|
1244
1318
|
render(raw, ctx) {
|
|
1245
|
-
const props = propsSchema$
|
|
1319
|
+
const props = propsSchema$2.parse(raw);
|
|
1246
1320
|
const colors = themeColors(ctx.brand, ctx.theme);
|
|
1247
1321
|
const pad = ctx.format.safeZone;
|
|
1248
1322
|
const headline = ctx.fitted.headline;
|
|
@@ -1257,9 +1331,24 @@ const releaseHero = {
|
|
|
1257
1331
|
width: "100%",
|
|
1258
1332
|
height: "100%",
|
|
1259
1333
|
backgroundColor: colors.background,
|
|
1260
|
-
padding: pad
|
|
1334
|
+
padding: pad,
|
|
1335
|
+
position: "relative"
|
|
1261
1336
|
},
|
|
1262
1337
|
children: [
|
|
1338
|
+
props.showLogo && ctx.logoSrc !== void 0 ? /* @__PURE__ */ jsx("div", {
|
|
1339
|
+
style: {
|
|
1340
|
+
display: "flex",
|
|
1341
|
+
position: "absolute",
|
|
1342
|
+
right: -Math.round(ctx.format.height * .12),
|
|
1343
|
+
top: Math.round(ctx.format.height * .18),
|
|
1344
|
+
opacity: .06
|
|
1345
|
+
},
|
|
1346
|
+
children: /* @__PURE__ */ jsx("img", {
|
|
1347
|
+
src: ctx.logoSrc,
|
|
1348
|
+
width: Math.round(ctx.format.height * .72),
|
|
1349
|
+
height: Math.round(ctx.format.height * .72)
|
|
1350
|
+
})
|
|
1351
|
+
}) : void 0,
|
|
1263
1352
|
/* @__PURE__ */ jsxs("div", {
|
|
1264
1353
|
style: {
|
|
1265
1354
|
display: "flex",
|
|
@@ -1301,7 +1390,7 @@ const releaseHero = {
|
|
|
1301
1390
|
fontFamily: props.bodyFamily,
|
|
1302
1391
|
fontWeight: props.bodyWeight,
|
|
1303
1392
|
fontSize: 22,
|
|
1304
|
-
color: colors.
|
|
1393
|
+
color: colors.foreground
|
|
1305
1394
|
},
|
|
1306
1395
|
children: `v${props.version}`
|
|
1307
1396
|
})
|
|
@@ -1360,15 +1449,31 @@ const releaseHero = {
|
|
|
1360
1449
|
},
|
|
1361
1450
|
children: cta?.text ?? props.cta
|
|
1362
1451
|
})
|
|
1363
|
-
}) : /* @__PURE__ */ jsx("div", {}),
|
|
1452
|
+
}) : /* @__PURE__ */ jsx("div", {}), /* @__PURE__ */ jsxs("div", {
|
|
1364
1453
|
style: {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1454
|
+
display: "flex",
|
|
1455
|
+
flexDirection: "column",
|
|
1456
|
+
alignItems: "flex-end",
|
|
1457
|
+
gap: 6
|
|
1369
1458
|
},
|
|
1370
|
-
children:
|
|
1371
|
-
|
|
1459
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1460
|
+
style: {
|
|
1461
|
+
fontFamily: props.bodyFamily,
|
|
1462
|
+
fontWeight: props.headingWeight,
|
|
1463
|
+
fontSize: 20,
|
|
1464
|
+
color: colors.primary
|
|
1465
|
+
},
|
|
1466
|
+
children: props.seal.join(" · ")
|
|
1467
|
+
}), ctx.attribution ? /* @__PURE__ */ jsx("div", {
|
|
1468
|
+
style: {
|
|
1469
|
+
fontFamily: props.bodyFamily,
|
|
1470
|
+
fontWeight: props.bodyWeight,
|
|
1471
|
+
fontSize: 18,
|
|
1472
|
+
color: colors.muted
|
|
1473
|
+
},
|
|
1474
|
+
children: "made with shipseal.dev"
|
|
1475
|
+
}) : void 0]
|
|
1476
|
+
})]
|
|
1372
1477
|
})
|
|
1373
1478
|
]
|
|
1374
1479
|
});
|
|
@@ -1377,7 +1482,7 @@ const releaseHero = {
|
|
|
1377
1482
|
|
|
1378
1483
|
//#endregion
|
|
1379
1484
|
//#region src/templates/release-highlights.tsx
|
|
1380
|
-
const propsSchema = z.object({
|
|
1485
|
+
const propsSchema$1 = z.object({
|
|
1381
1486
|
name: z.string(),
|
|
1382
1487
|
version: z.string(),
|
|
1383
1488
|
highlights: z.array(z.string()),
|
|
@@ -1391,7 +1496,7 @@ const releaseHighlights = {
|
|
|
1391
1496
|
id: "release-highlights",
|
|
1392
1497
|
events: ["release"],
|
|
1393
1498
|
formats: ["x", "linkedin"],
|
|
1394
|
-
propsSchema,
|
|
1499
|
+
propsSchema: propsSchema$1,
|
|
1395
1500
|
slots(format) {
|
|
1396
1501
|
const width = format.width - 2 * format.safeZone - 48;
|
|
1397
1502
|
const slots = {};
|
|
@@ -1405,7 +1510,7 @@ const releaseHighlights = {
|
|
|
1405
1510
|
return slots;
|
|
1406
1511
|
},
|
|
1407
1512
|
slotText(raw) {
|
|
1408
|
-
const props = propsSchema.parse(raw);
|
|
1513
|
+
const props = propsSchema$1.parse(raw);
|
|
1409
1514
|
const text = {};
|
|
1410
1515
|
props.highlights.forEach((line, index) => {
|
|
1411
1516
|
text[`highlight-${String(index)}`] = line;
|
|
@@ -1413,7 +1518,7 @@ const releaseHighlights = {
|
|
|
1413
1518
|
return text;
|
|
1414
1519
|
},
|
|
1415
1520
|
slotFont(raw) {
|
|
1416
|
-
const props = propsSchema.parse(raw);
|
|
1521
|
+
const props = propsSchema$1.parse(raw);
|
|
1417
1522
|
return {
|
|
1418
1523
|
family: props.bodyFamily,
|
|
1419
1524
|
weight: props.bodyWeight,
|
|
@@ -1441,7 +1546,7 @@ const releaseHighlights = {
|
|
|
1441
1546
|
};
|
|
1442
1547
|
},
|
|
1443
1548
|
render(raw, ctx) {
|
|
1444
|
-
const props = propsSchema.parse(raw);
|
|
1549
|
+
const props = propsSchema$1.parse(raw);
|
|
1445
1550
|
const colors = themeColors(ctx.brand, ctx.theme);
|
|
1446
1551
|
const pad = ctx.format.safeZone;
|
|
1447
1552
|
const title = props.version.length > 0 ? `What's new in v${props.version}` : "What's new";
|
|
@@ -1535,6 +1640,404 @@ const releaseHighlights = {
|
|
|
1535
1640
|
}
|
|
1536
1641
|
};
|
|
1537
1642
|
|
|
1643
|
+
//#endregion
|
|
1644
|
+
//#region src/facts/schema.ts
|
|
1645
|
+
const SOURCE_IDS = [
|
|
1646
|
+
"git",
|
|
1647
|
+
"package-json",
|
|
1648
|
+
"readme",
|
|
1649
|
+
"changelog",
|
|
1650
|
+
"github-api",
|
|
1651
|
+
"npm-api",
|
|
1652
|
+
"bench-file",
|
|
1653
|
+
"user-config",
|
|
1654
|
+
"brand"
|
|
1655
|
+
];
|
|
1656
|
+
const sourceIdSchema = z.enum(SOURCE_IDS);
|
|
1657
|
+
const provenanceSchema = z.object({
|
|
1658
|
+
source: sourceIdSchema,
|
|
1659
|
+
ref: z.string().min(1),
|
|
1660
|
+
fetchedAt: z.iso.datetime()
|
|
1661
|
+
});
|
|
1662
|
+
function factSchema(valueSchema) {
|
|
1663
|
+
return z.object({
|
|
1664
|
+
value: valueSchema,
|
|
1665
|
+
provenance: provenanceSchema
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
const stringFact = factSchema(z.string());
|
|
1669
|
+
const numberFact = factSchema(z.number());
|
|
1670
|
+
const stringListFact = factSchema(z.array(z.string()));
|
|
1671
|
+
const codeSnippetFact = factSchema(z.object({
|
|
1672
|
+
code: z.string(),
|
|
1673
|
+
lang: z.string()
|
|
1674
|
+
}));
|
|
1675
|
+
const betterFact = factSchema(z.enum(["lower", "higher"]));
|
|
1676
|
+
const storyPageSchema = z.object({
|
|
1677
|
+
kind: z.enum([
|
|
1678
|
+
"cover",
|
|
1679
|
+
"change",
|
|
1680
|
+
"code",
|
|
1681
|
+
"comparison",
|
|
1682
|
+
"upgrade"
|
|
1683
|
+
]),
|
|
1684
|
+
title: stringFact,
|
|
1685
|
+
body: stringFact,
|
|
1686
|
+
before: stringFact.optional(),
|
|
1687
|
+
after: stringFact.optional()
|
|
1688
|
+
});
|
|
1689
|
+
const factsSchema = z.object({
|
|
1690
|
+
story: z.array(storyPageSchema).optional(),
|
|
1691
|
+
project: z.object({
|
|
1692
|
+
name: stringFact,
|
|
1693
|
+
tagline: stringFact.optional(),
|
|
1694
|
+
url: stringFact.optional(),
|
|
1695
|
+
repo: stringFact.optional(),
|
|
1696
|
+
npmPackage: stringFact.optional(),
|
|
1697
|
+
cli: factSchema(z.boolean()).optional(),
|
|
1698
|
+
license: stringFact.optional()
|
|
1699
|
+
}),
|
|
1700
|
+
release: z.object({
|
|
1701
|
+
version: stringFact,
|
|
1702
|
+
tag: stringFact,
|
|
1703
|
+
kind: factSchema(z.enum([
|
|
1704
|
+
"major",
|
|
1705
|
+
"minor",
|
|
1706
|
+
"patch"
|
|
1707
|
+
])).optional(),
|
|
1708
|
+
headline: stringFact.optional(),
|
|
1709
|
+
subheadline: stringFact.optional(),
|
|
1710
|
+
previousVersion: stringFact.optional(),
|
|
1711
|
+
date: stringFact,
|
|
1712
|
+
features: z.array(stringFact),
|
|
1713
|
+
fixes: z.array(stringFact),
|
|
1714
|
+
breaking: z.array(stringFact),
|
|
1715
|
+
commitCount: numberFact.optional(),
|
|
1716
|
+
contributors: stringListFact.optional(),
|
|
1717
|
+
codeSnippet: codeSnippetFact.optional()
|
|
1718
|
+
}).optional(),
|
|
1719
|
+
metrics: z.object({
|
|
1720
|
+
stars: numberFact.optional(),
|
|
1721
|
+
weeklyDownloads: numberFact.optional(),
|
|
1722
|
+
contributorCount: numberFact.optional()
|
|
1723
|
+
}).optional(),
|
|
1724
|
+
bench: z.object({
|
|
1725
|
+
title: stringFact,
|
|
1726
|
+
metrics: z.array(z.object({
|
|
1727
|
+
label: stringFact,
|
|
1728
|
+
before: numberFact,
|
|
1729
|
+
after: numberFact,
|
|
1730
|
+
unit: stringFact,
|
|
1731
|
+
better: betterFact
|
|
1732
|
+
})),
|
|
1733
|
+
note: stringFact.optional()
|
|
1734
|
+
}).optional(),
|
|
1735
|
+
milestone: z.object({
|
|
1736
|
+
metric: stringFact,
|
|
1737
|
+
threshold: numberFact
|
|
1738
|
+
}).optional()
|
|
1739
|
+
});
|
|
1740
|
+
|
|
1741
|
+
//#endregion
|
|
1742
|
+
//#region src/templates/story-page.tsx
|
|
1743
|
+
const propsSchema = z.object({
|
|
1744
|
+
page: storyPageSchema,
|
|
1745
|
+
name: z.string(),
|
|
1746
|
+
version: z.string(),
|
|
1747
|
+
heading: z.string(),
|
|
1748
|
+
body: z.string(),
|
|
1749
|
+
mono: z.string(),
|
|
1750
|
+
weight: z.number()
|
|
1751
|
+
});
|
|
1752
|
+
const HEADER_HEIGHT = 36;
|
|
1753
|
+
const BODY_PAD = 24;
|
|
1754
|
+
const CODE_LINE_HEIGHT = 1.35;
|
|
1755
|
+
const CODE_MIN_FONT_SIZE = 14;
|
|
1756
|
+
const FOOTER_ALLOWANCE = 144;
|
|
1757
|
+
const TALL_THRESHOLD = 900;
|
|
1758
|
+
function geometry(format) {
|
|
1759
|
+
const tall = format.height > TALL_THRESHOLD;
|
|
1760
|
+
const titleHeight = tall ? 220 : 112;
|
|
1761
|
+
return {
|
|
1762
|
+
tall,
|
|
1763
|
+
titleHeight,
|
|
1764
|
+
width: format.width - 2 * format.safeZone,
|
|
1765
|
+
bodyHeight: format.height - 2 * format.safeZone - titleHeight - FOOTER_ALLOWANCE
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
const NO_BORDER = "0px solid transparent";
|
|
1769
|
+
/**
|
|
1770
|
+
* Monospace `pre` text does not wrap, so the generic line-count fitter cannot keep it inside
|
|
1771
|
+
* the card: it reports two lines that fit and the long one runs off the right edge. Mono
|
|
1772
|
+
* glyphs are a constant fraction of the em, so the widest line gives a size directly. Same
|
|
1773
|
+
* approach as code-card, which is why MONO_ADVANCE is shared rather than copied.
|
|
1774
|
+
*/
|
|
1775
|
+
function codeFontSize(text, box, max) {
|
|
1776
|
+
const lines = text.split("\n");
|
|
1777
|
+
const longest = Math.max(1, ...lines.map((line) => line.length));
|
|
1778
|
+
const byWidth = box.width / (longest * MONO_ADVANCE);
|
|
1779
|
+
const byHeight = box.height / (lines.length * CODE_LINE_HEIGHT);
|
|
1780
|
+
return Math.max(CODE_MIN_FONT_SIZE, Math.min(max, Math.floor(Math.min(byWidth, byHeight))));
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* A body the reader is meant to copy and run: a code snippet, or upgrade instructions the
|
|
1784
|
+
* maintainer supplied. Generated upgrade prose (a link to the release notes) is not one, so
|
|
1785
|
+
* the provenance decides rather than the page kind alone.
|
|
1786
|
+
*/
|
|
1787
|
+
function isCommand(page) {
|
|
1788
|
+
return page.kind === "code" || page.kind === "upgrade" && page.body.provenance.source === "user-config";
|
|
1789
|
+
}
|
|
1790
|
+
const storyPage = {
|
|
1791
|
+
id: "story-page",
|
|
1792
|
+
events: ["release"],
|
|
1793
|
+
formats: [
|
|
1794
|
+
"og",
|
|
1795
|
+
"github-social",
|
|
1796
|
+
"x",
|
|
1797
|
+
"linkedin",
|
|
1798
|
+
"square",
|
|
1799
|
+
"portrait",
|
|
1800
|
+
"producthunt"
|
|
1801
|
+
],
|
|
1802
|
+
propsSchema,
|
|
1803
|
+
slots(format) {
|
|
1804
|
+
const { width, titleHeight, bodyHeight, tall } = geometry(format);
|
|
1805
|
+
return {
|
|
1806
|
+
name: {
|
|
1807
|
+
maxLines: 1,
|
|
1808
|
+
maxFontSize: 24,
|
|
1809
|
+
minFontSize: 18,
|
|
1810
|
+
step: 2,
|
|
1811
|
+
box: { width: width - 300 }
|
|
1812
|
+
},
|
|
1813
|
+
version: {
|
|
1814
|
+
maxLines: 1,
|
|
1815
|
+
maxFontSize: 22,
|
|
1816
|
+
minFontSize: 16,
|
|
1817
|
+
step: 2,
|
|
1818
|
+
box: { width: 220 }
|
|
1819
|
+
},
|
|
1820
|
+
title: {
|
|
1821
|
+
maxLines: tall ? 3 : 2,
|
|
1822
|
+
maxFontSize: tall ? 64 : 48,
|
|
1823
|
+
minFontSize: 28,
|
|
1824
|
+
step: 2,
|
|
1825
|
+
box: {
|
|
1826
|
+
width: width - 32,
|
|
1827
|
+
height: titleHeight
|
|
1828
|
+
}
|
|
1829
|
+
},
|
|
1830
|
+
body: {
|
|
1831
|
+
maxLines: Math.floor(bodyHeight / 30),
|
|
1832
|
+
maxFontSize: tall ? 34 : 28,
|
|
1833
|
+
minFontSize: 20,
|
|
1834
|
+
step: 2,
|
|
1835
|
+
box: {
|
|
1836
|
+
width: width - 48,
|
|
1837
|
+
height: bodyHeight - 48
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
};
|
|
1841
|
+
},
|
|
1842
|
+
slotText(raw) {
|
|
1843
|
+
const props = propsSchema.parse(raw);
|
|
1844
|
+
return {
|
|
1845
|
+
name: props.name,
|
|
1846
|
+
version: props.version,
|
|
1847
|
+
title: props.page.title.value,
|
|
1848
|
+
body: props.page.body.value
|
|
1849
|
+
};
|
|
1850
|
+
},
|
|
1851
|
+
slotFont(raw, slot) {
|
|
1852
|
+
const props = propsSchema.parse(raw);
|
|
1853
|
+
const mono = slot === "body" && isCommand(props.page);
|
|
1854
|
+
return {
|
|
1855
|
+
family: mono ? props.mono : slot === "title" ? props.heading : props.body,
|
|
1856
|
+
weight: slot === "title" ? props.weight : 400,
|
|
1857
|
+
lineHeight: mono ? 1.35 : 1.2,
|
|
1858
|
+
whiteSpace: mono ? "pre" : "normal"
|
|
1859
|
+
};
|
|
1860
|
+
},
|
|
1861
|
+
buildProps(facts, _copy, brand) {
|
|
1862
|
+
const page = facts.story?.[0];
|
|
1863
|
+
if (page === void 0) throw new ShipsealError("story-page.no-page", "The story-page template has no story page to render.", "Run shipseal story to build a story pack. Do not list story-page in release.templates.");
|
|
1864
|
+
return {
|
|
1865
|
+
props: {
|
|
1866
|
+
page,
|
|
1867
|
+
name: brand.name,
|
|
1868
|
+
version: facts.release?.tag.value ?? "",
|
|
1869
|
+
heading: brand.style === "terminal" ? brand.fonts.mono.family : brand.fonts.heading.family,
|
|
1870
|
+
body: brand.fonts.body.family,
|
|
1871
|
+
mono: brand.fonts.mono.family,
|
|
1872
|
+
weight: brand.fonts.heading.weight
|
|
1873
|
+
},
|
|
1874
|
+
missing: []
|
|
1875
|
+
};
|
|
1876
|
+
},
|
|
1877
|
+
render(raw, ctx) {
|
|
1878
|
+
const props = propsSchema.parse(raw);
|
|
1879
|
+
const colors = themeColors(ctx.brand, ctx.theme);
|
|
1880
|
+
const { tall, titleHeight, bodyHeight, width } = geometry(ctx.format);
|
|
1881
|
+
const editorial = ctx.brand.style === "editorial";
|
|
1882
|
+
const terminal = ctx.brand.style === "terminal";
|
|
1883
|
+
const code = isCommand(props.page);
|
|
1884
|
+
const comparison = props.page.kind === "comparison";
|
|
1885
|
+
const title = ctx.fitted.title;
|
|
1886
|
+
const body = ctx.fitted.body;
|
|
1887
|
+
const hasBody = props.page.body.value.trim().length > 0;
|
|
1888
|
+
const bodyText = body?.text ?? props.page.body.value;
|
|
1889
|
+
const bodyFontSize = code ? codeFontSize(bodyText, {
|
|
1890
|
+
width: width - 48,
|
|
1891
|
+
height: bodyHeight - 48
|
|
1892
|
+
}, tall ? 34 : 28) : body?.fontSize ?? 28;
|
|
1893
|
+
const paneWidth = tall ? width : (width - 16) / 2;
|
|
1894
|
+
const paneHeight = tall ? (bodyHeight - 16) / 2 : bodyHeight;
|
|
1895
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1896
|
+
style: {
|
|
1897
|
+
display: "flex",
|
|
1898
|
+
flexDirection: "column",
|
|
1899
|
+
width: "100%",
|
|
1900
|
+
height: "100%",
|
|
1901
|
+
backgroundColor: colors.background,
|
|
1902
|
+
color: colors.foreground,
|
|
1903
|
+
padding: ctx.format.safeZone,
|
|
1904
|
+
gap: 24,
|
|
1905
|
+
borderTop: editorial ? `16px solid ${colors.primary}` : NO_BORDER
|
|
1906
|
+
},
|
|
1907
|
+
children: [
|
|
1908
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1909
|
+
style: {
|
|
1910
|
+
display: "flex",
|
|
1911
|
+
justifyContent: "space-between",
|
|
1912
|
+
alignItems: "center",
|
|
1913
|
+
height: HEADER_HEIGHT,
|
|
1914
|
+
borderBottom: terminal ? `1px solid ${colors.muted}` : NO_BORDER,
|
|
1915
|
+
paddingBottom: terminal ? 12 : 0
|
|
1916
|
+
},
|
|
1917
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1918
|
+
style: {
|
|
1919
|
+
display: "flex",
|
|
1920
|
+
gap: 12,
|
|
1921
|
+
alignItems: "center"
|
|
1922
|
+
},
|
|
1923
|
+
children: [ctx.logoSrc !== void 0 ? /* @__PURE__ */ jsx("img", {
|
|
1924
|
+
src: ctx.logoSrc,
|
|
1925
|
+
width: 32,
|
|
1926
|
+
height: 32
|
|
1927
|
+
}) : void 0, /* @__PURE__ */ jsx("div", {
|
|
1928
|
+
style: {
|
|
1929
|
+
fontFamily: terminal ? props.mono : props.body,
|
|
1930
|
+
fontWeight: 400,
|
|
1931
|
+
fontSize: ctx.fitted.name?.fontSize ?? 24,
|
|
1932
|
+
color: colors.primary
|
|
1933
|
+
},
|
|
1934
|
+
children: ctx.fitted.name?.text ?? props.name
|
|
1935
|
+
})]
|
|
1936
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1937
|
+
style: {
|
|
1938
|
+
fontFamily: props.mono,
|
|
1939
|
+
fontWeight: 400,
|
|
1940
|
+
fontSize: ctx.fitted.version?.fontSize ?? 22,
|
|
1941
|
+
color: colors.muted
|
|
1942
|
+
},
|
|
1943
|
+
children: ctx.fitted.version?.text ?? props.version
|
|
1944
|
+
})]
|
|
1945
|
+
}),
|
|
1946
|
+
/* @__PURE__ */ jsx("div", {
|
|
1947
|
+
style: {
|
|
1948
|
+
display: "flex",
|
|
1949
|
+
alignItems: "center",
|
|
1950
|
+
height: titleHeight,
|
|
1951
|
+
flexShrink: 0,
|
|
1952
|
+
paddingLeft: editorial ? 24 : 0,
|
|
1953
|
+
borderLeft: editorial ? `8px solid ${colors.primary}` : NO_BORDER
|
|
1954
|
+
},
|
|
1955
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1956
|
+
style: {
|
|
1957
|
+
fontFamily: props.heading,
|
|
1958
|
+
fontWeight: props.weight,
|
|
1959
|
+
fontSize: title?.fontSize ?? 48,
|
|
1960
|
+
lineHeight: 1.2,
|
|
1961
|
+
maxWidth: width - (editorial ? 32 : 0)
|
|
1962
|
+
},
|
|
1963
|
+
children: title?.text ?? props.page.title.value
|
|
1964
|
+
})
|
|
1965
|
+
}),
|
|
1966
|
+
comparison ? /* @__PURE__ */ jsx("div", {
|
|
1967
|
+
style: {
|
|
1968
|
+
display: "flex",
|
|
1969
|
+
flexDirection: tall ? "column" : "row",
|
|
1970
|
+
gap: 16,
|
|
1971
|
+
height: bodyHeight
|
|
1972
|
+
},
|
|
1973
|
+
children: ["before", "after"].map((label) => /* @__PURE__ */ jsxs("div", {
|
|
1974
|
+
style: {
|
|
1975
|
+
display: "flex",
|
|
1976
|
+
flexDirection: "column",
|
|
1977
|
+
gap: 8,
|
|
1978
|
+
width: paneWidth,
|
|
1979
|
+
height: paneHeight
|
|
1980
|
+
},
|
|
1981
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1982
|
+
style: {
|
|
1983
|
+
fontFamily: props.body,
|
|
1984
|
+
fontSize: 20,
|
|
1985
|
+
color: colors.muted
|
|
1986
|
+
},
|
|
1987
|
+
children: label === "before" ? "Before" : "After"
|
|
1988
|
+
}), /* @__PURE__ */ jsx("img", {
|
|
1989
|
+
src: `story-${label}`,
|
|
1990
|
+
width: paneWidth,
|
|
1991
|
+
height: paneHeight - HEADER_HEIGHT,
|
|
1992
|
+
style: { objectFit: "contain" }
|
|
1993
|
+
})]
|
|
1994
|
+
}, label))
|
|
1995
|
+
}) : hasBody ? /* @__PURE__ */ jsx("div", {
|
|
1996
|
+
style: {
|
|
1997
|
+
display: "flex",
|
|
1998
|
+
flexDirection: "column",
|
|
1999
|
+
justifyContent: "flex-start",
|
|
2000
|
+
height: bodyHeight,
|
|
2001
|
+
padding: BODY_PAD,
|
|
2002
|
+
backgroundColor: terminal || code ? colors.card : "transparent",
|
|
2003
|
+
borderRadius: terminal ? 0 : ctx.brand.radius,
|
|
2004
|
+
borderLeft: terminal ? `3px solid ${colors.primary}` : NO_BORDER
|
|
2005
|
+
},
|
|
2006
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
2007
|
+
style: {
|
|
2008
|
+
fontFamily: code ? props.mono : props.body,
|
|
2009
|
+
fontWeight: 400,
|
|
2010
|
+
fontSize: bodyFontSize,
|
|
2011
|
+
lineHeight: code ? CODE_LINE_HEIGHT : 1.2,
|
|
2012
|
+
whiteSpace: code ? "pre" : "normal",
|
|
2013
|
+
color: colors.foreground
|
|
2014
|
+
},
|
|
2015
|
+
children: bodyText
|
|
2016
|
+
})
|
|
2017
|
+
}) : void 0,
|
|
2018
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2019
|
+
style: {
|
|
2020
|
+
display: "flex",
|
|
2021
|
+
justifyContent: "space-between",
|
|
2022
|
+
marginTop: "auto",
|
|
2023
|
+
fontFamily: props.mono,
|
|
2024
|
+
fontWeight: 400,
|
|
2025
|
+
fontSize: 16,
|
|
2026
|
+
color: colors.muted
|
|
2027
|
+
},
|
|
2028
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2029
|
+
style: { fontFamily: props.mono },
|
|
2030
|
+
children: comparison ? "Supplied screenshots" : "Sources in manifest.json"
|
|
2031
|
+
}), ctx.attribution ? /* @__PURE__ */ jsx("div", {
|
|
2032
|
+
style: { fontFamily: props.mono },
|
|
2033
|
+
children: "made with shipseal.dev"
|
|
2034
|
+
}) : void 0]
|
|
2035
|
+
})
|
|
2036
|
+
]
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
};
|
|
2040
|
+
|
|
1538
2041
|
//#endregion
|
|
1539
2042
|
//#region src/templates/registry.ts
|
|
1540
2043
|
const templates = [
|
|
@@ -1542,7 +2045,8 @@ const templates = [
|
|
|
1542
2045
|
releaseHighlights,
|
|
1543
2046
|
codeCard,
|
|
1544
2047
|
milestone,
|
|
1545
|
-
bench
|
|
2048
|
+
bench,
|
|
2049
|
+
storyPage
|
|
1546
2050
|
];
|
|
1547
2051
|
function getTemplate(id) {
|
|
1548
2052
|
const found = templates.find((template) => template.id === id);
|
|
@@ -1552,6 +2056,14 @@ function getTemplate(id) {
|
|
|
1552
2056
|
|
|
1553
2057
|
//#endregion
|
|
1554
2058
|
//#region src/core/generate.ts
|
|
2059
|
+
/**
|
|
2060
|
+
* Digest via WebCrypto, not `node:crypto`. The browser demo imports `generate()`, so nothing
|
|
2061
|
+
* in the pure core may reach for a Node builtin. Do not "simplify" this back to createHash.
|
|
2062
|
+
*/
|
|
2063
|
+
async function sha256Hex(bytes) {
|
|
2064
|
+
const digest = await crypto.subtle.digest("SHA-256", Uint8Array.from(bytes));
|
|
2065
|
+
return Array.from(new Uint8Array(digest)).map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
2066
|
+
}
|
|
1555
2067
|
async function generate(input) {
|
|
1556
2068
|
const templateIds = templateIdsFor(input.event, input.config);
|
|
1557
2069
|
const formatIds = input.config.formats ?? [
|
|
@@ -1613,8 +2125,10 @@ async function generate(input) {
|
|
|
1613
2125
|
src: "shipseal-logo",
|
|
1614
2126
|
data: logo
|
|
1615
2127
|
}];
|
|
2128
|
+
if (input.images !== void 0) renderOpts.images = [...renderOpts.images ?? [], ...input.images];
|
|
1616
2129
|
const bytes = await input.renderer.render(node, renderOpts);
|
|
1617
2130
|
const fileName = outputName(template.id, formatId, theme, omitThemeSuffix, imageFormat);
|
|
2131
|
+
const sha256 = await sha256Hex(bytes);
|
|
1618
2132
|
files.push({
|
|
1619
2133
|
fileName,
|
|
1620
2134
|
template: template.id,
|
|
@@ -1623,7 +2137,7 @@ async function generate(input) {
|
|
|
1623
2137
|
width: format.width,
|
|
1624
2138
|
height: format.height,
|
|
1625
2139
|
bytes,
|
|
1626
|
-
sha256
|
|
2140
|
+
sha256
|
|
1627
2141
|
});
|
|
1628
2142
|
}
|
|
1629
2143
|
}
|
|
@@ -1745,6 +2259,7 @@ function mergeFacts(parts) {
|
|
|
1745
2259
|
if (project.url !== void 0) facts.project.url = project.url;
|
|
1746
2260
|
if (project.repo !== void 0) facts.project.repo = project.repo;
|
|
1747
2261
|
if (project.npmPackage !== void 0) facts.project.npmPackage = project.npmPackage;
|
|
2262
|
+
if (project.cli !== void 0) facts.project.cli = project.cli;
|
|
1748
2263
|
if (project.license !== void 0) facts.project.license = project.license;
|
|
1749
2264
|
if (isCompleteRelease(release)) {
|
|
1750
2265
|
facts.release = {
|
|
@@ -1755,6 +2270,9 @@ function mergeFacts(parts) {
|
|
|
1755
2270
|
fixes: release.fixes ?? [],
|
|
1756
2271
|
breaking: release.breaking ?? []
|
|
1757
2272
|
};
|
|
2273
|
+
if (release.kind !== void 0) facts.release.kind = release.kind;
|
|
2274
|
+
if (release.headline !== void 0) facts.release.headline = release.headline;
|
|
2275
|
+
if (release.subheadline !== void 0) facts.release.subheadline = release.subheadline;
|
|
1758
2276
|
if (release.previousVersion !== void 0) facts.release.previousVersion = release.previousVersion;
|
|
1759
2277
|
if (release.commitCount !== void 0) facts.release.commitCount = release.commitCount;
|
|
1760
2278
|
if (release.contributors !== void 0) facts.release.contributors = release.contributors;
|
|
@@ -2028,6 +2546,17 @@ async function collectChangelog(cwd, version, changelogPath = "CHANGELOG.md") {
|
|
|
2028
2546
|
fixes,
|
|
2029
2547
|
breaking
|
|
2030
2548
|
} };
|
|
2549
|
+
for (const slot of ["headline", "subheadline"]) {
|
|
2550
|
+
const text = new RegExp(`<!--\\s*shipseal:\\s*${slot}\\s+"([^"]+)"\\s*-->`).exec(section.body)?.[1]?.trim();
|
|
2551
|
+
if (text !== void 0 && text.length > 0) out.release = {
|
|
2552
|
+
...out.release,
|
|
2553
|
+
[slot]: fact(text, {
|
|
2554
|
+
source: "user-config",
|
|
2555
|
+
ref: `${changelogPath} ${section.heading} shipseal:${slot} marker`,
|
|
2556
|
+
fetchedAt
|
|
2557
|
+
})
|
|
2558
|
+
};
|
|
2559
|
+
}
|
|
2031
2560
|
const snippet = extractFirstCodeFence(section.body);
|
|
2032
2561
|
if (snippet !== void 0) out.release = {
|
|
2033
2562
|
...out.release,
|
|
@@ -2071,7 +2600,7 @@ function findVersionSection(markdown, version) {
|
|
|
2071
2600
|
}
|
|
2072
2601
|
}
|
|
2073
2602
|
function cleanChangelogItem(item) {
|
|
2074
|
-
return item.replace(/^\s*[-*]\s*/, "").replace(/\[`[a-f0-9]{7,40}`]\([^)]+\)/gi, "").replace(/^[a-f0-9]{7,40}:\s*/i, "").replace(/\(#\d+\)/g, "").replace(/\[#\d+]\([^)]+\)/g, "").replace(/Thanks\s+\[@[\w-]+]\([^)]+\)!?\s*-?\s*/gi, "").replace(/\[@[\w-]+]\([^)]+\)/g, "").replace(/\(@[\w-]+\)/g, "").replace(/\s+by\s+@[\w-]+/gi, "").replace(/@[\w-]+/g, "").replace(/^Thanks\s*!?\s*-?\s*/i, "").replace(/^\s*[-*]\s*/, "").replace(/\s+/g, " ").trim().replace(/\.$/, "");
|
|
2603
|
+
return item.replace(/^\s*[-*]\s*/, "").replace(/\[`[a-f0-9]{7,40}`]\([^)]+\)/gi, "").replace(/^[a-f0-9]{7,40}:\s*/i, "").replace(/\(#\d+\)/g, "").replace(/\[#\d+]\([^)]+\)/g, "").replace(/Thanks\s+\[@[\w-]+]\([^)]+\)!?\s*-?\s*/gi, "").replace(/\[@[\w-]+]\([^)]+\)/g, "").replace(/\(@[\w-]+\)/g, "").replace(/\s+by\s+@[\w-]+/gi, "").replace(/@[\w-]+/g, "").replace(/^Thanks\s*!?\s*-?\s*/i, "").replace(/^\s*[-*]\s*/, "").replaceAll(/`([^`]+)`/g, "$1").replaceAll("`", "").replace(/\s+/g, " ").trim().replace(/\.$/, "");
|
|
2075
2604
|
}
|
|
2076
2605
|
function headingIncludesVersion(heading, version) {
|
|
2077
2606
|
const unwrapped = heading.replace(/[[\]]/g, " ");
|
|
@@ -2201,11 +2730,19 @@ async function collectGit(cwd, event) {
|
|
|
2201
2730
|
fetchedAt
|
|
2202
2731
|
})
|
|
2203
2732
|
};
|
|
2204
|
-
if (previousTag !== void 0)
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2733
|
+
if (previousTag !== void 0) {
|
|
2734
|
+
release.previousVersion = fact(versionFromTag(previousTag), {
|
|
2735
|
+
source: "git",
|
|
2736
|
+
ref: `git tag ${previousTag}`,
|
|
2737
|
+
fetchedAt
|
|
2738
|
+
});
|
|
2739
|
+
const kind = releaseKind(versionFromTag(previousTag), versionFromTag(tag));
|
|
2740
|
+
if (kind !== void 0) release.kind = fact(kind, {
|
|
2741
|
+
source: "git",
|
|
2742
|
+
ref: `semver ${previousTag}..${tag}`,
|
|
2743
|
+
fetchedAt
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
2209
2746
|
if (names.length > 0) release.contributors = fact(names, {
|
|
2210
2747
|
source: "git",
|
|
2211
2748
|
ref: `git log ${range} unique authors`,
|
|
@@ -2294,6 +2831,30 @@ function githubSlug(url) {
|
|
|
2294
2831
|
const cleaned = url.replace(/^git\+/, "").replace(/\.git$/, "");
|
|
2295
2832
|
return /github\.com[/:]([^/]+\/[^/]+)$/.exec(cleaned)?.[1];
|
|
2296
2833
|
}
|
|
2834
|
+
/**
|
|
2835
|
+
* How significant is this release, by semver alone?
|
|
2836
|
+
*
|
|
2837
|
+
* Compared against the previous tag rather than read from a changelog heading, so it is a fact
|
|
2838
|
+
* with a reference someone can re-run. Returns undefined when either tag is not semver, or when
|
|
2839
|
+
* the two are equal: a caller should treat "unknown" as "announce it", never as "skip it".
|
|
2840
|
+
*/
|
|
2841
|
+
function releaseKind(previous, current) {
|
|
2842
|
+
const before = parseSemver(previous);
|
|
2843
|
+
const after = parseSemver(current);
|
|
2844
|
+
if (before === void 0 || after === void 0) return;
|
|
2845
|
+
if (after[0] !== before[0]) return "major";
|
|
2846
|
+
if (after[1] !== before[1]) return "minor";
|
|
2847
|
+
return after[2] === before[2] ? void 0 : "patch";
|
|
2848
|
+
}
|
|
2849
|
+
function parseSemver(value) {
|
|
2850
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(value.replace(/^v/, ""));
|
|
2851
|
+
if (match === null) return;
|
|
2852
|
+
return [
|
|
2853
|
+
Number(match[1]),
|
|
2854
|
+
Number(match[2]),
|
|
2855
|
+
Number(match[3])
|
|
2856
|
+
];
|
|
2857
|
+
}
|
|
2297
2858
|
|
|
2298
2859
|
//#endregion
|
|
2299
2860
|
//#region src/sources/github.ts
|
|
@@ -2492,6 +3053,7 @@ async function npmPackageExists(name, fetchImpl = fetch) {
|
|
|
2492
3053
|
const pkgSchema = z.object({
|
|
2493
3054
|
private: z.boolean().optional(),
|
|
2494
3055
|
name: z.string().optional(),
|
|
3056
|
+
bin: z.union([z.string(), z.record(z.string(), z.string())]).optional(),
|
|
2495
3057
|
description: z.string().optional(),
|
|
2496
3058
|
version: z.string().optional(),
|
|
2497
3059
|
homepage: z.string().optional(),
|
|
@@ -2543,6 +3105,11 @@ async function collectPackageJson(cwd, packagePath = "package.json") {
|
|
|
2543
3105
|
ref: `${packagePath}#name`,
|
|
2544
3106
|
fetchedAt
|
|
2545
3107
|
});
|
|
3108
|
+
if (pkg.data.bin !== void 0 && pkg.data.private !== true) project.cli = fact(true, {
|
|
3109
|
+
source: "package-json",
|
|
3110
|
+
ref: `${packagePath}#bin`,
|
|
3111
|
+
fetchedAt
|
|
3112
|
+
});
|
|
2546
3113
|
if (pkg.data.license !== void 0) project.license = fact(pkg.data.license, {
|
|
2547
3114
|
source: "package-json",
|
|
2548
3115
|
ref: `${packagePath}#license`,
|
|
@@ -2597,9 +3164,23 @@ async function collectFacts(options) {
|
|
|
2597
3164
|
const changelog = await collectBestChangelog(options.cwd, version, options.changelogPath ?? "CHANGELOG.md", options.packagePath);
|
|
2598
3165
|
const readme = await collectReadme(options.cwd);
|
|
2599
3166
|
const snippet = options.snippet !== void 0 && options.snippet !== null && options.snippet.length > 0 ? await collectConfiguredSnippet(options.cwd, options.snippet) : {};
|
|
3167
|
+
const workspacePkg = pkg.project?.npmPackage === void 0 ? await collectWorkspaceNpmPackage(options.cwd, options.skipNetwork === true ? {} : { verify: async (name) => npmPackageExists(name, options.fetchImpl ?? fetch) }) : {};
|
|
3168
|
+
const overrides = {};
|
|
3169
|
+
for (const slot of ["headline", "subheadline"]) {
|
|
3170
|
+
const text = options[slot]?.trim();
|
|
3171
|
+
if (text !== void 0 && text.length > 0) overrides.release = {
|
|
3172
|
+
...overrides.release,
|
|
3173
|
+
[slot]: fact(text, {
|
|
3174
|
+
source: "user-config",
|
|
3175
|
+
ref: `--${slot} or release.${slot}`,
|
|
3176
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3177
|
+
})
|
|
3178
|
+
};
|
|
3179
|
+
}
|
|
2600
3180
|
const parts = [
|
|
3181
|
+
overrides,
|
|
2601
3182
|
pkg,
|
|
2602
|
-
|
|
3183
|
+
workspacePkg,
|
|
2603
3184
|
remote,
|
|
2604
3185
|
git,
|
|
2605
3186
|
snippet,
|
|
@@ -2656,16 +3237,19 @@ async function collectWorkspaceNpmPackage(cwd, options = {}) {
|
|
|
2656
3237
|
const paths = await workspaceManifests(cwd);
|
|
2657
3238
|
const found = (await Promise.all(paths.map((path) => collectPackageJson(cwd, path)))).map((facts, index) => ({
|
|
2658
3239
|
name: facts.project?.npmPackage?.value,
|
|
3240
|
+
cli: facts.project?.cli,
|
|
2659
3241
|
path: paths[index] ?? ""
|
|
2660
3242
|
})).filter((entry) => entry.name !== void 0);
|
|
2661
3243
|
const only = found.length === 1 ? found[0] : void 0;
|
|
2662
3244
|
if (only === void 0) return {};
|
|
2663
3245
|
if (options.verify !== void 0 && await options.verify(only.name) === false) return {};
|
|
2664
|
-
|
|
3246
|
+
const project = { npmPackage: fact(only.name, {
|
|
2665
3247
|
source: "package-json",
|
|
2666
3248
|
ref: `${only.path}#name`,
|
|
2667
3249
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2668
|
-
}) }
|
|
3250
|
+
}) };
|
|
3251
|
+
if (only.cli !== void 0) project.cli = only.cli;
|
|
3252
|
+
return { project };
|
|
2669
3253
|
}
|
|
2670
3254
|
async function workspaceManifests(cwd) {
|
|
2671
3255
|
return (await workspaceChangelogs(cwd)).map((path) => path.replace(/CHANGELOG\.md$/, "package.json"));
|
|
@@ -2693,27 +3277,18 @@ function gitRepoFromParts(pkg, readme) {
|
|
|
2693
3277
|
//#endregion
|
|
2694
3278
|
//#region src/render/takumi.ts
|
|
2695
3279
|
const GEIST_MONO_FAMILY = "Geist Mono";
|
|
2696
|
-
|
|
2697
|
-
|
|
3280
|
+
/** Path of the vendored font, relative to the package root. Resolved in `takumi-node.ts`. */
|
|
3281
|
+
const FONT_REL = "assets/fonts/GeistMono[wght].ttf";
|
|
3282
|
+
/** Both runtimes end here: one adapter, one registered font, whoever supplied the bytes. */
|
|
3283
|
+
async function createRendererWithFont(font) {
|
|
2698
3284
|
const renderer = new Renderer();
|
|
2699
|
-
const fontPath = join(resolvePackageRoot(), FONT_REL);
|
|
2700
|
-
const data = await readFile(fontPath);
|
|
2701
3285
|
await renderer.registerFont({
|
|
2702
3286
|
name: GEIST_MONO_FAMILY,
|
|
2703
|
-
data,
|
|
3287
|
+
data: font,
|
|
2704
3288
|
generic: "monospace"
|
|
2705
3289
|
});
|
|
2706
3290
|
return new TakumiRenderer(renderer);
|
|
2707
3291
|
}
|
|
2708
|
-
function resolvePackageRoot(from = import.meta.url) {
|
|
2709
|
-
let dir = dirname(fileURLToPath(from));
|
|
2710
|
-
for (;;) {
|
|
2711
|
-
if (existsSync(join(dir, "package.json")) && existsSync(join(dir, FONT_REL))) return dir;
|
|
2712
|
-
const parent = dirname(dir);
|
|
2713
|
-
if (parent === dir) throw new ShipsealError("render.font-missing", `Could not find vendored font ${FONT_REL}.`, "Reinstall shipseal so assets/fonts/GeistMono[wght].ttf is present next to package.json.");
|
|
2714
|
-
dir = parent;
|
|
2715
|
-
}
|
|
2716
|
-
}
|
|
2717
3292
|
function countLines(node) {
|
|
2718
3293
|
const ys = /* @__PURE__ */ new Set();
|
|
2719
3294
|
const walk = (current) => {
|
|
@@ -2882,6 +3457,23 @@ function asReactElementLike(element) {
|
|
|
2882
3457
|
};
|
|
2883
3458
|
}
|
|
2884
3459
|
|
|
3460
|
+
//#endregion
|
|
3461
|
+
//#region src/render/takumi-node.ts
|
|
3462
|
+
async function createTakumiRenderer() {
|
|
3463
|
+
const fontPath = join(resolvePackageRoot(), FONT_REL);
|
|
3464
|
+
const data = await readFile(fontPath);
|
|
3465
|
+
return createRendererWithFont(data);
|
|
3466
|
+
}
|
|
3467
|
+
function resolvePackageRoot(from = import.meta.url) {
|
|
3468
|
+
let dir = dirname(fileURLToPath(from));
|
|
3469
|
+
for (;;) {
|
|
3470
|
+
if (existsSync(join(dir, "package.json")) && existsSync(join(dir, "assets/fonts/GeistMono[wght].ttf"))) return dir;
|
|
3471
|
+
const parent = dirname(dir);
|
|
3472
|
+
if (parent === dir) throw new ShipsealError("render.font-missing", `Could not find vendored font ${FONT_REL}.`, "Reinstall shipseal so assets/fonts/GeistMono[wght].ttf is present next to package.json.");
|
|
3473
|
+
dir = parent;
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
|
|
2885
3477
|
//#endregion
|
|
2886
3478
|
//#region src/copy/number-guard.ts
|
|
2887
3479
|
function allowedNumbers(facts) {
|
|
@@ -3112,6 +3704,7 @@ function flattenFacts(facts) {
|
|
|
3112
3704
|
if (typeof value === "object" && value !== null) for (const [key, nested] of Object.entries(value)) walk(prefix.length === 0 ? key : `${prefix}.${key}`, nested);
|
|
3113
3705
|
};
|
|
3114
3706
|
walk("project", facts.project);
|
|
3707
|
+
if (facts.story !== void 0) walk("story", facts.story);
|
|
3115
3708
|
if (facts.release !== void 0) walk("release", facts.release);
|
|
3116
3709
|
if (facts.metrics !== void 0) walk("metrics", facts.metrics);
|
|
3117
3710
|
if (facts.bench !== void 0) walk("bench", facts.bench);
|
|
@@ -3827,12 +4420,12 @@ function allVariables(css) {
|
|
|
3827
4420
|
return vars;
|
|
3828
4421
|
}
|
|
3829
4422
|
/**
|
|
3830
|
-
* Resolve a declaration to a hex
|
|
4423
|
+
* Resolve a declaration to a hex color.
|
|
3831
4424
|
*
|
|
3832
|
-
* Handles two things a plain
|
|
4425
|
+
* Handles two things a plain color parser does not. First, `var(--brand-500)` indirection,
|
|
3833
4426
|
* up to three hops within the same file. Second, bare channel lists: shadcn writes
|
|
3834
4427
|
* `--background: 0 0% 100%` and applies it as `hsl(var(--background))`, so the value is only
|
|
3835
|
-
* a
|
|
4428
|
+
* a color once wrapped. Percent signs on the last two channels mean HSL, three plain
|
|
3836
4429
|
* numbers mean RGB.
|
|
3837
4430
|
*/
|
|
3838
4431
|
function resolveColor(raw, vars, depth = 0) {
|
|
@@ -3993,7 +4586,7 @@ const SIGNATURE = Buffer.from([
|
|
|
3993
4586
|
26,
|
|
3994
4587
|
10
|
|
3995
4588
|
]);
|
|
3996
|
-
/** Channels per pixel for each PNG
|
|
4589
|
+
/** Channels per pixel for each PNG color type. Index is the color type. */
|
|
3997
4590
|
const CHANNELS = {
|
|
3998
4591
|
0: 1,
|
|
3999
4592
|
2: 3,
|
|
@@ -4143,10 +4736,10 @@ function toRgba(data, width, height, colorType, bytesPerSample, palette, palette
|
|
|
4143
4736
|
return pixels;
|
|
4144
4737
|
}
|
|
4145
4738
|
/**
|
|
4146
|
-
* Most common non-neutral
|
|
4739
|
+
* Most common non-neutral color in a logo, as a hex string.
|
|
4147
4740
|
*
|
|
4148
4741
|
* Nearly transparent pixels are ignored so a transparent background does not win, and greys
|
|
4149
|
-
* are ignored because a logo's black wordmark is not its brand
|
|
4742
|
+
* are ignored because a logo's black wordmark is not its brand color. Colors are bucketed
|
|
4150
4743
|
* into 16 levels per channel so anti-aliased edges group with the solid fill they belong to,
|
|
4151
4744
|
* then the winning bucket reports the average of the real pixels inside it rather than the
|
|
4152
4745
|
* bucket centre, which would drift the hue.
|
|
@@ -4483,7 +5076,7 @@ async function detectColors(cwd, logoPath, sources, notes) {
|
|
|
4483
5076
|
merged.primary = fromPng;
|
|
4484
5077
|
sources.push({
|
|
4485
5078
|
field: "colors.primary",
|
|
4486
|
-
source: `dominant
|
|
5079
|
+
source: `dominant color in ${logoPath}`
|
|
4487
5080
|
});
|
|
4488
5081
|
}
|
|
4489
5082
|
const svg = logoPath.endsWith(".svg") ? await readMaybe(join(cwd, logoPath)) : void 0;
|
|
@@ -4625,8 +5218,8 @@ function normalizeGitUrl(url) {
|
|
|
4625
5218
|
return url.replace(/^git\+/, "").replace(/\.git$/, "");
|
|
4626
5219
|
}
|
|
4627
5220
|
/**
|
|
4628
|
-
* Dominant non-neutral
|
|
4629
|
-
* carries no
|
|
5221
|
+
* Dominant non-neutral color of a PNG logo, or undefined when the file cannot be read or
|
|
5222
|
+
* carries no color. Detection reports the color as not found rather than falling back to a
|
|
4630
5223
|
* built-in default that would be presented to the user as "your brand".
|
|
4631
5224
|
*/
|
|
4632
5225
|
async function dominantLogoColor(path) {
|
|
@@ -4898,6 +5491,12 @@ function missingMetric(metric) {
|
|
|
4898
5491
|
|
|
4899
5492
|
//#endregion
|
|
4900
5493
|
//#region src/commands/release.ts
|
|
5494
|
+
/** Lowest kind worth announcing, most significant first. */
|
|
5495
|
+
const ANNOUNCE_ORDER = [
|
|
5496
|
+
"major",
|
|
5497
|
+
"minor",
|
|
5498
|
+
"patch"
|
|
5499
|
+
];
|
|
4901
5500
|
async function runRelease(flags) {
|
|
4902
5501
|
const cwd = flags.cwd;
|
|
4903
5502
|
const config = overlayReleaseConfig(await loadConfig(cwd), flags);
|
|
@@ -4923,9 +5522,25 @@ async function runRelease(flags) {
|
|
|
4923
5522
|
if (config.release?.snippet !== void 0) collectOpts.snippet = config.release.snippet;
|
|
4924
5523
|
if (env.githubToken !== void 0) collectOpts.githubToken = env.githubToken;
|
|
4925
5524
|
if (env.fetchImpl !== void 0) collectOpts.fetchImpl = env.fetchImpl;
|
|
5525
|
+
const headline = flags.headline ?? config.release?.headline ?? void 0;
|
|
5526
|
+
if (headline !== void 0) collectOpts.headline = headline;
|
|
5527
|
+
const subheadline = flags.subheadline ?? config.release?.subheadline ?? void 0;
|
|
5528
|
+
if (subheadline !== void 0) collectOpts.subheadline = subheadline;
|
|
4926
5529
|
const facts = await collectFacts(collectOpts);
|
|
5530
|
+
const announce = config.release?.announce ?? "patch";
|
|
5531
|
+
const kind = facts.release?.kind?.value;
|
|
5532
|
+
if (kind !== void 0 && ANNOUNCE_ORDER.indexOf(kind) > ANNOUNCE_ORDER.indexOf(announce)) return {
|
|
5533
|
+
dryRun: flags.dryRun === true,
|
|
5534
|
+
skipped: true,
|
|
5535
|
+
message: `This is a ${kind} release and release.announce is "${announce}". Nothing to generate.`,
|
|
5536
|
+
facts,
|
|
5537
|
+
copy: deterministicCopy(facts, config.release?.maxHighlights, brand.name),
|
|
5538
|
+
copyMode: "deterministic",
|
|
5539
|
+
warnings: [],
|
|
5540
|
+
exitCode: 0
|
|
5541
|
+
};
|
|
4927
5542
|
const noCopy = flags.copy === false;
|
|
4928
|
-
const resolved = await resolveCopy(facts, config, noCopy, deterministicCopy(facts, config.release?.maxHighlights));
|
|
5543
|
+
const resolved = await resolveCopy(facts, config, noCopy, deterministicCopy(facts, config.release?.maxHighlights, brand.name));
|
|
4929
5544
|
if (flags.dryRun === true) {
|
|
4930
5545
|
const dry = {
|
|
4931
5546
|
dryRun: true,
|
|
@@ -4977,6 +5592,687 @@ function overlayReleaseConfig(config, flags) {
|
|
|
4977
5592
|
});
|
|
4978
5593
|
}
|
|
4979
5594
|
|
|
5595
|
+
//#endregion
|
|
5596
|
+
//#region src/core/story.ts
|
|
5597
|
+
const DEFAULT_CHANGE_PAGES = 4;
|
|
5598
|
+
const STORY_FORMATS$1 = "portrait, square, og, github-social, x, linkedin, or producthunt";
|
|
5599
|
+
function storyFacts(facts, config, name, generatedAt) {
|
|
5600
|
+
const release = facts.release;
|
|
5601
|
+
if (release === void 0) throw new ShipsealError("story.no-release", "No release facts are available for a story.", "Choose a published release or pass --tag to a local repository.");
|
|
5602
|
+
const copy = deterministicCopy(facts, config.release?.maxHighlights, name);
|
|
5603
|
+
const configured = (value, ref) => fact(value, {
|
|
5604
|
+
source: "user-config",
|
|
5605
|
+
ref,
|
|
5606
|
+
fetchedAt: generatedAt
|
|
5607
|
+
});
|
|
5608
|
+
const changes = [
|
|
5609
|
+
...release.breaking,
|
|
5610
|
+
...release.features,
|
|
5611
|
+
...release.fixes
|
|
5612
|
+
];
|
|
5613
|
+
const headlineSource = release.headline ?? changes.find((item) => cleanLine(firstSentence(item.value)) === copy.headline) ?? release.version;
|
|
5614
|
+
const pages = [{
|
|
5615
|
+
kind: "cover",
|
|
5616
|
+
title: fact(copy.headline, headlineSource.provenance),
|
|
5617
|
+
body: release.subheadline ?? facts.project.tagline ?? fact("Release notes", release.tag.provenance)
|
|
5618
|
+
}];
|
|
5619
|
+
for (const item of changes.slice(0, config.release?.maxHighlights ?? DEFAULT_CHANGE_PAGES)) {
|
|
5620
|
+
const collapsed = item.value.replaceAll(/\s+/g, " ").trim();
|
|
5621
|
+
const sentence = firstSentence(collapsed);
|
|
5622
|
+
const remainder = collapsed.slice(sentence.length).trim();
|
|
5623
|
+
pages.push({
|
|
5624
|
+
kind: "change",
|
|
5625
|
+
title: fact(cleanLine(sentence), item.provenance),
|
|
5626
|
+
body: fact(remainder.length > 0 ? cleanLine(remainder) : "", item.provenance)
|
|
5627
|
+
});
|
|
5628
|
+
}
|
|
5629
|
+
if (release.codeSnippet !== void 0) pages.push({
|
|
5630
|
+
kind: "code",
|
|
5631
|
+
title: fact("Code example", release.codeSnippet.provenance),
|
|
5632
|
+
body: fact(release.codeSnippet.value.code, release.codeSnippet.provenance)
|
|
5633
|
+
});
|
|
5634
|
+
const options = config.release?.story;
|
|
5635
|
+
if (options?.before !== void 0 && options.after !== void 0) pages.push({
|
|
5636
|
+
kind: "comparison",
|
|
5637
|
+
title: configured("Before and after", "release.story.before + release.story.after"),
|
|
5638
|
+
body: configured("Supplied screenshots", "release.story"),
|
|
5639
|
+
before: configured(options.before, "release.story.before"),
|
|
5640
|
+
after: configured(options.after, "release.story.after")
|
|
5641
|
+
});
|
|
5642
|
+
const hasUpgrade = options?.upgrade !== void 0 && options.upgrade.trim().length > 0;
|
|
5643
|
+
let upgrade;
|
|
5644
|
+
if (hasUpgrade && options?.upgrade !== void 0) upgrade = configured(options.upgrade.trim(), "release.story.upgrade");
|
|
5645
|
+
else {
|
|
5646
|
+
const repo = facts.project.repo;
|
|
5647
|
+
upgrade = repo === void 0 ? fact(copy.cta, facts.project.url?.provenance ?? facts.project.name.provenance) : fact(`Read the release notes at github.com/${repo.value}/releases/tag/${encodeURIComponent(release.tag.value)}`, repo.provenance);
|
|
5648
|
+
}
|
|
5649
|
+
pages.push({
|
|
5650
|
+
kind: "upgrade",
|
|
5651
|
+
title: fact(hasUpgrade ? "How to upgrade" : "Get the release", upgrade.provenance),
|
|
5652
|
+
body: upgrade
|
|
5653
|
+
});
|
|
5654
|
+
return {
|
|
5655
|
+
...facts,
|
|
5656
|
+
story: pages
|
|
5657
|
+
};
|
|
5658
|
+
}
|
|
5659
|
+
async function generateStory(input) {
|
|
5660
|
+
const facts = storyFacts(input.facts, input.config, input.brand.name, input.generatedAt);
|
|
5661
|
+
const pages = facts.story ?? [];
|
|
5662
|
+
const result = {
|
|
5663
|
+
files: [],
|
|
5664
|
+
warnings: [],
|
|
5665
|
+
missing: [],
|
|
5666
|
+
facts,
|
|
5667
|
+
copy: input.copy,
|
|
5668
|
+
copyMode: "deterministic",
|
|
5669
|
+
generatedAt: input.generatedAt,
|
|
5670
|
+
computed: {}
|
|
5671
|
+
};
|
|
5672
|
+
const formats = input.config.formats ?? ["portrait"];
|
|
5673
|
+
if (formats.length === 0 || formats.includes("readme-banner")) throw new ShipsealError("story.bad-format", "The story format selection contains no supported output or includes readme-banner.", `Use ${STORY_FORMATS$1}.`);
|
|
5674
|
+
for (const [index, page] of pages.entries()) {
|
|
5675
|
+
const rendered = await generate({
|
|
5676
|
+
...input,
|
|
5677
|
+
facts: {
|
|
5678
|
+
...facts,
|
|
5679
|
+
story: [page]
|
|
5680
|
+
},
|
|
5681
|
+
config: {
|
|
5682
|
+
...input.config,
|
|
5683
|
+
formats,
|
|
5684
|
+
release: {
|
|
5685
|
+
...input.config.release,
|
|
5686
|
+
templates: ["story-page"]
|
|
5687
|
+
}
|
|
5688
|
+
}
|
|
5689
|
+
});
|
|
5690
|
+
const id = `story-${String(index + 1).padStart(2, "0")}-${page.kind}`;
|
|
5691
|
+
result.files.push(...rendered.files.map((file) => ({
|
|
5692
|
+
...file,
|
|
5693
|
+
fileName: file.fileName.replace("story-page", id)
|
|
5694
|
+
})));
|
|
5695
|
+
result.warnings.push(...rendered.warnings.map((warning) => ({
|
|
5696
|
+
...warning,
|
|
5697
|
+
slot: `page-${String(index + 1)}.${warning.slot}`
|
|
5698
|
+
})));
|
|
5699
|
+
result.missing.push(...rendered.missing);
|
|
5700
|
+
result.computed[`story.pages[${String(index)}]`] = {
|
|
5701
|
+
value: {
|
|
5702
|
+
kind: page.kind,
|
|
5703
|
+
files: result.files.filter((file) => file.fileName.startsWith(id)).map((file) => file.fileName)
|
|
5704
|
+
},
|
|
5705
|
+
computedFrom: [`story[${String(index)}].title`, `story[${String(index)}].body`]
|
|
5706
|
+
};
|
|
5707
|
+
}
|
|
5708
|
+
if (facts.release?.codeSnippet === void 0) result.missing.push({
|
|
5709
|
+
template: "story-page",
|
|
5710
|
+
fact: "release.codeSnippet",
|
|
5711
|
+
effect: "code page omitted"
|
|
5712
|
+
});
|
|
5713
|
+
if (pages.every((page) => page.kind !== "change")) result.missing.push({
|
|
5714
|
+
template: "story-page",
|
|
5715
|
+
fact: "release.features",
|
|
5716
|
+
effect: "change pages omitted because release notes have no changes"
|
|
5717
|
+
});
|
|
5718
|
+
return result;
|
|
5719
|
+
}
|
|
5720
|
+
|
|
5721
|
+
//#endregion
|
|
5722
|
+
//#region src/outputs/downloads.ts
|
|
5723
|
+
const encode = (value) => new TextEncoder().encode(value);
|
|
5724
|
+
const isJpeg = (bytes) => bytes[0] === 255 && bytes[1] === 216;
|
|
5725
|
+
function joinBytes(parts) {
|
|
5726
|
+
const result = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
|
|
5727
|
+
let offset = 0;
|
|
5728
|
+
for (const part of parts) {
|
|
5729
|
+
result.set(part, offset);
|
|
5730
|
+
offset += part.length;
|
|
5731
|
+
}
|
|
5732
|
+
return result;
|
|
5733
|
+
}
|
|
5734
|
+
/**
|
|
5735
|
+
* Image-only PDF: each JPEG becomes one page as a DCTDecode stream, so the page is the exact
|
|
5736
|
+
* rendered layout rather than a reflow of it. Pages are placed at half the pixel size, which
|
|
5737
|
+
* puts a 1080x1350 render on a 540x675pt page at 144dpi.
|
|
5738
|
+
*
|
|
5739
|
+
* Object numbering: 1 is the catalog, 2 the page tree, then each page contributes three
|
|
5740
|
+
* objects (page, image, content stream) at 3 + index * 3.
|
|
5741
|
+
*/
|
|
5742
|
+
function carouselPdf(pages) {
|
|
5743
|
+
if (pages.length === 0 || pages.some((page) => !isJpeg(page.bytes))) throw new ShipsealError("pdf.invalid-pages", "A carousel requires nonempty JPEG pages.", "Render the story as JPEG before building the PDF.");
|
|
5744
|
+
const objects = [encode("<< /Type /Catalog /Pages 2 0 R >>"), encode(`<< /Type /Pages /Count ${String(pages.length)} /Kids [${pages.map((_, index) => `${String(3 + index * 3)} 0 R`).join(" ")}] >>`)];
|
|
5745
|
+
for (const [index, page] of pages.entries()) {
|
|
5746
|
+
const id = 3 + index * 3;
|
|
5747
|
+
const width = page.width / 2;
|
|
5748
|
+
const height = page.height / 2;
|
|
5749
|
+
objects.push(encode(`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${String(width)} ${String(height)}] /Resources << /XObject << /Image ${String(id + 1)} 0 R >> >> /Contents ${String(id + 2)} 0 R >>`));
|
|
5750
|
+
objects.push(joinBytes([
|
|
5751
|
+
encode(`<< /Type /XObject /Subtype /Image /Width ${String(page.width)} /Height ${String(page.height)} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ${String(page.bytes.length)} >>\nstream\n`),
|
|
5752
|
+
page.bytes,
|
|
5753
|
+
encode("\nendstream")
|
|
5754
|
+
]));
|
|
5755
|
+
const content = `q ${String(width)} 0 0 ${String(height)} 0 0 cm /Image Do Q`;
|
|
5756
|
+
objects.push(encode(`<< /Length ${String(encode(content).length)} >>\nstream\n${content}\nendstream`));
|
|
5757
|
+
}
|
|
5758
|
+
const header = encode("%PDF-1.4\n%Shipseal\n");
|
|
5759
|
+
const parts = [header];
|
|
5760
|
+
const offsets = [];
|
|
5761
|
+
let position = header.length;
|
|
5762
|
+
for (const [index, object] of objects.entries()) {
|
|
5763
|
+
offsets.push(position);
|
|
5764
|
+
const bytes = joinBytes([
|
|
5765
|
+
encode(`${String(index + 1)} 0 obj\n`),
|
|
5766
|
+
object,
|
|
5767
|
+
encode("\nendobj\n")
|
|
5768
|
+
]);
|
|
5769
|
+
parts.push(bytes);
|
|
5770
|
+
position += bytes.length;
|
|
5771
|
+
}
|
|
5772
|
+
const size = objects.length + 1;
|
|
5773
|
+
parts.push(encode(`xref\n0 ${String(size)}\n0000000000 65535 f \n` + offsets.map((offset) => `${String(offset).padStart(10, "0")} 00000 n \n`).join("") + `trailer\n<< /Size ${String(size)} /Root 1 0 R >>\nstartxref\n${String(position)}\n%%EOF\n`));
|
|
5774
|
+
return joinBytes(parts);
|
|
5775
|
+
}
|
|
5776
|
+
function crc32(bytes) {
|
|
5777
|
+
let value = 4294967295;
|
|
5778
|
+
for (const byte of bytes) {
|
|
5779
|
+
value ^= byte;
|
|
5780
|
+
for (let bit = 0; bit < 8; bit += 1) value = value >>> 1 ^ ((value & 1) === 1 ? 3988292384 : 0);
|
|
5781
|
+
}
|
|
5782
|
+
return (value ^ 4294967295) >>> 0;
|
|
5783
|
+
}
|
|
5784
|
+
/**
|
|
5785
|
+
* ZIP with STORE and no compression: PNG and JPEG are already compressed, so deflating them
|
|
5786
|
+
* costs time and saves nothing. Every timestamp is the fixed DOS date 1980-01-01 (0x0021) so
|
|
5787
|
+
* the same inputs always produce the same archive bytes.
|
|
5788
|
+
*/
|
|
5789
|
+
function zipFiles(files) {
|
|
5790
|
+
const FIXED_DOS_DATE = 33;
|
|
5791
|
+
const names = /* @__PURE__ */ new Set();
|
|
5792
|
+
const local = [];
|
|
5793
|
+
const central = [];
|
|
5794
|
+
let offset = 0;
|
|
5795
|
+
for (const file of files) {
|
|
5796
|
+
if (!/^[a-zA-Z0-9_.-]+$/.test(file.name) || names.has(file.name)) throw new ShipsealError("zip.bad-name", "An archive file name is unsafe or duplicated.", "Use unique file names without path separators.");
|
|
5797
|
+
names.add(file.name);
|
|
5798
|
+
const name = encode(file.name);
|
|
5799
|
+
const checksum = crc32(file.bytes);
|
|
5800
|
+
const header = /* @__PURE__ */ new Uint8Array(30);
|
|
5801
|
+
const headerView = new DataView(header.buffer);
|
|
5802
|
+
headerView.setUint32(0, 67324752, true);
|
|
5803
|
+
headerView.setUint16(4, 20, true);
|
|
5804
|
+
headerView.setUint16(12, FIXED_DOS_DATE, true);
|
|
5805
|
+
headerView.setUint32(14, checksum, true);
|
|
5806
|
+
headerView.setUint32(18, file.bytes.length, true);
|
|
5807
|
+
headerView.setUint32(22, file.bytes.length, true);
|
|
5808
|
+
headerView.setUint16(26, name.length, true);
|
|
5809
|
+
local.push(header, name, file.bytes);
|
|
5810
|
+
const entry = /* @__PURE__ */ new Uint8Array(46);
|
|
5811
|
+
const entryView = new DataView(entry.buffer);
|
|
5812
|
+
entryView.setUint32(0, 33639248, true);
|
|
5813
|
+
entryView.setUint16(4, 20, true);
|
|
5814
|
+
entryView.setUint16(6, 20, true);
|
|
5815
|
+
entryView.setUint16(14, FIXED_DOS_DATE, true);
|
|
5816
|
+
entryView.setUint32(16, checksum, true);
|
|
5817
|
+
entryView.setUint32(20, file.bytes.length, true);
|
|
5818
|
+
entryView.setUint32(24, file.bytes.length, true);
|
|
5819
|
+
entryView.setUint16(28, name.length, true);
|
|
5820
|
+
entryView.setUint32(42, offset, true);
|
|
5821
|
+
central.push(entry, name);
|
|
5822
|
+
offset += header.length + name.length + file.bytes.length;
|
|
5823
|
+
}
|
|
5824
|
+
const end = /* @__PURE__ */ new Uint8Array(22);
|
|
5825
|
+
const endView = new DataView(end.buffer);
|
|
5826
|
+
endView.setUint32(0, 101010256, true);
|
|
5827
|
+
endView.setUint16(8, files.length, true);
|
|
5828
|
+
endView.setUint16(10, files.length, true);
|
|
5829
|
+
endView.setUint32(12, central.reduce((sum, part) => sum + part.length, 0), true);
|
|
5830
|
+
endView.setUint32(16, offset, true);
|
|
5831
|
+
return joinBytes([
|
|
5832
|
+
...local,
|
|
5833
|
+
...central,
|
|
5834
|
+
end
|
|
5835
|
+
]);
|
|
5836
|
+
}
|
|
5837
|
+
|
|
5838
|
+
//#endregion
|
|
5839
|
+
//#region src/studio/pack.ts
|
|
5840
|
+
/** Every format the `story-page` template declares, in the order the selector lists them. */
|
|
5841
|
+
const STORY_FORMATS = [
|
|
5842
|
+
"portrait",
|
|
5843
|
+
"square",
|
|
5844
|
+
"og",
|
|
5845
|
+
"github-social",
|
|
5846
|
+
"x",
|
|
5847
|
+
"linkedin",
|
|
5848
|
+
"producthunt"
|
|
5849
|
+
];
|
|
5850
|
+
const encodeJson = (value) => new TextEncoder().encode(`${JSON.stringify(value, null, 2)}\n`);
|
|
5851
|
+
const selectionSchema = z.object({
|
|
5852
|
+
pack: z.enum(["story", "release"]).default("story"),
|
|
5853
|
+
style: brandSchema.shape.style,
|
|
5854
|
+
theme: z.enum(["dark", "light"]),
|
|
5855
|
+
accent: brandSchema.shape.colors.shape.primary,
|
|
5856
|
+
headline: z.string().max(500).default(""),
|
|
5857
|
+
upgrade: z.string().max(4e3).default(""),
|
|
5858
|
+
format: z.enum([...STORY_FORMATS, "all"]).default("portrait")
|
|
5859
|
+
}).strict();
|
|
5860
|
+
function selectedConfig(config, selection) {
|
|
5861
|
+
const formats = selection.format === "all" ? selection.pack === "story" ? [...STORY_FORMATS] : [...V1_FORMAT_IDS] : [selection.format];
|
|
5862
|
+
const landscape = new Set(V1_FORMAT_IDS);
|
|
5863
|
+
if (selection.pack === "release" && formats.some((format) => !landscape.has(format))) throw new ShipsealError("preview.format", "The standard release templates do not support this format.", "Choose a landscape format or switch to a story pack.");
|
|
5864
|
+
return {
|
|
5865
|
+
...config,
|
|
5866
|
+
formats,
|
|
5867
|
+
release: {
|
|
5868
|
+
...config.release,
|
|
5869
|
+
headline: selection.headline || null,
|
|
5870
|
+
story: {
|
|
5871
|
+
...config.release?.story,
|
|
5872
|
+
upgrade: selection.upgrade
|
|
5873
|
+
}
|
|
5874
|
+
},
|
|
5875
|
+
output: { imageFormat: "png" }
|
|
5876
|
+
};
|
|
5877
|
+
}
|
|
5878
|
+
function selectedBrand(brand, selection) {
|
|
5879
|
+
return {
|
|
5880
|
+
...brand,
|
|
5881
|
+
style: selection.style,
|
|
5882
|
+
colors: {
|
|
5883
|
+
...brand.colors,
|
|
5884
|
+
primary: selection.accent
|
|
5885
|
+
}
|
|
5886
|
+
};
|
|
5887
|
+
}
|
|
5888
|
+
async function buildStudioPack(input) {
|
|
5889
|
+
const release = input.facts.release;
|
|
5890
|
+
if (release === void 0) throw new ShipsealError("preview.no-release", "No release is selected.", "Load a repository release first.");
|
|
5891
|
+
const config = selectedConfig(input.config, input.selection);
|
|
5892
|
+
const brand = selectedBrand(input.brand, input.selection);
|
|
5893
|
+
const facts = {
|
|
5894
|
+
...input.facts,
|
|
5895
|
+
release: { ...release }
|
|
5896
|
+
};
|
|
5897
|
+
if (input.selection.headline) facts.release = {
|
|
5898
|
+
...release,
|
|
5899
|
+
headline: fact(input.selection.headline, {
|
|
5900
|
+
source: "user-config",
|
|
5901
|
+
ref: "release.headline",
|
|
5902
|
+
fetchedAt: input.generatedAt
|
|
5903
|
+
})
|
|
5904
|
+
};
|
|
5905
|
+
const event = {
|
|
5906
|
+
kind: "release",
|
|
5907
|
+
tag: release.tag.value
|
|
5908
|
+
};
|
|
5909
|
+
const request = {
|
|
5910
|
+
event,
|
|
5911
|
+
facts,
|
|
5912
|
+
brand,
|
|
5913
|
+
config,
|
|
5914
|
+
copy: deterministicCopy(facts, config.release?.maxHighlights, brand.name),
|
|
5915
|
+
copyMode: "deterministic",
|
|
5916
|
+
renderer: input.renderer,
|
|
5917
|
+
themes: [input.selection.theme],
|
|
5918
|
+
generatedAt: input.generatedAt
|
|
5919
|
+
};
|
|
5920
|
+
if (input.logos !== void 0) request.logos = input.logos;
|
|
5921
|
+
if (input.images !== void 0) request.images = input.images;
|
|
5922
|
+
const result = await (input.selection.pack === "story" ? generateStory : generate)(request);
|
|
5923
|
+
const manifest = buildManifest({
|
|
5924
|
+
result,
|
|
5925
|
+
event,
|
|
5926
|
+
brand,
|
|
5927
|
+
shipsealVersion: input.version
|
|
5928
|
+
});
|
|
5929
|
+
manifest.brand.source = input.brandSource ?? ".shipseal/brand.json + preview selections";
|
|
5930
|
+
const downloads = result.files.map((file) => ({
|
|
5931
|
+
name: file.fileName,
|
|
5932
|
+
bytes: file.bytes
|
|
5933
|
+
}));
|
|
5934
|
+
if (input.selection.pack === "story") {
|
|
5935
|
+
const pdfFormat = config.formats?.[0] ?? "portrait";
|
|
5936
|
+
const jpeg = await generateStory({
|
|
5937
|
+
...request,
|
|
5938
|
+
config: {
|
|
5939
|
+
...config,
|
|
5940
|
+
formats: [pdfFormat],
|
|
5941
|
+
output: { imageFormat: "jpeg" }
|
|
5942
|
+
}
|
|
5943
|
+
});
|
|
5944
|
+
const pdf = carouselPdf(jpeg.files);
|
|
5945
|
+
downloads.push({
|
|
5946
|
+
name: "story.pdf",
|
|
5947
|
+
bytes: pdf
|
|
5948
|
+
});
|
|
5949
|
+
manifest.computed["story.pdf"] = {
|
|
5950
|
+
value: {
|
|
5951
|
+
format: pdfFormat,
|
|
5952
|
+
pages: jpeg.files.length,
|
|
5953
|
+
bytes: pdf.length
|
|
5954
|
+
},
|
|
5955
|
+
computedFrom: ["story"]
|
|
5956
|
+
};
|
|
5957
|
+
}
|
|
5958
|
+
downloads.push({
|
|
5959
|
+
name: "brand.json",
|
|
5960
|
+
bytes: encodeJson(brand)
|
|
5961
|
+
}, {
|
|
5962
|
+
name: "config.json",
|
|
5963
|
+
bytes: encodeJson(config)
|
|
5964
|
+
}, {
|
|
5965
|
+
name: "manifest.json",
|
|
5966
|
+
bytes: encodeJson(manifest)
|
|
5967
|
+
});
|
|
5968
|
+
return {
|
|
5969
|
+
result,
|
|
5970
|
+
manifest,
|
|
5971
|
+
downloads,
|
|
5972
|
+
archive: zipFiles(downloads),
|
|
5973
|
+
brand,
|
|
5974
|
+
config
|
|
5975
|
+
};
|
|
5976
|
+
}
|
|
5977
|
+
|
|
5978
|
+
//#endregion
|
|
5979
|
+
//#region src/commands/story.ts
|
|
5980
|
+
const STORY_IMAGE_LABELS = ["before", "after"];
|
|
5981
|
+
const MAX_STORY_IMAGE_BYTES = 5e6;
|
|
5982
|
+
/**
|
|
5983
|
+
* Screenshots are supplied by the maintainer, never generated or captured here: no headless
|
|
5984
|
+
* browser is a hard rule (AGENTS.md rule 2).
|
|
5985
|
+
*/
|
|
5986
|
+
async function readStoryImage(cwd, label, path) {
|
|
5987
|
+
let data;
|
|
5988
|
+
try {
|
|
5989
|
+
data = await readFile(resolve(cwd, path));
|
|
5990
|
+
} catch (error) {
|
|
5991
|
+
throw new ShipsealError("story.image-missing", `Could not read the ${label} screenshot: ${path}.`, "Set release.story.before and release.story.after to existing PNG or JPEG files.", { cause: error });
|
|
5992
|
+
}
|
|
5993
|
+
const png = data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71;
|
|
5994
|
+
const jpeg = data[0] === 255 && data[1] === 216;
|
|
5995
|
+
if (!png && !jpeg || data.length > MAX_STORY_IMAGE_BYTES) throw new ShipsealError("story.image-invalid", `The ${label} screenshot is not a supported image or exceeds the size limit.`, "Use a PNG or JPEG smaller than 5 MB.");
|
|
5996
|
+
return {
|
|
5997
|
+
src: `story-${label}`,
|
|
5998
|
+
data
|
|
5999
|
+
};
|
|
6000
|
+
}
|
|
6001
|
+
async function loadStoryImages(cwd, config) {
|
|
6002
|
+
const configured = STORY_IMAGE_LABELS.flatMap((label) => {
|
|
6003
|
+
const path = config.release?.story?.[label];
|
|
6004
|
+
return path === void 0 ? [] : [{
|
|
6005
|
+
label,
|
|
6006
|
+
path
|
|
6007
|
+
}];
|
|
6008
|
+
});
|
|
6009
|
+
return Promise.all(configured.map((image) => readStoryImage(cwd, image.label, image.path)));
|
|
6010
|
+
}
|
|
6011
|
+
async function runStory(flags) {
|
|
6012
|
+
const brand = await loadBrand(flags.cwd);
|
|
6013
|
+
const config = await loadConfig(flags.cwd);
|
|
6014
|
+
const tag = flags.tag ?? await gitCurrentTag(flags.cwd);
|
|
6015
|
+
if (tag === void 0) throw new ShipsealError("story.no-tag", "No release tag was found.", "Pass --tag with an existing release tag.");
|
|
6016
|
+
const parsed = selectionSchema.safeParse({
|
|
6017
|
+
pack: "story",
|
|
6018
|
+
style: flags.style ?? brand.style,
|
|
6019
|
+
theme: flags.theme ?? brand.theme,
|
|
6020
|
+
accent: brand.colors.primary ?? "#ff4d4d",
|
|
6021
|
+
headline: flags.headline ?? config.release?.headline ?? "",
|
|
6022
|
+
upgrade: config.release?.story?.upgrade ?? "",
|
|
6023
|
+
format: flags.format ?? "portrait"
|
|
6024
|
+
});
|
|
6025
|
+
if (!parsed.success) {
|
|
6026
|
+
const fields = [...new Set(parsed.error.issues.map((issue) => issue.path.join(".")))];
|
|
6027
|
+
throw new ShipsealError("story.bad-options", `These story options are not valid: ${fields.join(", ")}.`, "Use --format portrait, square, og, github-social, x, linkedin, producthunt or all; --style minimal, editorial or terminal; --theme dark or light.", { cause: parsed.error });
|
|
6028
|
+
}
|
|
6029
|
+
const selection = parsed.data;
|
|
6030
|
+
const collectOptions = {
|
|
6031
|
+
cwd: flags.cwd,
|
|
6032
|
+
event: {
|
|
6033
|
+
kind: "release",
|
|
6034
|
+
tag
|
|
6035
|
+
},
|
|
6036
|
+
skipNetwork: true
|
|
6037
|
+
};
|
|
6038
|
+
if (flags.package !== void 0) collectOptions.packagePath = flags.package;
|
|
6039
|
+
if (config.release?.changelogPath !== void 0) collectOptions.changelogPath = config.release.changelogPath;
|
|
6040
|
+
if (config.release?.snippet !== void 0) collectOptions.snippet = config.release.snippet;
|
|
6041
|
+
const input = {
|
|
6042
|
+
facts: await collectFacts(collectOptions),
|
|
6043
|
+
brand,
|
|
6044
|
+
config,
|
|
6045
|
+
selection,
|
|
6046
|
+
renderer: await createTakumiRenderer(),
|
|
6047
|
+
version: readShipsealVersion(),
|
|
6048
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6049
|
+
images: await loadStoryImages(flags.cwd, config),
|
|
6050
|
+
brandSource: ".shipseal/brand.json + story flags"
|
|
6051
|
+
};
|
|
6052
|
+
const logos = await loadLogos(flags.cwd, brand);
|
|
6053
|
+
if (logos !== void 0) input.logos = logos;
|
|
6054
|
+
const pack = await buildStudioPack(input);
|
|
6055
|
+
const dir = resolve(flags.cwd, flags.out ?? config.outputDir ?? ".shipseal/output", `story-${tag.replaceAll(/[^a-zA-Z0-9_.-]/g, "-")}`);
|
|
6056
|
+
await mkdir(dir, { recursive: true });
|
|
6057
|
+
await Promise.all(pack.downloads.map((file) => writeFile(join(dir, file.name), file.bytes)));
|
|
6058
|
+
await writeFile(join(dir, "story.zip"), pack.archive);
|
|
6059
|
+
return {
|
|
6060
|
+
dir,
|
|
6061
|
+
manifest: pack.manifest,
|
|
6062
|
+
exitCode: flags.strict === true && pack.manifest.warnings.length > 0 ? 2 : 0
|
|
6063
|
+
};
|
|
6064
|
+
}
|
|
6065
|
+
|
|
6066
|
+
//#endregion
|
|
6067
|
+
//#region src/commands/preview.ts
|
|
6068
|
+
/** Localhost only, so the page is served inline rather than from a template file. */
|
|
6069
|
+
const PAGE = `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
6070
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"><title>Shipseal preview</title>
|
|
6071
|
+
</head><body style="margin:0;background:#0c0c0f"><main id="studio"></main><script type="module">
|
|
6072
|
+
import { mountStudio } from '/studio-client.js';
|
|
6073
|
+
const token = new URL(location.href).searchParams.get('token');
|
|
6074
|
+
async function api(path, value) {
|
|
6075
|
+
const response = await fetch(path, {
|
|
6076
|
+
method: value === undefined ? 'GET' : 'POST',
|
|
6077
|
+
headers: { 'Content-Type': 'application/json', 'X-Shipseal-Token': token },
|
|
6078
|
+
...(value === undefined ? {} : { body: JSON.stringify(value) }),
|
|
6079
|
+
});
|
|
6080
|
+
const body = await response.json();
|
|
6081
|
+
if (!response.ok) throw new Error(body.error);
|
|
6082
|
+
return body;
|
|
6083
|
+
}
|
|
6084
|
+
mountStudio(document.getElementById('studio'), {
|
|
6085
|
+
mode: 'local',
|
|
6086
|
+
load: () => api('/api/state'),
|
|
6087
|
+
render: (value) => api('/api/render', value),
|
|
6088
|
+
save: (value) => api('/api/save', value),
|
|
6089
|
+
});
|
|
6090
|
+
<\/script></body></html>`;
|
|
6091
|
+
const PAGE_HEADERS = {
|
|
6092
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
6093
|
+
"Cache-Control": "no-store",
|
|
6094
|
+
"Referrer-Policy": "no-referrer",
|
|
6095
|
+
"Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' blob:; connect-src 'self'; frame-ancestors 'none'"
|
|
6096
|
+
};
|
|
6097
|
+
function json(response, status, value) {
|
|
6098
|
+
response.writeHead(status, {
|
|
6099
|
+
"Content-Type": "application/json",
|
|
6100
|
+
"Cache-Control": "no-store",
|
|
6101
|
+
"X-Content-Type-Options": "nosniff"
|
|
6102
|
+
});
|
|
6103
|
+
response.end(JSON.stringify(value));
|
|
6104
|
+
}
|
|
6105
|
+
async function readSelection(request) {
|
|
6106
|
+
let text = "";
|
|
6107
|
+
for await (const value of request) {
|
|
6108
|
+
if (!Buffer.isBuffer(value)) throw new ShipsealError("preview.body", "The request body is not valid bytes.", "Reload the preview.");
|
|
6109
|
+
text += value.toString("utf8");
|
|
6110
|
+
if (text.length > 16384) throw new ShipsealError("preview.body-size", "The preview request is too large.", "Shorten the headline or upgrade instructions.");
|
|
6111
|
+
}
|
|
6112
|
+
try {
|
|
6113
|
+
const raw = JSON.parse(text);
|
|
6114
|
+
return selectionSchema.parse(raw);
|
|
6115
|
+
} catch (error) {
|
|
6116
|
+
throw new ShipsealError("preview.selection", "The preview options are invalid.", "Reload the preview and choose supported options.", { cause: error });
|
|
6117
|
+
}
|
|
6118
|
+
}
|
|
6119
|
+
/** Write through a temporary file so an interrupted save never truncates the original. */
|
|
6120
|
+
async function writeAtomic(destination, value) {
|
|
6121
|
+
const temporary = `${destination}.${randomUUID()}.tmp`;
|
|
6122
|
+
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
6123
|
+
await rename(temporary, destination);
|
|
6124
|
+
}
|
|
6125
|
+
async function startPreview(options) {
|
|
6126
|
+
const resolvedTag = options.tag ?? await gitCurrentTag(options.cwd);
|
|
6127
|
+
if (resolvedTag === void 0) throw new ShipsealError("preview.no-tag", "No release tag was found.", "Pass --tag with an existing release tag.");
|
|
6128
|
+
const tag = resolvedTag;
|
|
6129
|
+
await loadBrand(options.cwd);
|
|
6130
|
+
const token = randomUUID();
|
|
6131
|
+
let origin = "";
|
|
6132
|
+
async function handle(request, response) {
|
|
6133
|
+
if (request.headers.host !== new URL(origin).host) {
|
|
6134
|
+
json(response, 403, { error: "Invalid preview host. Open the localhost URL printed by Shipseal." });
|
|
6135
|
+
return;
|
|
6136
|
+
}
|
|
6137
|
+
const url = new URL(request.url ?? "/", origin);
|
|
6138
|
+
if (request.method === "GET" && url.pathname === "/") {
|
|
6139
|
+
response.writeHead(200, PAGE_HEADERS);
|
|
6140
|
+
response.end(PAGE);
|
|
6141
|
+
return;
|
|
6142
|
+
}
|
|
6143
|
+
if (request.method === "GET" && url.pathname === "/studio-client.js") {
|
|
6144
|
+
const script = await readFile(join(resolvePackageRoot(), "dist", "studio-client.js"));
|
|
6145
|
+
response.writeHead(200, {
|
|
6146
|
+
"Content-Type": "text/javascript",
|
|
6147
|
+
"Cache-Control": "no-store"
|
|
6148
|
+
});
|
|
6149
|
+
response.end(script);
|
|
6150
|
+
return;
|
|
6151
|
+
}
|
|
6152
|
+
if (request.headers["x-shipseal-token"] !== token || request.method === "POST" && request.headers.origin !== origin) {
|
|
6153
|
+
json(response, 403, { error: "This preview request is not authorized. Reload the localhost URL printed by Shipseal." });
|
|
6154
|
+
return;
|
|
6155
|
+
}
|
|
6156
|
+
const brand = await loadBrand(options.cwd);
|
|
6157
|
+
const config = await loadConfig(options.cwd);
|
|
6158
|
+
const collect = {
|
|
6159
|
+
cwd: options.cwd,
|
|
6160
|
+
event: {
|
|
6161
|
+
kind: "release",
|
|
6162
|
+
tag
|
|
6163
|
+
},
|
|
6164
|
+
skipNetwork: true
|
|
6165
|
+
};
|
|
6166
|
+
if (options.package !== void 0) collect.packagePath = options.package;
|
|
6167
|
+
if (config.release?.changelogPath !== void 0) collect.changelogPath = config.release.changelogPath;
|
|
6168
|
+
if (config.release?.snippet !== void 0) collect.snippet = config.release.snippet;
|
|
6169
|
+
if (request.method === "GET" && url.pathname === "/api/state") {
|
|
6170
|
+
const facts = await collectFacts(collect);
|
|
6171
|
+
json(response, 200, {
|
|
6172
|
+
name: brand.name,
|
|
6173
|
+
style: brand.style,
|
|
6174
|
+
theme: brand.theme,
|
|
6175
|
+
accent: brand.colors.primary ?? "#ff4d4d",
|
|
6176
|
+
headline: config.release?.headline ?? "",
|
|
6177
|
+
upgrade: config.release?.story?.upgrade ?? "",
|
|
6178
|
+
headlines: [...facts.release?.breaking ?? [], ...facts.release?.features ?? []].map((item) => cleanLine(firstSentence(item.value))),
|
|
6179
|
+
notes: ["Local facts come from your checkout. Change files and refresh this page to reload them. Screenshots use release.story.before and release.story.after in config.json."],
|
|
6180
|
+
releases: [{
|
|
6181
|
+
tag,
|
|
6182
|
+
label: tag
|
|
6183
|
+
}],
|
|
6184
|
+
tag
|
|
6185
|
+
});
|
|
6186
|
+
return;
|
|
6187
|
+
}
|
|
6188
|
+
if (request.method !== "POST") {
|
|
6189
|
+
json(response, 404, { error: "Preview route not found." });
|
|
6190
|
+
return;
|
|
6191
|
+
}
|
|
6192
|
+
const selection = await readSelection(request);
|
|
6193
|
+
if (url.pathname === "/api/save") {
|
|
6194
|
+
const nextConfig = selectedConfig(config, selection);
|
|
6195
|
+
nextConfig.formats = config.formats;
|
|
6196
|
+
await Promise.all([writeAtomic(join(options.cwd, ".shipseal", "brand.json"), selectedBrand(brand, selection)), writeAtomic(join(options.cwd, ".shipseal", "config.json"), nextConfig)]);
|
|
6197
|
+
json(response, 200, { saved: true });
|
|
6198
|
+
return;
|
|
6199
|
+
}
|
|
6200
|
+
if (url.pathname !== "/api/render") {
|
|
6201
|
+
json(response, 404, { error: "Preview route not found." });
|
|
6202
|
+
return;
|
|
6203
|
+
}
|
|
6204
|
+
const packInput = {
|
|
6205
|
+
facts: await collectFacts(collect),
|
|
6206
|
+
brand,
|
|
6207
|
+
config,
|
|
6208
|
+
selection,
|
|
6209
|
+
renderer: await createTakumiRenderer(),
|
|
6210
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6211
|
+
version: readShipsealVersion(),
|
|
6212
|
+
images: await loadStoryImages(options.cwd, config)
|
|
6213
|
+
};
|
|
6214
|
+
const logos = await loadLogos(options.cwd, brand);
|
|
6215
|
+
if (logos !== void 0) packInput.logos = logos;
|
|
6216
|
+
const pack = await buildStudioPack(packInput);
|
|
6217
|
+
const output = {
|
|
6218
|
+
files: pack.result.files.map((file) => ({
|
|
6219
|
+
name: file.fileName,
|
|
6220
|
+
data: Buffer.from(file.bytes).toString("base64"),
|
|
6221
|
+
width: file.width,
|
|
6222
|
+
height: file.height
|
|
6223
|
+
})),
|
|
6224
|
+
archive: Buffer.from(pack.archive).toString("base64"),
|
|
6225
|
+
manifest: pack.manifest
|
|
6226
|
+
};
|
|
6227
|
+
const pdf = pack.downloads.find((file) => file.name === "story.pdf");
|
|
6228
|
+
if (pdf !== void 0) output.pdf = Buffer.from(pdf.bytes).toString("base64");
|
|
6229
|
+
json(response, 200, output);
|
|
6230
|
+
}
|
|
6231
|
+
const server = createServer((request, response) => {
|
|
6232
|
+
handle(request, response).catch((error) => {
|
|
6233
|
+
json(response, 400, { error: error instanceof ShipsealError ? formatError(error) : `${error instanceof Error ? error.message : "Preview failed"}. Check the files and retry.` });
|
|
6234
|
+
});
|
|
6235
|
+
});
|
|
6236
|
+
await new Promise((accept, reject) => {
|
|
6237
|
+
server.once("error", reject);
|
|
6238
|
+
server.listen(options.port ?? 4175, "127.0.0.1", () => {
|
|
6239
|
+
server.removeListener("error", reject);
|
|
6240
|
+
accept();
|
|
6241
|
+
});
|
|
6242
|
+
});
|
|
6243
|
+
const address = server.address();
|
|
6244
|
+
if (address === null || typeof address === "string") throw new ShipsealError("preview.listen", "The preview server could not start.", "Choose another --port and retry.");
|
|
6245
|
+
origin = `http://127.0.0.1:${String(address.port)}`;
|
|
6246
|
+
return {
|
|
6247
|
+
server,
|
|
6248
|
+
url: `${origin}/?token=${token}`,
|
|
6249
|
+
token
|
|
6250
|
+
};
|
|
6251
|
+
}
|
|
6252
|
+
async function runPreview(options) {
|
|
6253
|
+
let preview;
|
|
6254
|
+
try {
|
|
6255
|
+
preview = await startPreview(options);
|
|
6256
|
+
} catch (error) {
|
|
6257
|
+
if (error instanceof Error && "code" in error && error.code === "EADDRINUSE") throw new ShipsealError("preview.port", "The preview port is already in use.", "Pass --port with another available port.", { cause: error });
|
|
6258
|
+
throw error;
|
|
6259
|
+
}
|
|
6260
|
+
process.stdout.write(`Preview: ${preview.url}\nPress Ctrl+C to stop.\n`);
|
|
6261
|
+
await new Promise((accept) => {
|
|
6262
|
+
const stop = () => {
|
|
6263
|
+
preview.server.close();
|
|
6264
|
+
preview.server.closeAllConnections();
|
|
6265
|
+
};
|
|
6266
|
+
process.once("SIGINT", stop);
|
|
6267
|
+
process.once("SIGTERM", stop);
|
|
6268
|
+
preview.server.once("close", () => {
|
|
6269
|
+
process.removeListener("SIGINT", stop);
|
|
6270
|
+
process.removeListener("SIGTERM", stop);
|
|
6271
|
+
accept();
|
|
6272
|
+
});
|
|
6273
|
+
});
|
|
6274
|
+
}
|
|
6275
|
+
|
|
4980
6276
|
//#endregion
|
|
4981
6277
|
//#region src/cli.ts
|
|
4982
6278
|
async function runCli(argv = process.argv) {
|
|
@@ -5004,6 +6300,46 @@ async function runCli(argv = process.argv) {
|
|
|
5004
6300
|
for (const note of result.detection.notes) process.stdout.write(` note: ${note}\n`);
|
|
5005
6301
|
}
|
|
5006
6302
|
});
|
|
6303
|
+
cli.command("story", "Generate an ordered release story, PDF carousel and ZIP").option("--tag <tag>", "Release tag").option("--format <format>", "portrait, square, og, github-social, x, linkedin, producthunt, or all").option("--style <style>", "minimal, editorial, or terminal").option("--theme <theme>", "dark or light").option("--headline <text>", "Override the cover headline").option("--out <dir>", "Output directory").option("--package <path>", "package.json path for monorepos").option("--strict", "Fit warnings exit with code 2").option("--no-copy", "Use source text only (stories always use deterministic copy)").action(async (flags) => {
|
|
6304
|
+
const options = {
|
|
6305
|
+
cwd: stringFlag(flags.cwd, process.cwd()),
|
|
6306
|
+
strict: flags.strict === true
|
|
6307
|
+
};
|
|
6308
|
+
for (const key of [
|
|
6309
|
+
"tag",
|
|
6310
|
+
"format",
|
|
6311
|
+
"style",
|
|
6312
|
+
"theme",
|
|
6313
|
+
"headline",
|
|
6314
|
+
"out",
|
|
6315
|
+
"package"
|
|
6316
|
+
]) {
|
|
6317
|
+
const value = optionalString(flags[key]);
|
|
6318
|
+
if (value !== void 0) options[key] = value;
|
|
6319
|
+
}
|
|
6320
|
+
const result = await runStory(options);
|
|
6321
|
+
process.exitCode = result.exitCode;
|
|
6322
|
+
if (flags.json === true) writeJson(result);
|
|
6323
|
+
else if (flags.quiet !== true) writeHumanResult({
|
|
6324
|
+
...result,
|
|
6325
|
+
dryRun: false,
|
|
6326
|
+
facts: result.manifest.facts,
|
|
6327
|
+
warnings: result.manifest.warnings
|
|
6328
|
+
});
|
|
6329
|
+
});
|
|
6330
|
+
cli.command("preview", "Preview release and story packs locally, then save your choices").option("--tag <tag>", "Release tag").option("--port <port>", "Localhost port (default: 4175)").option("--package <path>", "package.json path for monorepos").action(async (flags) => {
|
|
6331
|
+
const options = { cwd: stringFlag(flags.cwd, process.cwd()) };
|
|
6332
|
+
for (const key of ["tag", "package"]) {
|
|
6333
|
+
const value = optionalString(flags[key]);
|
|
6334
|
+
if (value !== void 0) options[key] = value;
|
|
6335
|
+
}
|
|
6336
|
+
if (flags.port !== void 0) {
|
|
6337
|
+
const port = Number(flags.port);
|
|
6338
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new ShipsealError("preview.port", "The preview port is invalid.", "Pass an integer from 0 through 65535.");
|
|
6339
|
+
options.port = port;
|
|
6340
|
+
}
|
|
6341
|
+
await runPreview(options);
|
|
6342
|
+
});
|
|
5007
6343
|
cli.command("doctor", "Check Node, git, brand, fonts, and Takumi").action(async (flags) => {
|
|
5008
6344
|
const result = await runDoctor(stringFlag(flags.cwd, process.cwd()));
|
|
5009
6345
|
if (flags.json === true) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
@@ -5014,7 +6350,7 @@ async function runCli(argv = process.argv) {
|
|
|
5014
6350
|
}
|
|
5015
6351
|
if (!result.ok) process.exitCode = 1;
|
|
5016
6352
|
});
|
|
5017
|
-
cli.command("release", "Generate the release pack").option("--tag <tag>", "Release tag").option("--from <tag>", "Previous tag").option("--formats <list>", "Comma-separated formats").option("--templates <list>", "Comma-separated templates").option("--themes <dark|light|both>", "Themes to render").option("--copy", "Allow optional LLM copy (default: on when config.copy.llm is true)").option("--out <dir>", "Output directory").option("--strict", "Fit warnings exit with code 2").option("--dry-run", "Collect facts and print them; render nothing").option("--package <path>", "package.json path for monorepos").option("--upload", "Upload PNG files to the GitHub release").action(async (flags) => {
|
|
6353
|
+
cli.command("release", "Generate the release pack").option("--tag <tag>", "Release tag").option("--from <tag>", "Previous tag").option("--formats <list>", "Comma-separated formats").option("--templates <list>", "Comma-separated templates").option("--themes <dark|light|both>", "Themes to render").option("--copy", "Allow optional LLM copy (default: on when config.copy.llm is true)").option("--out <dir>", "Output directory").option("--strict", "Fit warnings exit with code 2").option("--dry-run", "Collect facts and print them; render nothing").option("--package <path>", "package.json path for monorepos").option("--headline <text>", "Headline for the cards, overriding the changelog").option("--subheadline <text>", "Subheadline for the cards").option("--upload", "Upload PNG files to the GitHub release").action(async (flags) => {
|
|
5018
6354
|
const releaseFlags = {
|
|
5019
6355
|
cwd: stringFlag(flags.cwd, process.cwd()),
|
|
5020
6356
|
strict: flags.strict === true,
|
|
@@ -5023,6 +6359,10 @@ async function runCli(argv = process.argv) {
|
|
|
5023
6359
|
applySharedFlags(releaseFlags, flags);
|
|
5024
6360
|
const tag = optionalString(flags.tag);
|
|
5025
6361
|
if (tag !== void 0) releaseFlags.tag = tag;
|
|
6362
|
+
const headline = optionalString(flags.headline);
|
|
6363
|
+
if (headline !== void 0) releaseFlags.headline = headline;
|
|
6364
|
+
const subheadline = optionalString(flags.subheadline);
|
|
6365
|
+
if (subheadline !== void 0) releaseFlags.subheadline = subheadline;
|
|
5026
6366
|
const from = optionalString(flags.from);
|
|
5027
6367
|
if (from !== void 0) releaseFlags.from = from;
|
|
5028
6368
|
const templates = optionalString(flags.templates);
|