shipseal 0.0.5 → 0.0.7
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 +224 -135
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/cli.js
CHANGED
|
@@ -307,8 +307,11 @@ function deterministicCopy(facts, maxHighlights = 4) {
|
|
|
307
307
|
const features = (facts.release?.features ?? []).map((item) => item.value);
|
|
308
308
|
const fixes = (facts.release?.fixes ?? []).map((item) => item.value);
|
|
309
309
|
const breaking = (facts.release?.breaking ?? []).map((item) => item.value);
|
|
310
|
-
const
|
|
311
|
-
|
|
310
|
+
const headline = [
|
|
311
|
+
...breaking,
|
|
312
|
+
...features,
|
|
313
|
+
...fixes
|
|
314
|
+
].map((line) => cleanLine(firstSentence(line))).find((line) => line.length <= HEADLINE_MAX_CHARS) ?? (version === void 0 ? name : `${name} ${version}`);
|
|
312
315
|
const tagline = facts.project.tagline?.value;
|
|
313
316
|
const subheadline = tagline !== void 0 && tagline.length > 0 ? cleanLine(tagline) : cleanLine(features[1] ?? fixes[1] ?? fixes[0] ?? `What's new in ${version ?? name}`);
|
|
314
317
|
const highlights = [];
|
|
@@ -319,12 +322,27 @@ function deterministicCopy(facts, maxHighlights = 4) {
|
|
|
319
322
|
for (const line of breaking) addHighlight(line, "Breaking: ");
|
|
320
323
|
for (const line of features) addHighlight(line);
|
|
321
324
|
for (const line of fixes) addHighlight(line);
|
|
322
|
-
|
|
325
|
+
const copy = {
|
|
323
326
|
headline,
|
|
324
327
|
subheadline,
|
|
325
328
|
highlights,
|
|
326
329
|
cta: ctaFor(facts)
|
|
327
330
|
};
|
|
331
|
+
const snippet = facts.release?.codeSnippet?.value.code;
|
|
332
|
+
if (snippet !== void 0 && isInstallOnly(snippet)) copy.codeTitle = "Get started";
|
|
333
|
+
return copy;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Is this snippet nothing but install commands?
|
|
337
|
+
*
|
|
338
|
+
* Pairing "npm i thing" with a release headline reads as though the release was about
|
|
339
|
+
* installing, which is how v0.0.6 shipped a bug-fix headline over the README's install block.
|
|
340
|
+
* Install commands are still worth showing, they just need their own title.
|
|
341
|
+
*/
|
|
342
|
+
const INSTALL_COMMAND = /^\s*(?:\$\s*)?(?:npm\s+(?:i|install|add)|pnpm\s+(?:i|install|add|dlx)|yarn\s+(?:add|install)|bun\s+(?:a|add|install)|npx|deno\s+add|pip\s+install|cargo\s+add|go\s+get|gem\s+install|brew\s+install)\b/;
|
|
343
|
+
function isInstallOnly(code) {
|
|
344
|
+
const lines = code.split("\n").map((line) => line.replace(/\s+#.*$/, "").trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
345
|
+
return lines.length > 0 && lines.every((line) => INSTALL_COMMAND.test(line));
|
|
328
346
|
}
|
|
329
347
|
function formatCount$1(value) {
|
|
330
348
|
return value.toLocaleString("en-US");
|
|
@@ -363,6 +381,12 @@ function firstSentence(text) {
|
|
|
363
381
|
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
364
382
|
return /^(.+?[.?!])\s+[A-Z]/.exec(collapsed)?.[1] ?? collapsed;
|
|
365
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* Longest headline that fits two lines at the minimum font size on a 1200 wide card.
|
|
386
|
+
* Measured rather than guessed: §13.1 originally said 48, which rejects most real changelog
|
|
387
|
+
* sentences, and the fitting pass comfortably handles more.
|
|
388
|
+
*/
|
|
389
|
+
const HEADLINE_MAX_CHARS = 72;
|
|
366
390
|
function cleanLine(text) {
|
|
367
391
|
const stripped = text.replace(/\s*\u2014\s*/g, ": ").replace(/\s+\u2013\s+/g, ", ").replaceAll("–", "-").replaceAll("!", ".").replace(/\s+/g, " ").trim();
|
|
368
392
|
const noTrail = stripped.endsWith(".") ? stripped.slice(0, -1) : stripped;
|
|
@@ -579,7 +603,7 @@ const codeCard = {
|
|
|
579
603
|
});
|
|
580
604
|
return {
|
|
581
605
|
props: {
|
|
582
|
-
headline: copy.headline,
|
|
606
|
+
headline: copy.codeTitle ?? copy.headline,
|
|
583
607
|
headingFamily: brand.fonts.heading.family,
|
|
584
608
|
monoFamily: brand.fonts.mono.family,
|
|
585
609
|
headingWeight: brand.fonts.heading.weight,
|
|
@@ -1460,7 +1484,9 @@ const releaseHighlights = {
|
|
|
1460
1484
|
style: {
|
|
1461
1485
|
display: "flex",
|
|
1462
1486
|
flexDirection: "column",
|
|
1463
|
-
|
|
1487
|
+
flexGrow: 1,
|
|
1488
|
+
justifyContent: props.highlights.length > 2 ? "flex-start" : "center",
|
|
1489
|
+
gap: props.highlights.length > 2 ? 16 : 20
|
|
1464
1490
|
},
|
|
1465
1491
|
children: props.highlights.map((line, index) => {
|
|
1466
1492
|
const fitted = ctx.fitted[`highlight-${String(index)}`];
|
|
@@ -1684,6 +1710,20 @@ async function copyForTemplate(templateId, copy, facts, theme) {
|
|
|
1684
1710
|
};
|
|
1685
1711
|
}
|
|
1686
1712
|
|
|
1713
|
+
//#endregion
|
|
1714
|
+
//#region src/facts/fact.ts
|
|
1715
|
+
function fact(value, provenance) {
|
|
1716
|
+
const fetchedAt = provenance.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1717
|
+
return {
|
|
1718
|
+
value,
|
|
1719
|
+
provenance: {
|
|
1720
|
+
source: provenance.source,
|
|
1721
|
+
ref: provenance.ref,
|
|
1722
|
+
fetchedAt
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1687
1727
|
//#endregion
|
|
1688
1728
|
//#region src/facts/merge.ts
|
|
1689
1729
|
function mergeFacts(parts) {
|
|
@@ -1756,20 +1796,6 @@ function isCompleteRelease(release) {
|
|
|
1756
1796
|
return release.version !== void 0 && release.tag !== void 0 && release.date !== void 0;
|
|
1757
1797
|
}
|
|
1758
1798
|
|
|
1759
|
-
//#endregion
|
|
1760
|
-
//#region src/facts/fact.ts
|
|
1761
|
-
function fact(value, provenance) {
|
|
1762
|
-
const fetchedAt = provenance.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1763
|
-
return {
|
|
1764
|
-
value,
|
|
1765
|
-
provenance: {
|
|
1766
|
-
source: provenance.source,
|
|
1767
|
-
ref: provenance.ref,
|
|
1768
|
-
fetchedAt
|
|
1769
|
-
}
|
|
1770
|
-
};
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
1799
|
//#endregion
|
|
1774
1800
|
//#region src/sources/bench-file.ts
|
|
1775
1801
|
const metricSchema = z.object({
|
|
@@ -1844,6 +1870,117 @@ async function collectBenchFile(cwd, filePath = ".shipseal/bench.json") {
|
|
|
1844
1870
|
return { bench };
|
|
1845
1871
|
}
|
|
1846
1872
|
|
|
1873
|
+
//#endregion
|
|
1874
|
+
//#region src/sources/readme.ts
|
|
1875
|
+
async function collectReadme(cwd) {
|
|
1876
|
+
let markdown;
|
|
1877
|
+
try {
|
|
1878
|
+
markdown = await readFile(join(cwd, "README.md"), "utf8");
|
|
1879
|
+
} catch {
|
|
1880
|
+
return {};
|
|
1881
|
+
}
|
|
1882
|
+
const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1883
|
+
const out = {};
|
|
1884
|
+
const h1 = extractH1(markdown);
|
|
1885
|
+
const tagline = extractTagline(markdown);
|
|
1886
|
+
const snippet = extractFirstCodeFence(markdown);
|
|
1887
|
+
const project = {};
|
|
1888
|
+
if (h1 !== void 0) project.name = fact(h1, {
|
|
1889
|
+
source: "readme",
|
|
1890
|
+
ref: "README.md H1",
|
|
1891
|
+
fetchedAt
|
|
1892
|
+
});
|
|
1893
|
+
if (tagline !== void 0) project.tagline = fact(tagline, {
|
|
1894
|
+
source: "readme",
|
|
1895
|
+
ref: "README.md first paragraph",
|
|
1896
|
+
fetchedAt
|
|
1897
|
+
});
|
|
1898
|
+
if (project.name !== void 0 || project.tagline !== void 0) out.project = project;
|
|
1899
|
+
if (snippet !== void 0) out.release = { codeSnippet: fact(snippet, {
|
|
1900
|
+
source: "readme",
|
|
1901
|
+
ref: "README.md first fenced code block",
|
|
1902
|
+
fetchedAt
|
|
1903
|
+
}) };
|
|
1904
|
+
return out;
|
|
1905
|
+
}
|
|
1906
|
+
async function collectConfiguredSnippet(cwd, spec) {
|
|
1907
|
+
const parsed = parseSnippetSpec(spec);
|
|
1908
|
+
if (parsed === void 0) return {};
|
|
1909
|
+
let contents;
|
|
1910
|
+
try {
|
|
1911
|
+
contents = await readFile(join(cwd, parsed.path), "utf8");
|
|
1912
|
+
} catch {
|
|
1913
|
+
return {};
|
|
1914
|
+
}
|
|
1915
|
+
const slice = contents.split(/\r?\n/).slice(parsed.start - 1, parsed.end).join("\n");
|
|
1916
|
+
const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1917
|
+
return { release: { codeSnippet: fact({
|
|
1918
|
+
code: slice,
|
|
1919
|
+
lang: langFromPath(parsed.path)
|
|
1920
|
+
}, {
|
|
1921
|
+
source: "user-config",
|
|
1922
|
+
ref: spec,
|
|
1923
|
+
fetchedAt
|
|
1924
|
+
}) } };
|
|
1925
|
+
}
|
|
1926
|
+
/**
|
|
1927
|
+
* Strip fenced code and HTML before reading prose out of a README.
|
|
1928
|
+
*
|
|
1929
|
+
* Skipping blocks that merely *start* with a fence is not enough. A `#` comment inside a YAML
|
|
1930
|
+
* example reads as a markdown H1, and a blank line inside a fence makes its body look like a
|
|
1931
|
+
* paragraph. Shipseal's own README hit both: the detected name became
|
|
1932
|
+
* ".github/workflows/shipseal.yml" and the tagline became a chunk of workflow YAML.
|
|
1933
|
+
*/
|
|
1934
|
+
function stripNonProse(markdown) {
|
|
1935
|
+
return markdown.replace(/```[\s\S]*?```/g, "").replace(/~~~[\s\S]*?~~~/g, "").replace(/<[^>]+>/g, "");
|
|
1936
|
+
}
|
|
1937
|
+
function extractH1(markdown) {
|
|
1938
|
+
const heading = /^#\s+(.+)$/m.exec(stripNonProse(markdown))?.[1];
|
|
1939
|
+
if (heading === void 0) return;
|
|
1940
|
+
return heading.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").trim();
|
|
1941
|
+
}
|
|
1942
|
+
function extractTagline(markdown) {
|
|
1943
|
+
const prose = stripNonProse(markdown);
|
|
1944
|
+
const h1 = /^#\s+.+$/m.exec(prose);
|
|
1945
|
+
const start = h1 === null ? 0 : (h1.index ?? 0) + h1[0].length;
|
|
1946
|
+
for (const block of prose.slice(start).split(/\n\s*\n/)) {
|
|
1947
|
+
const cleaned = stripBadges(block).trim();
|
|
1948
|
+
if (cleaned.length >= 20 && !cleaned.startsWith("#") && !cleaned.startsWith("```")) return cleaned;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
function extractFirstCodeFence(markdown) {
|
|
1952
|
+
const match = /```([a-zA-Z0-9+-]+)\n([\s\S]*?)```/.exec(markdown);
|
|
1953
|
+
if (match === null || match[1] === void 0 || match[2] === void 0) return;
|
|
1954
|
+
return {
|
|
1955
|
+
lang: match[1],
|
|
1956
|
+
code: match[2].replace(/\n$/, "")
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
function parseSnippetSpec(spec) {
|
|
1960
|
+
const match = /^(?<path>.+)#L(?<start>\d+)(?:-L(?<end>\d+))?$/.exec(spec);
|
|
1961
|
+
if (match === null || match.groups === void 0) return;
|
|
1962
|
+
const path = match.groups.path;
|
|
1963
|
+
const start = Number.parseInt(match.groups.start ?? "0", 10);
|
|
1964
|
+
const end = Number.parseInt(match.groups.end ?? match.groups.start ?? "0", 10);
|
|
1965
|
+
if (path === void 0 || start < 1 || end < start) return;
|
|
1966
|
+
return {
|
|
1967
|
+
path,
|
|
1968
|
+
start,
|
|
1969
|
+
end
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
function langFromPath(path) {
|
|
1973
|
+
const ext = extname(path).replace(".", "");
|
|
1974
|
+
if (ext === "ts") return "ts";
|
|
1975
|
+
if (ext === "tsx") return "tsx";
|
|
1976
|
+
if (ext === "js" || ext === "mjs" || ext === "cjs") return "js";
|
|
1977
|
+
if (ext === "jsx") return "jsx";
|
|
1978
|
+
return ext.length > 0 ? ext : "text";
|
|
1979
|
+
}
|
|
1980
|
+
function stripBadges(text) {
|
|
1981
|
+
return text.replace(/\[!\[[^\]]*]\([^)]+\)]\([^)]+\)/g, "").replace(/!\[[^\]]*]\([^)]+\)/g, "").replace(/<img[^>]*>/gi, "").replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1847
1984
|
//#endregion
|
|
1848
1985
|
//#region src/sources/changelog.ts
|
|
1849
1986
|
async function collectChangelog(cwd, version, changelogPath = "CHANGELOG.md") {
|
|
@@ -1891,6 +2028,15 @@ async function collectChangelog(cwd, version, changelogPath = "CHANGELOG.md") {
|
|
|
1891
2028
|
fixes,
|
|
1892
2029
|
breaking
|
|
1893
2030
|
} };
|
|
2031
|
+
const snippet = extractFirstCodeFence(section.body);
|
|
2032
|
+
if (snippet !== void 0) out.release = {
|
|
2033
|
+
...out.release,
|
|
2034
|
+
codeSnippet: fact(snippet, {
|
|
2035
|
+
source: "changelog",
|
|
2036
|
+
ref: `${changelogPath} ${section.heading} first fenced code block`,
|
|
2037
|
+
fetchedAt
|
|
2038
|
+
})
|
|
2039
|
+
};
|
|
1894
2040
|
if (section.date !== void 0) out.release = {
|
|
1895
2041
|
...out.release,
|
|
1896
2042
|
date: fact(section.date, {
|
|
@@ -2277,6 +2423,7 @@ function lastPageFromLink(link) {
|
|
|
2277
2423
|
//#endregion
|
|
2278
2424
|
//#region src/sources/npm.ts
|
|
2279
2425
|
const API_ROOT = "https://api.npmjs.org/downloads/point/last-week";
|
|
2426
|
+
const REGISTRY_ROOT = "https://registry.npmjs.org";
|
|
2280
2427
|
const pointSchema = z.object({
|
|
2281
2428
|
downloads: z.number().int().nonnegative(),
|
|
2282
2429
|
start: z.string().optional(),
|
|
@@ -2314,6 +2461,31 @@ async function npmGet(url, fetchImpl, cache) {
|
|
|
2314
2461
|
cache.set(url, json);
|
|
2315
2462
|
return json;
|
|
2316
2463
|
}
|
|
2464
|
+
/**
|
|
2465
|
+
* Is this name actually published?
|
|
2466
|
+
*
|
|
2467
|
+
* The downloads endpoint cannot answer this: it 404s for a package published minutes ago that
|
|
2468
|
+
* has no download data yet, which is exactly the case a first release is in. The registry
|
|
2469
|
+
* document is the authority.
|
|
2470
|
+
*
|
|
2471
|
+
* Only a definitive 404 counts as "no". A network error, a rate limit or anything else returns
|
|
2472
|
+
* undefined, so an unreachable registry never removes a call to action that was probably right.
|
|
2473
|
+
*/
|
|
2474
|
+
async function npmPackageExists(name, fetchImpl = fetch) {
|
|
2475
|
+
const trimmed = name.trim();
|
|
2476
|
+
if (trimmed.length === 0) return false;
|
|
2477
|
+
let response;
|
|
2478
|
+
try {
|
|
2479
|
+
response = await fetchImpl(`${REGISTRY_ROOT}/${encodeURIComponent(trimmed)}`, {
|
|
2480
|
+
method: "HEAD",
|
|
2481
|
+
headers: { accept: "application/json" }
|
|
2482
|
+
});
|
|
2483
|
+
} catch {
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2486
|
+
if (response.status === 404) return false;
|
|
2487
|
+
return response.ok ? true : void 0;
|
|
2488
|
+
}
|
|
2317
2489
|
|
|
2318
2490
|
//#endregion
|
|
2319
2491
|
//#region src/sources/package-json.ts
|
|
@@ -2343,9 +2515,10 @@ async function collectPackageJson(cwd, packagePath = "package.json") {
|
|
|
2343
2515
|
const pkg = pkgSchema.safeParse(parsed);
|
|
2344
2516
|
if (!pkg.success) return {};
|
|
2345
2517
|
const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2346
|
-
const
|
|
2518
|
+
const rawName = pkg.data.private === true ? void 0 : pkg.data.name;
|
|
2519
|
+
const project = { name: fact(stripScope$1(rawName ?? basenameFromPath(cwd)), {
|
|
2347
2520
|
source: "package-json",
|
|
2348
|
-
ref: `${packagePath}#name`,
|
|
2521
|
+
ref: rawName === void 0 ? `${packagePath} directory name` : `${packagePath}#name`,
|
|
2349
2522
|
fetchedAt
|
|
2350
2523
|
}) };
|
|
2351
2524
|
if (pkg.data.description !== void 0 && pkg.data.description.length > 0) project.tagline = fact(pkg.data.description, {
|
|
@@ -2409,117 +2582,6 @@ function normalizeGitUrl$1(url) {
|
|
|
2409
2582
|
return url.replace(/^git\+/, "").replace(/\.git$/, "");
|
|
2410
2583
|
}
|
|
2411
2584
|
|
|
2412
|
-
//#endregion
|
|
2413
|
-
//#region src/sources/readme.ts
|
|
2414
|
-
async function collectReadme(cwd) {
|
|
2415
|
-
let markdown;
|
|
2416
|
-
try {
|
|
2417
|
-
markdown = await readFile(join(cwd, "README.md"), "utf8");
|
|
2418
|
-
} catch {
|
|
2419
|
-
return {};
|
|
2420
|
-
}
|
|
2421
|
-
const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2422
|
-
const out = {};
|
|
2423
|
-
const h1 = extractH1(markdown);
|
|
2424
|
-
const tagline = extractTagline(markdown);
|
|
2425
|
-
const snippet = extractFirstCodeFence(markdown);
|
|
2426
|
-
const project = {};
|
|
2427
|
-
if (h1 !== void 0) project.name = fact(h1, {
|
|
2428
|
-
source: "readme",
|
|
2429
|
-
ref: "README.md H1",
|
|
2430
|
-
fetchedAt
|
|
2431
|
-
});
|
|
2432
|
-
if (tagline !== void 0) project.tagline = fact(tagline, {
|
|
2433
|
-
source: "readme",
|
|
2434
|
-
ref: "README.md first paragraph",
|
|
2435
|
-
fetchedAt
|
|
2436
|
-
});
|
|
2437
|
-
if (project.name !== void 0 || project.tagline !== void 0) out.project = project;
|
|
2438
|
-
if (snippet !== void 0) out.release = { codeSnippet: fact(snippet, {
|
|
2439
|
-
source: "readme",
|
|
2440
|
-
ref: "README.md first fenced code block",
|
|
2441
|
-
fetchedAt
|
|
2442
|
-
}) };
|
|
2443
|
-
return out;
|
|
2444
|
-
}
|
|
2445
|
-
async function collectConfiguredSnippet(cwd, spec) {
|
|
2446
|
-
const parsed = parseSnippetSpec(spec);
|
|
2447
|
-
if (parsed === void 0) return {};
|
|
2448
|
-
let contents;
|
|
2449
|
-
try {
|
|
2450
|
-
contents = await readFile(join(cwd, parsed.path), "utf8");
|
|
2451
|
-
} catch {
|
|
2452
|
-
return {};
|
|
2453
|
-
}
|
|
2454
|
-
const slice = contents.split(/\r?\n/).slice(parsed.start - 1, parsed.end).join("\n");
|
|
2455
|
-
const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2456
|
-
return { release: { codeSnippet: fact({
|
|
2457
|
-
code: slice,
|
|
2458
|
-
lang: langFromPath(parsed.path)
|
|
2459
|
-
}, {
|
|
2460
|
-
source: "user-config",
|
|
2461
|
-
ref: spec,
|
|
2462
|
-
fetchedAt
|
|
2463
|
-
}) } };
|
|
2464
|
-
}
|
|
2465
|
-
/**
|
|
2466
|
-
* Strip fenced code and HTML before reading prose out of a README.
|
|
2467
|
-
*
|
|
2468
|
-
* Skipping blocks that merely *start* with a fence is not enough. A `#` comment inside a YAML
|
|
2469
|
-
* example reads as a markdown H1, and a blank line inside a fence makes its body look like a
|
|
2470
|
-
* paragraph. Shipseal's own README hit both: the detected name became
|
|
2471
|
-
* ".github/workflows/shipseal.yml" and the tagline became a chunk of workflow YAML.
|
|
2472
|
-
*/
|
|
2473
|
-
function stripNonProse(markdown) {
|
|
2474
|
-
return markdown.replace(/```[\s\S]*?```/g, "").replace(/~~~[\s\S]*?~~~/g, "").replace(/<[^>]+>/g, "");
|
|
2475
|
-
}
|
|
2476
|
-
function extractH1(markdown) {
|
|
2477
|
-
const heading = /^#\s+(.+)$/m.exec(stripNonProse(markdown))?.[1];
|
|
2478
|
-
if (heading === void 0) return;
|
|
2479
|
-
return heading.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").trim();
|
|
2480
|
-
}
|
|
2481
|
-
function extractTagline(markdown) {
|
|
2482
|
-
const prose = stripNonProse(markdown);
|
|
2483
|
-
const h1 = /^#\s+.+$/m.exec(prose);
|
|
2484
|
-
const start = h1 === null ? 0 : (h1.index ?? 0) + h1[0].length;
|
|
2485
|
-
for (const block of prose.slice(start).split(/\n\s*\n/)) {
|
|
2486
|
-
const cleaned = stripBadges(block).trim();
|
|
2487
|
-
if (cleaned.length >= 20 && !cleaned.startsWith("#") && !cleaned.startsWith("```")) return cleaned;
|
|
2488
|
-
}
|
|
2489
|
-
}
|
|
2490
|
-
function extractFirstCodeFence(markdown) {
|
|
2491
|
-
const match = /```([a-zA-Z0-9+-]+)\n([\s\S]*?)```/.exec(markdown);
|
|
2492
|
-
if (match === null || match[1] === void 0 || match[2] === void 0) return;
|
|
2493
|
-
return {
|
|
2494
|
-
lang: match[1],
|
|
2495
|
-
code: match[2].replace(/\n$/, "")
|
|
2496
|
-
};
|
|
2497
|
-
}
|
|
2498
|
-
function parseSnippetSpec(spec) {
|
|
2499
|
-
const match = /^(?<path>.+)#L(?<start>\d+)(?:-L(?<end>\d+))?$/.exec(spec);
|
|
2500
|
-
if (match === null || match.groups === void 0) return;
|
|
2501
|
-
const path = match.groups.path;
|
|
2502
|
-
const start = Number.parseInt(match.groups.start ?? "0", 10);
|
|
2503
|
-
const end = Number.parseInt(match.groups.end ?? match.groups.start ?? "0", 10);
|
|
2504
|
-
if (path === void 0 || start < 1 || end < start) return;
|
|
2505
|
-
return {
|
|
2506
|
-
path,
|
|
2507
|
-
start,
|
|
2508
|
-
end
|
|
2509
|
-
};
|
|
2510
|
-
}
|
|
2511
|
-
function langFromPath(path) {
|
|
2512
|
-
const ext = extname(path).replace(".", "");
|
|
2513
|
-
if (ext === "ts") return "ts";
|
|
2514
|
-
if (ext === "tsx") return "tsx";
|
|
2515
|
-
if (ext === "js" || ext === "mjs" || ext === "cjs") return "js";
|
|
2516
|
-
if (ext === "jsx") return "jsx";
|
|
2517
|
-
return ext.length > 0 ? ext : "text";
|
|
2518
|
-
}
|
|
2519
|
-
function stripBadges(text) {
|
|
2520
|
-
return text.replace(/\[!\[[^\]]*]\([^)]+\)]\([^)]+\)/g, "").replace(/!\[[^\]]*]\([^)]+\)/g, "").replace(/<img[^>]*>/gi, "").replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
2521
|
-
}
|
|
2522
|
-
|
|
2523
2585
|
//#endregion
|
|
2524
2586
|
//#region src/sources/collect.ts
|
|
2525
2587
|
async function collectFacts(options) {
|
|
@@ -2534,13 +2596,15 @@ async function collectFacts(options) {
|
|
|
2534
2596
|
const remote = await collectGitRemote(options.cwd);
|
|
2535
2597
|
const changelog = await collectBestChangelog(options.cwd, version, options.changelogPath ?? "CHANGELOG.md", options.packagePath);
|
|
2536
2598
|
const readme = await collectReadme(options.cwd);
|
|
2599
|
+
const snippet = options.snippet !== void 0 && options.snippet !== null && options.snippet.length > 0 ? await collectConfiguredSnippet(options.cwd, options.snippet) : {};
|
|
2537
2600
|
const parts = [
|
|
2538
|
-
remote,
|
|
2539
2601
|
pkg,
|
|
2602
|
+
pkg.project?.npmPackage === void 0 ? await collectWorkspaceNpmPackage(options.cwd, options.skipNetwork === true ? {} : { verify: async (name) => npmPackageExists(name, options.fetchImpl ?? fetch) }) : {},
|
|
2603
|
+
remote,
|
|
2540
2604
|
git,
|
|
2541
|
-
|
|
2605
|
+
snippet,
|
|
2542
2606
|
changelog,
|
|
2543
|
-
|
|
2607
|
+
readme
|
|
2544
2608
|
];
|
|
2545
2609
|
if (options.event.kind === "bench") parts.push(await collectBenchFile(options.cwd, options.benchFile ?? ".shipseal/bench.json"));
|
|
2546
2610
|
if (options.skipNetwork !== true) {
|
|
@@ -2581,6 +2645,31 @@ async function collectBestChangelog(cwd, version, changelogPath, packagePath) {
|
|
|
2581
2645
|
const candidates = await workspaceChangelogs(cwd);
|
|
2582
2646
|
return (await Promise.all(candidates.map((candidate) => collectChangelog(cwd, version, candidate)))).find((facts) => changelogHasNotes(facts)) ?? {};
|
|
2583
2647
|
}
|
|
2648
|
+
/**
|
|
2649
|
+
* Find the one publishable package in a workspace.
|
|
2650
|
+
*
|
|
2651
|
+
* Only when there is exactly one candidate: two publishable packages make the CTA a guess, and
|
|
2652
|
+
* guessing which one to tell people to install is worse than falling back to the repository
|
|
2653
|
+
* URL. `--package` resolves the ambiguity explicitly.
|
|
2654
|
+
*/
|
|
2655
|
+
async function collectWorkspaceNpmPackage(cwd, options = {}) {
|
|
2656
|
+
const paths = await workspaceManifests(cwd);
|
|
2657
|
+
const found = (await Promise.all(paths.map((path) => collectPackageJson(cwd, path)))).map((facts, index) => ({
|
|
2658
|
+
name: facts.project?.npmPackage?.value,
|
|
2659
|
+
path: paths[index] ?? ""
|
|
2660
|
+
})).filter((entry) => entry.name !== void 0);
|
|
2661
|
+
const only = found.length === 1 ? found[0] : void 0;
|
|
2662
|
+
if (only === void 0) return {};
|
|
2663
|
+
if (options.verify !== void 0 && await options.verify(only.name) === false) return {};
|
|
2664
|
+
return { project: { npmPackage: fact(only.name, {
|
|
2665
|
+
source: "package-json",
|
|
2666
|
+
ref: `${only.path}#name`,
|
|
2667
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2668
|
+
}) } };
|
|
2669
|
+
}
|
|
2670
|
+
async function workspaceManifests(cwd) {
|
|
2671
|
+
return (await workspaceChangelogs(cwd)).map((path) => path.replace(/CHANGELOG\.md$/, "package.json"));
|
|
2672
|
+
}
|
|
2584
2673
|
async function workspaceChangelogs(cwd) {
|
|
2585
2674
|
return (await Promise.all(["packages", "apps"].map(async (root) => {
|
|
2586
2675
|
let entries;
|