shipseal 0.0.2 → 0.0.4
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/dist/cli.js +154 -21
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -307,10 +307,10 @@ 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 headlineSource = features[0];
|
|
311
|
-
const headline = headlineSource !== void 0 ? cleanLine(headlineSource) : version === void 0 ? name : `${name} ${version}`;
|
|
310
|
+
const headlineSource = breaking[0] ?? features[0] ?? fixes[0];
|
|
311
|
+
const headline = headlineSource !== void 0 ? cleanLine(firstSentence(headlineSource)) : version === void 0 ? name : `${name} ${version}`;
|
|
312
312
|
const tagline = facts.project.tagline?.value;
|
|
313
|
-
const subheadline = tagline !== void 0 && tagline.length > 0 ? cleanLine(tagline) : cleanLine(features[1] ?? fixes[0] ?? `What's new in ${version ?? name}`);
|
|
313
|
+
const subheadline = tagline !== void 0 && tagline.length > 0 ? cleanLine(tagline) : cleanLine(features[1] ?? fixes[1] ?? fixes[0] ?? `What's new in ${version ?? name}`);
|
|
314
314
|
const highlights = [];
|
|
315
315
|
for (const line of features) {
|
|
316
316
|
if (highlights.length >= maxHighlights) break;
|
|
@@ -356,8 +356,20 @@ function benchCopy(facts) {
|
|
|
356
356
|
cta: ctaFor(facts)
|
|
357
357
|
};
|
|
358
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* First sentence of an entry, for use as a headline.
|
|
361
|
+
*
|
|
362
|
+
* Changesets entries are prose paragraphs written for a changelog, so using one whole made a
|
|
363
|
+
* headline that could only be truncated mid-word. The first sentence is the part a person
|
|
364
|
+
* actually wrote as the summary. Abbreviations are not special-cased: a sentence ending is a
|
|
365
|
+
* period followed by a space and a capital, which leaves "e.g. foo" and version numbers alone.
|
|
366
|
+
*/
|
|
367
|
+
function firstSentence(text) {
|
|
368
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
369
|
+
return /^(.+?[.?!])\s+[A-Z]/.exec(collapsed)?.[1] ?? collapsed;
|
|
370
|
+
}
|
|
359
371
|
function cleanLine(text) {
|
|
360
|
-
const stripped = text.
|
|
372
|
+
const stripped = text.replace(/\s*\u2014\s*/g, ": ").replace(/\s+\u2013\s+/g, ", ").replaceAll("–", "-").replaceAll("!", ".").replace(/\s+/g, " ").trim();
|
|
361
373
|
const noTrail = stripped.endsWith(".") ? stripped.slice(0, -1) : stripped;
|
|
362
374
|
const first = noTrail.at(0);
|
|
363
375
|
if (first === void 0) return noTrail;
|
|
@@ -2111,6 +2123,36 @@ function displaySubject(text) {
|
|
|
2111
2123
|
if (first === void 0) return cleaned;
|
|
2112
2124
|
return first.toUpperCase() + cleaned.slice(1);
|
|
2113
2125
|
}
|
|
2126
|
+
/**
|
|
2127
|
+
* Repository slug and URL from `git remote get-url origin`.
|
|
2128
|
+
*
|
|
2129
|
+
* Kept separate from `collectGit`, which returns nothing without a tag. A monorepo root
|
|
2130
|
+
* often has no `homepage` or `repository` in its private package.json, which left the call
|
|
2131
|
+
* to action falling back to the workspace name. The remote is always there.
|
|
2132
|
+
*/
|
|
2133
|
+
async function collectGitRemote(cwd) {
|
|
2134
|
+
const remote = await git(cwd, [
|
|
2135
|
+
"remote",
|
|
2136
|
+
"get-url",
|
|
2137
|
+
"origin"
|
|
2138
|
+
]);
|
|
2139
|
+
if (remote === void 0) return {};
|
|
2140
|
+
const slug = githubSlug(remote.trim());
|
|
2141
|
+
if (slug === void 0) return {};
|
|
2142
|
+
const provenance = {
|
|
2143
|
+
source: "git",
|
|
2144
|
+
ref: "git remote get-url origin",
|
|
2145
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2146
|
+
};
|
|
2147
|
+
return { project: {
|
|
2148
|
+
repo: fact(slug, provenance),
|
|
2149
|
+
url: fact(`https://github.com/${slug}`, provenance)
|
|
2150
|
+
} };
|
|
2151
|
+
}
|
|
2152
|
+
function githubSlug(url) {
|
|
2153
|
+
const cleaned = url.replace(/^git\+/, "").replace(/\.git$/, "");
|
|
2154
|
+
return /github\.com[/:]([^/]+\/[^/]+)$/.exec(cleaned)?.[1];
|
|
2155
|
+
}
|
|
2114
2156
|
|
|
2115
2157
|
//#endregion
|
|
2116
2158
|
//#region src/sources/github.ts
|
|
@@ -2281,6 +2323,7 @@ async function npmGet(url, fetchImpl, cache) {
|
|
|
2281
2323
|
//#endregion
|
|
2282
2324
|
//#region src/sources/package-json.ts
|
|
2283
2325
|
const pkgSchema = z.object({
|
|
2326
|
+
private: z.boolean().optional(),
|
|
2284
2327
|
name: z.string().optional(),
|
|
2285
2328
|
description: z.string().optional(),
|
|
2286
2329
|
version: z.string().optional(),
|
|
@@ -2327,7 +2370,7 @@ async function collectPackageJson(cwd, packagePath = "package.json") {
|
|
|
2327
2370
|
ref: `${packagePath}#repository`,
|
|
2328
2371
|
fetchedAt
|
|
2329
2372
|
});
|
|
2330
|
-
if (pkg.data.name !== void 0) project.npmPackage = fact(pkg.data.name, {
|
|
2373
|
+
if (pkg.data.name !== void 0 && pkg.data.private !== true) project.npmPackage = fact(pkg.data.name, {
|
|
2331
2374
|
source: "package-json",
|
|
2332
2375
|
ref: `${packagePath}#name`,
|
|
2333
2376
|
fetchedAt
|
|
@@ -2493,9 +2536,11 @@ async function collectFacts(options) {
|
|
|
2493
2536
|
if (previousTag !== void 0) gitEvent.previousTag = previousTag;
|
|
2494
2537
|
const git = await collectGit(options.cwd, gitEvent);
|
|
2495
2538
|
const version = git.release?.version?.value ?? pkg.release?.version?.value ?? (tag === void 0 ? void 0 : versionFromTag(tag));
|
|
2539
|
+
const remote = await collectGitRemote(options.cwd);
|
|
2496
2540
|
const changelog = await collectBestChangelog(options.cwd, version, options.changelogPath ?? "CHANGELOG.md", options.packagePath);
|
|
2497
2541
|
const readme = await collectReadme(options.cwd);
|
|
2498
2542
|
const parts = [
|
|
2543
|
+
remote,
|
|
2499
2544
|
pkg,
|
|
2500
2545
|
git,
|
|
2501
2546
|
readme,
|
|
@@ -2536,7 +2581,21 @@ async function collectBestChangelog(cwd, version, changelogPath, packagePath) {
|
|
|
2536
2581
|
const sibling = join(dirname(packagePath), "CHANGELOG.md");
|
|
2537
2582
|
if (!paths.includes(sibling)) paths.push(sibling);
|
|
2538
2583
|
}
|
|
2539
|
-
|
|
2584
|
+
const found = (await Promise.all(paths.map((path) => collectChangelog(cwd, version, path)))).find((facts) => changelogHasNotes(facts));
|
|
2585
|
+
if (found !== void 0) return found;
|
|
2586
|
+
const candidates = await workspaceChangelogs(cwd);
|
|
2587
|
+
return (await Promise.all(candidates.map((candidate) => collectChangelog(cwd, version, candidate)))).find((facts) => changelogHasNotes(facts)) ?? {};
|
|
2588
|
+
}
|
|
2589
|
+
async function workspaceChangelogs(cwd) {
|
|
2590
|
+
return (await Promise.all(["packages", "apps"].map(async (root) => {
|
|
2591
|
+
let entries;
|
|
2592
|
+
try {
|
|
2593
|
+
entries = await readdir(join(cwd, root), { withFileTypes: true });
|
|
2594
|
+
} catch {
|
|
2595
|
+
return [];
|
|
2596
|
+
}
|
|
2597
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => join(root, entry.name, "CHANGELOG.md"));
|
|
2598
|
+
}))).flat().toSorted();
|
|
2540
2599
|
}
|
|
2541
2600
|
function changelogHasNotes(facts) {
|
|
2542
2601
|
const release = facts.release;
|
|
@@ -3651,16 +3710,63 @@ function parseNumber(token) {
|
|
|
3651
3710
|
|
|
3652
3711
|
//#endregion
|
|
3653
3712
|
//#region src/brand/css-vars.ts
|
|
3654
|
-
const PROP = /--(primary|brand|accent|background|foreground|muted-foreground|muted)\s*:\s*([^;]+)
|
|
3713
|
+
const PROP = /--(primary|brand|accent|background|foreground|muted-foreground|muted)\s*:\s*([^;}]+)[;}]?/g;
|
|
3714
|
+
/** Any declaration, used to resolve `var(--brand-500)` style indirection. */
|
|
3715
|
+
const ANY_PROP = /(--[\w-]+)\s*:\s*([^;}]+)[;}]?/g;
|
|
3716
|
+
/**
|
|
3717
|
+
* Selectors that carry theme tokens in real projects. `:root` alone missed shadcn's
|
|
3718
|
+
* `[data-theme]` blocks, plain `html {}`, and `.light`.
|
|
3719
|
+
*/
|
|
3720
|
+
const SELECTORS = [
|
|
3721
|
+
/:root\s*\{/g,
|
|
3722
|
+
/(?:^|[\s,}])html\s*\{/g,
|
|
3723
|
+
/\.light\b[^{]*\{/g,
|
|
3724
|
+
/\[data-theme=["']?light["']?\][^{]*\{/g,
|
|
3725
|
+
/\.dark\b[^{]*\{/g,
|
|
3726
|
+
/\[data-theme=["']?dark["']?\][^{]*\{/g
|
|
3727
|
+
];
|
|
3655
3728
|
function extractCssRootColors(css) {
|
|
3656
|
-
const
|
|
3657
|
-
const
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3729
|
+
const vars = allVariables(css);
|
|
3730
|
+
const found = {};
|
|
3731
|
+
for (const selector of SELECTORS) Object.assign(found, extractBlockColors(css, selector, vars));
|
|
3732
|
+
return mapFoundColors(found);
|
|
3733
|
+
}
|
|
3734
|
+
function allVariables(css) {
|
|
3735
|
+
const vars = {};
|
|
3736
|
+
ANY_PROP.lastIndex = 0;
|
|
3737
|
+
let match;
|
|
3738
|
+
while ((match = ANY_PROP.exec(css)) !== null) {
|
|
3739
|
+
const name = match[1];
|
|
3740
|
+
const value = match[2];
|
|
3741
|
+
if (name !== void 0 && value !== void 0) vars[name] = value.trim();
|
|
3742
|
+
}
|
|
3743
|
+
return vars;
|
|
3744
|
+
}
|
|
3745
|
+
/**
|
|
3746
|
+
* Resolve a declaration to a hex colour.
|
|
3747
|
+
*
|
|
3748
|
+
* Handles two things a plain colour parser does not. First, `var(--brand-500)` indirection,
|
|
3749
|
+
* up to three hops within the same file. Second, bare channel lists: shadcn writes
|
|
3750
|
+
* `--background: 0 0% 100%` and applies it as `hsl(var(--background))`, so the value is only
|
|
3751
|
+
* a colour once wrapped. Percent signs on the last two channels mean HSL, three plain
|
|
3752
|
+
* numbers mean RGB.
|
|
3753
|
+
*/
|
|
3754
|
+
function resolveColor(raw, vars, depth = 0) {
|
|
3755
|
+
const value = raw.trim();
|
|
3756
|
+
if (depth < 3) {
|
|
3757
|
+
const ref = /^var\(\s*(--[\w-]+)\s*(?:,[^)]*)?\)$/.exec(value);
|
|
3758
|
+
const target = ref?.[1] === void 0 ? void 0 : vars[ref[1]];
|
|
3759
|
+
if (target !== void 0) return resolveColor(target, vars, depth + 1);
|
|
3760
|
+
}
|
|
3761
|
+
const direct = parseCssColor(value);
|
|
3762
|
+
if (direct !== void 0) return direct;
|
|
3763
|
+
const parts = value.split(/[\s/]+/).filter((p) => p.length > 0);
|
|
3764
|
+
if (parts.length === 3) {
|
|
3765
|
+
if (parts[1]?.endsWith("%") === true && parts[2]?.endsWith("%") === true) return parseCssColor(`hsl(${parts[0]} ${parts[1]} ${parts[2]})`);
|
|
3766
|
+
if (parts.every((p) => /^\d+(\.\d+)?$/.test(p))) return parseCssColor(`rgb(${parts[0]} ${parts[1]} ${parts[2]})`);
|
|
3767
|
+
}
|
|
3662
3768
|
}
|
|
3663
|
-
function extractBlockColors(css, blockRe) {
|
|
3769
|
+
function extractBlockColors(css, blockRe, vars) {
|
|
3664
3770
|
const found = {};
|
|
3665
3771
|
blockRe.lastIndex = 0;
|
|
3666
3772
|
let match;
|
|
@@ -3676,7 +3782,7 @@ function extractBlockColors(css, blockRe) {
|
|
|
3676
3782
|
const name = prop[1];
|
|
3677
3783
|
const raw = prop[2];
|
|
3678
3784
|
if (name === void 0 || raw === void 0) continue;
|
|
3679
|
-
const hex =
|
|
3785
|
+
const hex = resolveColor(raw, vars);
|
|
3680
3786
|
if (hex !== void 0) found[name] = hex;
|
|
3681
3787
|
}
|
|
3682
3788
|
}
|
|
@@ -4481,12 +4587,39 @@ async function runInit(options) {
|
|
|
4481
4587
|
const sampleDir = join(brandDir, "output", "sample");
|
|
4482
4588
|
await mkdir(sampleDir, { recursive: true });
|
|
4483
4589
|
const samplePath = join(sampleDir, "release-hero-og.png");
|
|
4484
|
-
const
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4590
|
+
const renderer = await createTakumiRenderer();
|
|
4591
|
+
const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4592
|
+
const provenance = {
|
|
4593
|
+
source: "brand",
|
|
4594
|
+
ref: ".shipseal/brand.json",
|
|
4595
|
+
fetchedAt
|
|
4596
|
+
};
|
|
4597
|
+
const sampleFacts = { project: { name: fact(brand.name, provenance) } };
|
|
4598
|
+
if (brand.tagline !== void 0) sampleFacts.project.tagline = fact(brand.tagline, provenance);
|
|
4599
|
+
if (brand.url !== void 0) sampleFacts.project.url = fact(brand.url, provenance);
|
|
4600
|
+
const first = (await generate({
|
|
4601
|
+
event: {
|
|
4602
|
+
kind: "release",
|
|
4603
|
+
tag: "v1.0.0"
|
|
4604
|
+
},
|
|
4605
|
+
facts: sampleFacts,
|
|
4606
|
+
brand,
|
|
4607
|
+
config: {
|
|
4608
|
+
...config,
|
|
4609
|
+
formats: ["og"],
|
|
4610
|
+
release: {
|
|
4611
|
+
...config.release,
|
|
4612
|
+
templates: ["release-hero"]
|
|
4613
|
+
}
|
|
4614
|
+
},
|
|
4615
|
+
copy: deterministicCopy(sampleFacts),
|
|
4616
|
+
copyMode: "deterministic",
|
|
4617
|
+
renderer,
|
|
4618
|
+
themes: [brand.theme],
|
|
4619
|
+
generatedAt: fetchedAt
|
|
4620
|
+
})).files[0];
|
|
4621
|
+
if (first === void 0) throw new ShipsealError("init.sample-failed", "Could not render the sample card.", "Run shipseal doctor to check fonts and the renderer.");
|
|
4622
|
+
await writeFile(samplePath, first.bytes);
|
|
4490
4623
|
return {
|
|
4491
4624
|
brandPath,
|
|
4492
4625
|
configPath,
|