StudioReleasesPage
uses node:fs
A package's own releases page (pkg; the index, ReleasesIndexPage, leads to it): its next release as things stand, first (the version it
calls for and why, the bump, each semver finding behind it with what it means for code using the package, what it
can't be released for yet, the summary drafted for it, and how to release it), then the releases made, newest
first, each with its notes folded under it; beside them, the package: its namespace, exports, commands and the
changelog. A tab for each package, saying how many findings wait in its next release.
import { publishingList, releasePage } from "#codebase/tcmts/cli/release_pages"; import { draftFile, writtenPart } from "#codebase/tcmts/cli/releases"; import { ago, at, codeHref, CopyButton, Fold, Html, markdownHtml, servedCodebase, shareNow, } from "#codebase/tcmts/share"; import { AnnounceButton, ReleaseButton, SinceRelease, StudioFrame, studioShare, SummaryEditor, } from "#codebase/tcmts/studio"; import fs from "node:fs"; export async function StudioReleasesPage({ pkg }: { pkg: string }) { studioShare(); const s = shareNow(); if (!s.signedIn) return <StudioFrame tab="releases" title="Sign in">{null}</StudioFrame>; const all = publishingList(s.cb); const name = all.some((p) => p.name === pkg) ? pkg : undefined; if (!name) return ( <StudioFrame tab="releases" title="No such package"> <div className="crumbs"><a href={at("/releases")}>Releases</a> <span className="muted">/</span></div> <div className="box"> <div className="empty"> There's no package named <code>{pkg}</code> here{s.cb.store.reading ? <> on <code>{s.cb.store.branch().name}</code></> : null}. </div> </div> </StudioFrame> ); const p = releasePage(s.cb, name); // which part of the version the release moves (before 1.0.0, less than its changes' level: semver's way) const parts = (v: string) => v.split(/[.+-]/).slice(0, 3).map(Number); const bump = (() => { if (!p.next.after) return undefined; const [a, b] = [parts(p.next.after), parts(p.next.version)]; return a[0] !== b[0] ? "major" : a[1] !== b[1] ? "minor" : "patch"; })(); // (releases are main's: made on main, the codebase's own branch, not on another read here) const onMain = s.cb.store.reading === undefined && s.cb.store.branch().name === "main"; const next = p.next; const latest = s.cb.store.releases(name)[0]; // (the draft releasing reads its words from, as written: tcm's own placeholder comment left out) const drafted = draftFile(s.cb, name); const draft = fs.existsSync(drafted) ? writtenPart(fs.readFileSync(drafted, "utf8")).replace(/<!--[\s\S]*?-->/g, "").trim() : ""; const levels: { level: string; label: string }[] = [ { level: "major", label: "Breaking changes" }, { level: "minor", label: "Added and extended" }, { level: "patch", label: "Changed, with the same API" }, ]; const finding = (f: (typeof next.findings)[number]) => ( <div className="lrow" key={`${f.lint} ${f.path}`}> <div> <a className="mono" href={codeHref(f.path)}>{f.path}</a> {f.summary} <div className="muted" style={{ fontSize: 12.5, marginTop: 2 }}> <code>{f.lint}</code> · {f.why} </div> {f.detail && ( <pre className="code" style={{ marginTop: 6, padding: "6px 10px", whiteSpace: "pre-wrap" }}>{f.detail}</pre> )} </div> </div> ); return ( <StudioFrame tab="releases" title={`${name} · releases`}> <div className="crumbs"><a href={at("/releases")}>Releases</a> <span className="muted">/</span></div> <div className="pagehead"> <h2 className="title mono">{name}</h2> </div> <div className="cols-2" style={{ display: "grid", gap: 24 }}> <div> <div className="box"> <div className="boxhead" style={{ padding: "12px 16px" }}> <span className="ver"> {next.upToDate ? ( next.after ) : ( <> {next.after && <span className="from">{next.after} →</span>} {next.version} </> )} </span> <span className="muted"> {next.upToDate ? "its latest release: nothing of its own has changed since, so there's nothing to release" : `its next release, as things stand: ${next.reason}`} </span> {!next.upToDate && bump && next.level && ( <span className="bump right" aria-label={`a ${bump} release`}> {["patch", "minor", "major"].map((l) => ( <span key={l} className={l === bump ? `on ${l}` : ""}>{l}</span> ))} </span> )} {!next.upToDate && bump && next.level && next.level !== bump && ( <span className="muted" style={{ flexBasis: "100%", fontSize: 12.5 }}> Before 1.0.0, {next.level === "major" ? "breaking changes make a minor release" : "additions make a patch release"}, as semver has it: {next.level} changes, a {bump} release. </span> )} </div> {next.blocked && ( <div className="lrow"> <div className="bad">It can't be released yet: {next.blocked}</div> </div> )} {!next.upToDate && ( <> <div className="relpart md relnotes"> {s.cb.store.reading === undefined ? ( <SummaryEditor name={name} version={next.version} initial={draft} html={draft ? markdownHtml(s.cb, draft) : ""} /> ) : draft ? ( <Html html={markdownHtml(s.cb, draft)} /> ) : null} <Html html={markdownHtml(s.cb, next.notes)} /> </div> {next.findings.length > 0 && ( <div className="relgroup"> <Fold head={ <> <b>Why {next.version}</b> <span className="count">{next.findings.length}</span>{" "} <span className="muted">each change behind it, its lint, and what it means for code using {name}</span> </> } > {levels.map(({ level, label }) => { const fs_ = next.findings.filter((f) => f.level === level); if (!fs_.length) return null; return ( <div className="relgroup" key={level}> <div className={`relhead${level === "major" ? " bad" : ""}`}> {label} <span className="count">{fs_.length}</span> </div> {fs_.map(finding)} </div> ); })} </Fold> </div> )} {next.following.length > 0 && ( <div className="lrow muted" style={{ fontSize: 12.5 }}> <div>and {next.following.length} definition(s) that only use what changed: not changes of their own</div> </div> )} </> )} </div> {!next.upToDate && latest && ( <SinceRelease namespace={p.namespace} since={latest.causal} version={latest.version} /> )} {!next.upToDate && ( <div className={`mergebox ${next.blocked ? "blocked" : onMain ? "good" : ""}`}> <span className={`big ${next.blocked ? "bad" : onMain ? "ok" : "muted"}`}>{next.blocked ? "✕" : onMain ? "✓" : "⑂"}</span> <div style={{ flex: "1 1 300px", minWidth: 0 }}> <b> {next.blocked ? `${name} ${next.version} waits for todo` : onMain ? `Ready to release ${name} ${next.version}` : "Releases are made on main"} </b> <div className="muted"> {next.blocked ? ( next.blocked ) : onMain ? ( <> It names this point of its history {next.version} for good, with these notes.{" "} <code>tcm release {name} --announce</code> announces it on the AT Protocol too. </> ) : s.cb.store.reading === "main" ? ( <> Main is only read here: this codebase is on <code>{servedCodebase().cb.store.branch().name}</code>. With it on main (<code>tcm switch main</code>), the studio releases from here. </> ) : s.cb.store.reading !== undefined ? ( <> This is <code>{s.cb.store.branch().name}</code>, read only: what it has lands on main, and is released from there (the branch menu reads main). </> ) : ( <> This codebase is on <code>{s.cb.store.branch().name}</code>: what it has lands on main, and is released from there. </> )} </div> </div> <span className="right actions"> {onMain && !next.blocked ? ( <ReleaseButton name={name} version={next.version} /> ) : ( <button className="btn primary" disabled> Release {next.version} </button> )} </span> </div> )} <div className="box"> <div className="boxhead"> <b>Released</b> <span className="count">{p.releases.length}</span> </div> {p.releases.length ? ( p.releases.map((r) => ( <div className="relline" key={r.version}> <Fold head={ <> <b className="mono">{r.version}</b> <span className="relsum">{r.summary.replace(/^#+\s*/, "") || <span className="muted">no summary</span>}</span> <span className="muted right">{ago(r.ts)} · {r.author}</span> </> } > <Html className="boxbody md" html={markdownHtml(s.cb, r.body || "_No notes._")} /> <div className="boxbody muted" style={{ paddingTop: 0, fontSize: 12.5 }}> <a href={at(`/history/${r.causal}`)}>Its history entry</a> · <code>tcm diff {name}@{r.version}</code> {onMain && ( <span className="right" style={{ float: "right" }}> <AnnounceButton name={name} version={r.version} /> </span> )} </div> </Fold> </div> )) ) : ( <div className="empty">None yet: its first release is 0.1.0.</div> )} </div> </div> <aside className="side"> <div className="sect"> <h4>The package</h4> {p.description && <Html className="md muted" html={markdownHtml(s.cb, p.description)} />} <dl className="kv"> <dt>Namespace</dt> <dd><a className="mono" href={codeHref(p.namespace)}>{p.namespace}</a></dd> <dt>Exports</dt> <dd> {p.exports.map((e) => ( <div key={e.subpath}> {e.subpath === "." ? null : <code>{e.subpath}</code>} {e.count} definition(s) </div> ))} </dd> {p.bin.length > 0 && ( <> <dt>Commands</dt> <dd>{p.bin.map((b) => <div key={b.command}><code>{b.command}</code></div>)}</dd> </> )} </dl> </div> <div className="sect"> <h4>Its changelog</h4> <span className="muted">Made from its releases, never edited by hand: </span> <CopyButton text={`tcm changelog ${name} --out CHANGELOG.md`} label="Copy the command" /> </div> </aside> </div> </StudioFrame> ); }
uses
- tcmts.cli.release_pages.publishingList
- tcmts.cli.release_pages.releasePage
- tcmts.cli.releases.draftFile
- tcmts.cli.releases.writtenPart
- tcmts.share.CopyButton
- tcmts.share.Fold
- tcmts.share.Html
- tcmts.share.ago
- tcmts.share.at
- tcmts.share.codeHref
- tcmts.share.markdownHtml
- tcmts.share.servedCodebase
- tcmts.share.shareNow
- tcmts.studio.AnnounceButton
- tcmts.studio.ReleaseButton
- tcmts.studio.SinceRelease
- tcmts.studio.StudioFrame
- tcmts.studio.SummaryEditor
- tcmts.studio.studioShare