From 48bce3731066d8ed6faf6a2156709b52103f0826 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Wed, 11 Nov 2026 23:00:12 +0000 Subject: [PATCH 1/4] feat: added unions package --- src/fixtures/github/releases/unions.json | 1 + src/fixtures/github/repos/unions.json | 1 + .../nuget/nuspec/purview.unions.nuspec | 22 ++ src/fixtures/nuget/purview.unions.json | 5 + src/fixtures/nuget/search/Purview.Unions.json | 8 + src/fixtures/releases/index.json | 55 ++++ .../components/islands/ReleaseOverview.tsx | 305 ++++++++++++++++++ src/src/data/projects/unions.yml | 153 +++++++++ src/src/pages/about.astro | 89 +++++ src/src/pages/releases/index.astro | 86 ++++- src/tsconfig.json | 2 +- 11 files changed, 720 insertions(+), 7 deletions(-) create mode 100644 src/fixtures/github/releases/unions.json create mode 100644 src/fixtures/github/repos/unions.json create mode 100644 src/fixtures/nuget/nuspec/purview.unions.nuspec create mode 100644 src/fixtures/nuget/purview.unions.json create mode 100644 src/fixtures/nuget/search/Purview.Unions.json create mode 100644 src/src/components/islands/ReleaseOverview.tsx create mode 100644 src/src/data/projects/unions.yml diff --git a/src/fixtures/github/releases/unions.json b/src/fixtures/github/releases/unions.json new file mode 100644 index 0000000..6d708f2 --- /dev/null +++ b/src/fixtures/github/releases/unions.json @@ -0,0 +1 @@ +[{"url":"https://api.github.com/repos/purview-dev/unions/releases/441984033","assets_url":"https://api.github.com/repos/purview-dev/unions/releases/441984033/assets","upload_url":"https://uploads.github.com/repos/purview-dev/unions/releases/441984033/assets{?name,label}","html_url":"https://github.com/purview-dev/unions/releases/tag/v1.0.0-prerelease.1","id":441984033,"author":{"login":"github-actions[bot]","id":41898282,"node_id":"MDM6Qm90NDE4OTgyODI=","avatar_url":"https://avatars.githubusercontent.com/in/15368?v=4","gravatar_id":"","url":"https://api.github.com/users/github-actions%5Bbot%5D","html_url":"https://github.com/apps/github-actions","followers_url":"https://api.github.com/users/github-actions%5Bbot%5D/followers","following_url":"https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github-actions%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/github-actions%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/github-actions%5Bbot%5D/repos","events_url":"https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/github-actions%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false},"node_id":"RE_kwDOVp0H5c4aWCQh","tag_name":"v1.0.0-prerelease.1","target_commitish":"main","name":"v1.0.0-prerelease.1","draft":false,"immutable":false,"prerelease":true,"created_at":"2026-11-11T17:12:10Z","updated_at":"2026-11-11T17:14:20Z","published_at":"2026-11-11T17:14:20Z","assets":[],"tarball_url":"https://api.github.com/repos/purview-dev/unions/tarball/v1.0.0-prerelease.1","zipball_url":"https://api.github.com/repos/purview-dev/unions/zipball/v1.0.0-prerelease.1","body":"## What's Changed\n* Net11 update by @kieronlanning in https://github.com/purview-dev/unions/pull/1\n\n## New Contributors\n* @kieronlanning made their first contribution in https://github.com/purview-dev/unions/pull/1\n\n**Full Changelog**: https://github.com/purview-dev/unions/commits/v1.0.0-prerelease.1","mentions_count":1}] diff --git a/src/fixtures/github/repos/unions.json b/src/fixtures/github/repos/unions.json new file mode 100644 index 0000000..815d307 --- /dev/null +++ b/src/fixtures/github/repos/unions.json @@ -0,0 +1 @@ +{"id":1453131749,"node_id":"R_kgDOVp0H5Q","name":"unions","full_name":"purview-dev/unions","private":false,"owner":{"login":"purview-dev","id":96894084,"node_id":"O_kgDOBcZ8hA","avatar_url":"https://avatars.githubusercontent.com/u/96894084?v=4","gravatar_id":"","url":"https://api.github.com/users/purview-dev","html_url":"https://github.com/purview-dev","followers_url":"https://api.github.com/users/purview-dev/followers","following_url":"https://api.github.com/users/purview-dev/following{/other_user}","gists_url":"https://api.github.com/users/purview-dev/gists{/gist_id}","starred_url":"https://api.github.com/users/purview-dev/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/purview-dev/subscriptions","organizations_url":"https://api.github.com/users/purview-dev/orgs","repos_url":"https://api.github.com/users/purview-dev/repos","events_url":"https://api.github.com/users/purview-dev/events{/privacy}","received_events_url":"https://api.github.com/users/purview-dev/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/purview-dev/unions","description":"Dependency-free C# 15 unions that do not box value-type cases - a source generator emits the non-boxing custom-union shape with typed storage, TryGetValue, exhaustive switch and UNI diagnostics, plus a hand-written Union.","fork":false,"url":"https://api.github.com/repos/purview-dev/unions","forks_url":"https://api.github.com/repos/purview-dev/unions/forks","keys_url":"https://api.github.com/repos/purview-dev/unions/keys{/key_id}","collaborators_url":"https://api.github.com/repos/purview-dev/unions/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/purview-dev/unions/teams","hooks_url":"https://api.github.com/repos/purview-dev/unions/hooks","issue_events_url":"https://api.github.com/repos/purview-dev/unions/issues/events{/number}","events_url":"https://api.github.com/repos/purview-dev/unions/events","assignees_url":"https://api.github.com/repos/purview-dev/unions/assignees{/user}","branches_url":"https://api.github.com/repos/purview-dev/unions/branches{/branch}","tags_url":"https://api.github.com/repos/purview-dev/unions/tags","blobs_url":"https://api.github.com/repos/purview-dev/unions/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/purview-dev/unions/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/purview-dev/unions/git/refs{/sha}","trees_url":"https://api.github.com/repos/purview-dev/unions/git/trees{/sha}","statuses_url":"https://api.github.com/repos/purview-dev/unions/statuses/{sha}","languages_url":"https://api.github.com/repos/purview-dev/unions/languages","stargazers_url":"https://api.github.com/repos/purview-dev/unions/stargazers","contributors_url":"https://api.github.com/repos/purview-dev/unions/contributors","subscribers_url":"https://api.github.com/repos/purview-dev/unions/subscribers","subscription_url":"https://api.github.com/repos/purview-dev/unions/subscription","commits_url":"https://api.github.com/repos/purview-dev/unions/commits{/sha}","git_commits_url":"https://api.github.com/repos/purview-dev/unions/git/commits{/sha}","comments_url":"https://api.github.com/repos/purview-dev/unions/comments{/number}","issue_comment_url":"https://api.github.com/repos/purview-dev/unions/issues/comments{/number}","contents_url":"https://api.github.com/repos/purview-dev/unions/contents/{+path}","compare_url":"https://api.github.com/repos/purview-dev/unions/compare/{base}...{head}","merges_url":"https://api.github.com/repos/purview-dev/unions/merges","archive_url":"https://api.github.com/repos/purview-dev/unions/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/purview-dev/unions/downloads","issues_url":"https://api.github.com/repos/purview-dev/unions/issues{/number}","pulls_url":"https://api.github.com/repos/purview-dev/unions/pulls{/number}","milestones_url":"https://api.github.com/repos/purview-dev/unions/milestones{/number}","notifications_url":"https://api.github.com/repos/purview-dev/unions/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/purview-dev/unions/labels{/name}","releases_url":"https://api.github.com/repos/purview-dev/unions/releases{/id}","deployments_url":"https://api.github.com/repos/purview-dev/unions/deployments","created_at":"2026-11-10T20:00:01Z","updated_at":"2026-11-11T17:15:01Z","pushed_at":"2026-11-11T17:14:20Z","git_url":"git://github.com/purview-dev/unions.git","ssh_url":"git@github.com:purview-dev/unions.git","clone_url":"https://github.com/purview-dev/unions.git","svn_url":"https://github.com/purview-dev/unions","homepage":"https://purview.dev/projects/unions/","size":364,"stargazers_count":1,"watchers_count":1,"language":"C#","has_issues":true,"has_projects":false,"has_downloads":false,"has_wiki":false,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":["boxing","c-sharp","code-generation","csharp","csharp-15","discriminated-unions","dotnet","net10","net11","nuget","performance","roslyn","source-generator","union","union-types","unions","value-types","zero-allocation"],"visibility":"public","forks":0,"open_issues":0,"watchers":1,"default_branch":"main","temp_clone_token":null,"custom_properties":{},"organization":{"login":"purview-dev","id":96894084,"node_id":"O_kgDOBcZ8hA","avatar_url":"https://avatars.githubusercontent.com/u/96894084?v=4","gravatar_id":"","url":"https://api.github.com/users/purview-dev","html_url":"https://github.com/purview-dev","followers_url":"https://api.github.com/users/purview-dev/followers","following_url":"https://api.github.com/users/purview-dev/following{/other_user}","gists_url":"https://api.github.com/users/purview-dev/gists{/gist_id}","starred_url":"https://api.github.com/users/purview-dev/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/purview-dev/subscriptions","organizations_url":"https://api.github.com/users/purview-dev/orgs","repos_url":"https://api.github.com/users/purview-dev/repos","events_url":"https://api.github.com/users/purview-dev/events{/privacy}","received_events_url":"https://api.github.com/users/purview-dev/received_events","type":"Organization","user_view_type":"public","site_admin":false},"network_count":0,"subscribers_count":0} diff --git a/src/fixtures/nuget/nuspec/purview.unions.nuspec b/src/fixtures/nuget/nuspec/purview.unions.nuspec new file mode 100644 index 0000000..59fe5ba --- /dev/null +++ b/src/fixtures/nuget/nuspec/purview.unions.nuspec @@ -0,0 +1,22 @@ + + + + Purview.Unions + 1.0.0-prerelease.1 + Purview Unions + Kieron Lanning + MIT + https://licenses.nuget.org/MIT + purview-logo-light.png + README.md + https://purview.dev/projects/unions/ + Dependency-free C# 15 unions that do not box value-type cases: a source generator emits the non-boxing custom-union shape, plus a hand-written Union<T1, T2> for .NET 10 and for projects that do not use a generator. + Copyright © 2026 Purview-Dev + unions union discriminated-union csharp-15 net10 net11 source-generator boxing purview + + + + + + + \ No newline at end of file diff --git a/src/fixtures/nuget/purview.unions.json b/src/fixtures/nuget/purview.unions.json new file mode 100644 index 0000000..bb59240 --- /dev/null +++ b/src/fixtures/nuget/purview.unions.json @@ -0,0 +1,5 @@ +{ + "versions": [ + "1.0.0-prerelease.1" + ] +} diff --git a/src/fixtures/nuget/search/Purview.Unions.json b/src/fixtures/nuget/search/Purview.Unions.json new file mode 100644 index 0000000..b6b988b --- /dev/null +++ b/src/fixtures/nuget/search/Purview.Unions.json @@ -0,0 +1,8 @@ +{ + "@context": { + "@vocab": "http://schema.nuget.org/schema#", + "@base": "https://api.nuget.org/v3/registration5-semver1/" + }, + "totalHits": 0, + "data": [] +} diff --git a/src/fixtures/releases/index.json b/src/fixtures/releases/index.json index 947a69c..c0c7227 100644 --- a/src/fixtures/releases/index.json +++ b/src/fixtures/releases/index.json @@ -67,6 +67,38 @@ "updatedAt": "2026-11-02T23:13:45Z", "hasDiscussions": false }, + "purview-dev/unions": { + "name": "unions", + "fullName": "purview-dev/unions", + "description": "Dependency-free C# 15 unions that do not box value-type cases - a source generator emits the non-boxing custom-union shape with typed storage, TryGetValue, exhaustive switch and UNI diagnostics, plus a hand-written Union.", + "archived": false, + "homepage": "https://purview.dev/projects/unions/", + "topics": [ + "boxing", + "c-sharp", + "code-generation", + "csharp", + "csharp-15", + "discriminated-unions", + "dotnet", + "net10", + "net11", + "nuget", + "performance", + "roslyn", + "source-generator", + "union", + "union-types", + "unions", + "value-types", + "zero-allocation" + ], + "stargazersCount": 1, + "openIssuesCount": 0, + "defaultBranch": "main", + "updatedAt": "2026-11-11T17:15:01Z", + "hasDiscussions": false + }, "purview-dev/zodsharp": { "name": "zodsharp", "fullName": "purview-dev/zodsharp", @@ -966,6 +998,17 @@ "htmlUrl": "https://github.com/purview-dev/event-sourcing/releases/tag/v1.0.0" } ], + "purview-dev/unions": [ + { + "tagName": "v1.0.0-prerelease.1", + "name": "v1.0.0-prerelease.1", + "body": "## What's Changed\n* Net11 update by @kieronlanning in https://github.com/purview-dev/unions/pull/1\n\n## New Contributors\n* @kieronlanning made their first contribution in https://github.com/purview-dev/unions/pull/1\n\n**Full Changelog**: https://github.com/purview-dev/unions/commits/v1.0.0-prerelease.1", + "prerelease": true, + "draft": false, + "publishedAt": "2026-11-11T17:14:20Z", + "htmlUrl": "https://github.com/purview-dev/unions/releases/tag/v1.0.0-prerelease.1" + } + ], "purview-dev/zodsharp": [ { "tagName": "v3.0.0-prerelease.2", @@ -3474,6 +3517,17 @@ ], "targetFrameworksVersion": "2.0.0-prerelease.39" }, + "Purview.Unions": { + "id": "Purview.Unions", + "versions": [ + "1.0.0-prerelease.1" + ], + "targetFrameworks": [ + "net10.0", + "net11.0" + ], + "targetFrameworksVersion": "1.0.0-prerelease.1" + }, "Purview.ZodSharp": { "id": "Purview.ZodSharp", "versions": [ @@ -4467,6 +4521,7 @@ "Purview.EventSourcing.InMemory": null, "Purview.EventSourcing.Validation.FluentValidation": null, "Purview.EventSourcing.Validation.ZodSharp": null, + "Purview.Unions": null, "Purview.ZodSharp": { "id": "Purview.ZodSharp", "version": "2.4.2", diff --git a/src/src/components/islands/ReleaseOverview.tsx b/src/src/components/islands/ReleaseOverview.tsx new file mode 100644 index 0000000..756297f --- /dev/null +++ b/src/src/components/islands/ReleaseOverview.tsx @@ -0,0 +1,305 @@ +import type { PackageRegistry } from '~/lib/manifest/schema'; + +import { useEffect, useMemo, useState } from 'preact/hooks'; + +import { + isStale, + latestFromVersions, + livePackageKey, + mergeVersionPair, + readLiveVersions, + refreshLiveVersions, + rollupPackageVersions, + versionStatusMessage, + type LivePackageRef, + type LiveVersions, + type PackageVersionPair, +} from '~/lib/releases/live'; +import { DISPLAY_STATUS_LABELS, type DisplayStatus } from '~/lib/status'; + +/** + * The releases page quick view: one card per project, rolled up from its packages + * and re-checked against NuGet/npm in the browser (ADR 0008, ADR 0012). It is the + * single-page overview of the whole catalogue, so a reader can see what is + * current without opening each project. + */ + +/** A package behind a project row, as the browser needs it to recompute live. */ +export interface ReleaseOverviewPackage { + id: string; + registry: PackageRegistry; + stable: string | null; + prerelease: string | null; +} + +/** One project's build-time snapshot; the browser recomputes the rollup from `packages`. */ +export interface ReleaseOverviewRow { + projectId: string; + projectName: string; + projectUrl: string; + docsUrl: string | null; + repositoryUrl: string; + status: DisplayStatus; + category: string; + packageCount: number; + skillCount: number; + packages: ReleaseOverviewPackage[]; + latestRelease: { + tagName: string; + url: string; + publishedAt: string | null; + } | null; + downloads: number | null; + downloadsPeriod: 'total' | 'weekly'; + tags: string[]; +} + +interface Props { + rows: ReleaseOverviewRow[]; +} + +type RefreshStatus = 'idle' | 'checking' | 'live' | 'stored' | 'unavailable'; + +function formatDate(value: string | null): string { + if (!value) { + return ''; + } + const date = new Date(value); + return Number.isNaN(date.getTime()) + ? '' + : date.toLocaleDateString('en-GB', { year: 'numeric', month: 'short', day: 'numeric' }); +} + +function formatDownloads(value: number | null, period: 'total' | 'weekly'): string | null { + if (value == null) { + return null; + } + return period === 'weekly' + ? `${value.toLocaleString('en-US')}/wk` + : value.toLocaleString('en-US'); +} + +export default function ReleaseOverview({ rows }: Props) { + const [search, setSearch] = useState(''); + const [live, setLive] = useState(null); + const [status, setStatus] = useState('idle'); + + const packageRefs = useMemo( + () => + rows.flatMap((row) => row.packages.map((pkg) => ({ id: pkg.id, registry: pkg.registry }))), + [rows], + ); + + // The build-time snapshot renders first; on mount the stored copy is applied + // instantly and the registries are re-checked only when the stored copy is + // missing or older than the TTL (ADR 0008, ADR 0012). + useEffect(() => { + let cancelled = false; + const stored = readLiveVersions(); + if (stored) { + setLive(stored); + setStatus('stored'); + } + if (stored && !isStale(stored)) { + return () => { + cancelled = true; + }; + } + setStatus('checking'); + void refreshLiveVersions(packageRefs, stored).then((refreshed) => { + if (cancelled) { + return; + } + if (refreshed) { + setLive(refreshed); + setStatus('live'); + } else { + setStatus(stored ? 'stored' : 'unavailable'); + } + }); + return () => { + cancelled = true; + }; + }, [packageRefs]); + + // Recompute each project's rollup from the package-level versions, so a newer + // version of any package moves the project's stable/prerelease badges. A live + // version never downgrades a build-time one (`mergeVersionPair`). + const resolved = useMemo(() => { + if (!live) { + return rows.map((row) => ({ row, rollup: rollupPackageVersions(row.packages) })); + } + return rows.map((row) => { + const pairs: PackageVersionPair[] = row.packages.map((pkg) => { + const current = { stable: pkg.stable, prerelease: pkg.prerelease }; + const versions = live.versions[livePackageKey({ id: pkg.id, registry: pkg.registry })]; + return versions ? mergeVersionPair(current, latestFromVersions(versions)) : current; + }); + return { row, rollup: rollupPackageVersions(pairs) }; + }); + }, [rows, live]); + + const query = search.trim().toLowerCase(); + const visible = resolved.filter( + ({ row }) => + query === '' || + row.projectName.toLowerCase().includes(query) || + row.category.toLowerCase().includes(query) || + row.tags.some((tag) => tag.toLowerCase().includes(query)), + ); + const currentCount = visible.filter( + ({ rollup }) => rollup.stableVersion !== null || rollup.prereleaseVersion !== null, + ).length; + + return ( +
+
+ +

+ {currentCount} of {visible.length} project{visible.length === 1 ? '' : 's'} publishing a + version +

+
+ +
+ {visible.map(({ row, rollup }) => { + const downloads = formatDownloads(row.downloads, row.downloadsPeriod); + return ( +
+
+ + {row.projectName} + + + {DISPLAY_STATUS_LABELS[row.status]} + +
+ +
+ + {rollup.stableVersion ?? '—'} + + + {rollup.prereleaseVersion ?? '—'} + + {row.latestRelease && ( + + {row.latestRelease.tagName} + + )} +
+ +

+ {row.latestRelease?.publishedAt ? ( + + ) : ( + 'No GitHub release yet' + )} + + {row.packageCount === 0 ? ( + No packages published yet + ) : rollup.laggingPackageCount > 0 ? ( + + {rollup.laggingPackageCount} of {row.packageCount} packages behind + + ) : ( + + {row.packageCount} package{row.packageCount === 1 ? '' : 's'} in step + + )} +

+ +
+ {row.category} + {row.skillCount > 0 && ( + + {row.skillCount} skill{row.skillCount === 1 ? '' : 's'} + + )} + {downloads && ( + + ↓ {downloads} + + )} + {row.docsUrl && ( + + Docs → + + )} + + Repository ↗ + +
+
+ ); + })} +
+ + {visible.length === 0 && ( +

+ No projects match your search. +

+ )} + + {status !== 'idle' && ( +

+ {versionStatusMessage(status, live)} +

+ )} +
+ ); +} diff --git a/src/src/data/projects/unions.yml b/src/src/data/projects/unions.yml new file mode 100644 index 0000000..c58e4f9 --- /dev/null +++ b/src/src/data/projects/unions.yml @@ -0,0 +1,153 @@ +$schema: ../../lib/manifest/schema.ts + +id: unions +name: Unions +shortDescription: Dependency-free C# 15 unions that do not box value-type cases. +description: >- + The C# 15 `union` keyword lowers to a single `object?` field, so every + value-type case is boxed on entry. Purview.Unions generates the language's + non-boxing custom-union shape instead: each case gets its own typed field, a + public constructor, and a `TryGetValue` that never allocates. The compiler + still sees a union — implicit conversions, exhaustive `switch`, and `is`/`null` + patterns all work — plus a hand-written `Union` for the two-case shape. +origin: >- + Discriminated unions were needed for error modelling and optional values, and + the language's own `union` declaration boxes every value-type case on entry. + The compiler already exposes the custom-union contract the boxing-free shape + needs, so the generator that emits it became a shared package instead of a + hand-written type in each project. +useWhen: >- + You model a closed set of cases — expected errors, optional results, a + small state machine — and either the value-type cases are allocation-sensitive + or you want one type the whole team matches on exhaustively. +avoidWhen: >- + You target a runtime below .NET 11 with C# 15, or a single shape with two + reference-type cases is enough: the built-in `union` declaration is simpler + and the boxing cost does not apply to reference types. +repository: purview-dev/unions +category: source-generation +status: preview +order: 25 +docs: + source: github-path + path: docs/wiki + rootPage: Getting-Started.md + exclude: + - _Sidebar.md + - Home.md + - index.md +targetFrameworks: + - net10.0 + - net11.0 +packages: + - id: Purview.Unions + description: The [GenerateUnion] attribute, the source generator with its code fixes, and the hand-written Union. + primary: true +skills: true +related: + - results + - zodsharp + - sourcegenerator-framework +useCases: + - audience: developer + title: Match a value-type union without boxing a case + scenario: >- + A method returns one of a handful of value-type outcomes, and the caller must + branch on which one it received — but the `union` keyword boxes every + value-type case on entry and again while matching. + code: | + [GenerateUnion(typeof(int), typeof(string))] + public readonly partial struct IntOrString; + + IntOrString value = 42; + + var message = value switch + { + int number => $"int {number}", + string text => $"string {text}", + null => "empty", + }; + language: csharp + outcome: >- + The compiler treats the generated type as a union, so implicit conversions, + exhaustive `switch` and `is`/`null` patterns all work, while construction, + `TryGetValue`, `is` and `switch` never allocate. + evidence: >- + The committed BenchmarkDotNet suite measures construction, both + `TryGetValue` directions, the miss path, `HasValue`, `is` and both `switch` + shapes at 0 B; only reading `Value` allocates the 24-byte box for the `int` + case, which is inherent to the union contract. + docsPage: performance + - audience: developer + title: Declare a union whose cases are type parameters + scenario: >- + You want a generic option or outcome type, but `typeof(T)` is not a legal + attribute argument, so an ordinary `[GenerateUnion(typeof(…))]` list cannot + name a type parameter. + code: | + [GenerateUnion] + public readonly partial struct Option + { + internal static partial Option Some(Some value); + internal static partial Option None(None value); + } + language: csharp + outcome: >- + Each partial factory signature names a case, the generator supplies the + bodies and derives the public constructors from them, and the compiler reports + `CS0416` only if you try to use a `typeof(T)` list instead. + evidence: >- + A generic union must name its cases in source because `CS0416` rejects + `typeof(T)` in an attribute; a `typeof` list on a generic union is reported + as `UNI0011`. + docsPage: union-design + - audience: architect + title: Let a second source generator see the union's cases + scenario: >- + Another Purview generator — Results, or the telemetry generator unwrapping a + parameter — must read the case list, but Roslyn generators never see each + other's generated output. + code: | + [GenerateUnion] + public readonly partial struct Outcome : Outcome.IUnionMembers + { + public interface IUnionMembers + { + static Outcome Create(T? value) => Outcome.CreateCase0(value); + static Outcome Create(Exception? error) => Outcome.CreateCase1(error); + + bool TryGetValue(out T value); + bool TryGetValue(out Exception value); + } + } + language: csharp + outcome: >- + A public nested `IUnionMembers` provider exposes the case list in the source + the other generator compiles against, while this generator still emits the + storage and `CreateCaseN` methods. + evidence: >- + `Purview.Telemetry.SourceGenerator` 6.0.0 reads the source-visible case list in + the same order as this package, and a `[GenerateUnion]` target with no + source-visible case list is reported as `UNI0003` rather than unwrapped. + docsPage: interop + - audience: contributor + title: Fail fast on a malformed union declaration + scenario: >- + A union is declared without `partial`, with a duplicate case, or with a case + type less accessible than the union, and the generated shape cannot be emitted + coherently. + code: | + error UNI0001: The union declaration is not partial + error UNI0004: Duplicate case type + error UNI0005: A case type is less accessible than the union + language: plaintext + outcome: >- + Every error blocks its union's output rather than emitting a partial shape, + and the package ships code fixes for the common declarations — add `partial` + (`UNI0001`), widen an inaccessible case type (`UNI0005`), add `[Union]` on the + fallback path (`UNI0007`). + evidence: >- + `UNI0015` (a single-case union) is the only warning; every other `UNI` rule + blocks emission, so a malformed union fails the build instead of producing + half a type. + docsPage: diagnostics diff --git a/src/src/pages/about.astro b/src/src/pages/about.astro index 5215409..5cfa502 100644 --- a/src/src/pages/about.astro +++ b/src/src/pages/about.astro @@ -1,17 +1,43 @@ --- import SiteLayout from '~/layouts/SiteLayout.astro'; import MermaidDiagram from '~/components/MermaidDiagram.astro'; +import StatusBadge from '~/components/StatusBadge.astro'; import { loadProjects } from '~/lib/manifest/load'; import { SITE } from '~/lib/site'; +import { displayStatus } from '~/lib/status'; import { withBase } from '~/lib/urls'; const projects = loadProjects(); const active = projects.filter((project) => project.status !== 'archived'); + +// The person behind Purview. Kept in one place so the page, the aside card and +// the Person structured data never disagree about where to find me. +const AUTHOR = { + name: 'Kieron Lanning', + role: 'Software engineer and application architect', + homepage: 'https://kieronlanning.dev/', + github: 'https://github.com/kieronlanning', + linkedin: 'https://www.linkedin.com/in/kieronlanning', + bluesky: 'https://bsky.app/profile/kieronlanning.dev', +} as const; + +const structuredData = { + '@context': 'https://schema.org', + '@type': 'Person', + name: AUTHOR.name, + jobTitle: AUTHOR.role, + url: AUTHOR.homepage, + description: `Creator of ${SITE.orgName} — open-source .NET developer tooling.`, + worksFor: { '@type': 'Organization', name: SITE.orgName, url: SITE.url }, + knowsAbout: active.map((project) => project.name), + sameAs: [AUTHOR.homepage, AUTHOR.github, AUTHOR.linkedin, AUTHOR.bluesky], +}; ---
@@ -26,6 +52,14 @@ const active = projects.filter((project) => project.status !== 'archived'); experience — removing repetitive work, catching problems earlier, and automating the things that shouldn't need continual attention.

+ +

+ Purview is created and maintained by {AUTHOR.name}, + who writes about developer experience and application architecture at{' '} + + kieronlanning.dev + . +

@@ -185,9 +219,64 @@ const active = projects.filter((project) => project.status !== 'archived'); projects that originally inspired them.

+ +
+

Projects I build

+ +

+ Purview currently spans {active.length} active projects. Each one below links to its project + page, where you will find the documentation, use cases, packages and install instructions. +

+ +
+ { + active.map((project) => ( + + {project.name} + + + )) + } +
+