Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions scripts/build-site.sh
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,12 @@ fi
node "$script_dir/stage-port-docs.mjs" --wrappers
if [ -n "${LIBTMUX_DOCS_SOURCE_SHA:-}" ]; then
if [ "$LIBTMUX_DOCS_PORT" = clojure ]; then
# gen-tmux-usage also reads the Java core the wrapper stands on. Clojure
# lives in the Java repository, so its checkout is the Java checkout; a
# clean CI runner has no other copy at Java's default path.
if [ -n "${LIBTMUX_DOCS_CHECKOUT_CLOJURE:-}" ]; then
export LIBTMUX_DOCS_CHECKOUT_JAVA="${LIBTMUX_DOCS_CHECKOUT_JAVA:-$LIBTMUX_DOCS_CHECKOUT_CLOJURE}"
fi
node "$script_dir/gen-tmux-usage.mjs" --ports clojure
fi
# The selected API and wire contracts must come from the same product source.
Expand Down
29 changes: 19 additions & 10 deletions scripts/lib/badges.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,22 @@ import { toMarkdown } from 'mdast-util-to-markdown'
const root = join(dirname(fileURLToPath(import.meta.url)), '../..')
export const BADGE_DIRECTORY = 'badges'

/** Registries whose `/<service>/v/<package>` badge reports a published version. */
/**
* Registries whose `/<service>/v/<qualifier>.../<package>` badge reports a
* published version. `label` is what shields shows without a `label` query;
* `qualifiers` counts the path segments before the package (a LuaRocks user,
* a Maven group).
*/
const VERSION_SERVICES = {
gem: 'gem',
crates: 'crates.io',
npm: 'npm',
nuget: 'nuget',
powershellgallery: 'powershell gallery',
pypi: 'pypi',
gem: { label: 'gem', qualifiers: 0 },
crates: { label: 'crates.io', qualifiers: 0 },
hexpm: { label: 'hex', qualifiers: 0 },
luarocks: { label: 'luarocks', qualifiers: 1 },
'maven-central': { label: 'maven-central', qualifiers: 1 },
npm: { label: 'npm', qualifiers: 0 },
nuget: { label: 'nuget', qualifiers: 0 },
powershellgallery: { label: 'powershell gallery', qualifiers: 0 },
pypi: { label: 'pypi', qualifiers: 0 },
}

const OFF_ORIGIN = /^https?:\/\/(?:img\.shields\.io|badgen\.net|badge\.fury\.io|(?:www\.)?github\.com\/[^/]+\/[^/]+\/(?:actions\/workflows\/[^/]+|workflows\/[^/]+)\/badge\.svg|github\.com\/[^/]+\/[^/]+\/workflows\/.*\/badge\.svg)/i
Expand Down Expand Up @@ -84,9 +92,10 @@ export function badgeSpec(url, { port, registry }) {
const [, label, message, color] = match
return { label: query.label ?? unescapeStatic(label), message: unescapeStatic(message), color: query.color ?? color }
}
if (VERSION_SERVICES[segments[0]] && segments[1] === 'v' && segments.length === 3) {
const version = registryVersion(registry, port, decodeURIComponent(segments[2]))
return { label: query.label ?? VERSION_SERVICES[segments[0]], message: addV(version), color: query.color ?? versionColor(version) }
const service = Object.hasOwn(VERSION_SERVICES, segments[0]) ? VERSION_SERVICES[segments[0]] : undefined
if (service && segments[1] === 'v' && segments.length === 3 + service.qualifiers) {
const version = registryVersion(registry, port, decodeURIComponent(segments.at(-1)))
return { label: query.label ?? service.label, message: addV(version), color: query.color ?? versionColor(version) }
}
throw new Error(`${port}: unsupported badge ${url}; add a mapping to scripts/lib/badges.mjs`)
}
Expand Down
14 changes: 4 additions & 10 deletions site/src/components/CoreShowcase.astro
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import { homeCodeColumns } from '../lib/quickstarts'
* and the APIs it uses under that. Narrow pages fold the menu into a dropdown.
* The menu is plain links, so Tab and Enter reach every capability.
* The selection is the `capability` query parameter and leaves `product` and
* the example toggles alone. Every example is an excerpt of a program or test
* the port's CI runs; see `lib/core-capabilities.ts`.
* the example toggles alone. Every example is a program the port's CI runs, listed
* in the page footer; see `lib/core-capabilities.ts`.
*
* A port whose only example is its quickstart gets that one panel, with no
* list. Returns nothing when the port has neither.
Expand All @@ -32,10 +32,11 @@ interface Props {
links?: { href: string; label: string }[]
}
const { port, links = [] } = Astro.props as Props
const programs = portCapabilities(port, TABLE as CapabilityTable, SOURCES as Record<string, CachedSource>)
const quickstart = QUICKSTARTS[port]?.complete
const quickstartCapability = CAPABILITIES.find((item) => item.id === (quickstart?.capability ?? 'typed-graph'))
if (!quickstartCapability) throw new Error(`CoreShowcase: ${port} names an unknown quickstart capability`)
// The quickstart is the panel for its capability, so a port table entry for the same capability is not a second panel: the script selects panels by id.
const programs = portCapabilities(port, TABLE as CapabilityTable, SOURCES as Record<string, CachedSource>).filter((item) => !quickstart || item.id !== quickstartCapability.id)
const quickstartCode = quickstart ? homeProgram(quickstart, 'full') : ''
// A recording of this program, run for real, makes the first capability a player; without one the program stands alone.
const player = quickstart ? await playableCapability(port, quickstartCapability.id, quickstartCode) : undefined
Expand Down Expand Up @@ -92,10 +93,6 @@ const apis = new Map(shown.map((item) => [item.id, apisUsed(port, CORE_APIS[port
{apis.get(item.id)!.more > 0 && <li><a href={apis.get(item.id)!.moreHref}>+{apis.get(item.id)!.more} more</a></li>}
</ul>
</div>}
<p class="core-showcase__source">
{item.tmux && <small class="core-showcase__tmux">tmux {item.tmux}</small>}
The whole program, <a href={item.sourceUrl}><code>{item.file}</code></a>. The port's CI runs it in <code>{item.ci.workflow}</code>{(item.ci.where ?? item.ci.directory) && <> from <code>{item.ci.where ?? item.ci.directory}/</code></>}: <code>{item.ci.command}</code>.
</p>
</section>)}
</div>
{links.length > 0 && <p class="core-showcase__links">{links.map((link) => <a href={link.href}>{link.label}</a>)}</p>}
Expand Down Expand Up @@ -189,9 +186,6 @@ const apis = new Map(shown.map((item) => [item.id, apisUsed(port, CORE_APIS[port
margin-inline-start: .5rem; padding: .05rem .4rem; border: 1px solid var(--color-edge); border-radius: .75rem;
font-size: .6875rem; font-weight: 500; color: var(--color-muted);
}
.core-showcase__source { margin-top: .6rem; font-size: .8125rem; color: var(--color-muted); }
.core-showcase__source a { color: var(--color-link); }
.core-showcase__source code { white-space: normal; overflow-wrap: anywhere; }
.core-showcase :global(.expressive-code pre) { overflow: auto; tab-size: 8; }
@media (min-width: 56rem) {
.core-showcase__picker > summary { display: none !important; }
Expand Down
19 changes: 13 additions & 6 deletions site/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ import AgentPrompt from '../components/widgets/AgentPrompt.astro'
import HomeIntro from '../components/HomeIntro.astro'
import HomeLauncher from '../components/HomeLauncher.astro'
import { getDocumentationSurfaces } from '../lib/documentation-navigation'
import { withPortRoot, withRoot } from '../lib/site-root'
import { withRoot } from '../lib/site-root'
import { localesFor } from '../i18n/resolve'
import { getCollection, render } from 'astro:content'
import { DEFAULT_LOCALE } from '../i18n/locales'
import { CONTRIBUTE_BRANCH, CONTRIBUTE_REPO, repoPath } from '../i18n/contribute'
import { portCapabilities, type CapabilityTable } from '../lib/core-capabilities'
import type { CachedSource } from '../lib/port-landing'
import TABLE from '../data/core-capabilities.json'
import SOURCES from '../data/example-sources.json'
import { documentationCards, documentationSectionsFor } from '../lib/port-documentation'

/**
Expand All @@ -43,6 +47,8 @@ const versionSections = versionRootPort
{ route: 'concepts', href: portPageUrl(versionRootPort, buildVersion, 'concepts'), label: 'Concepts', body: 'The domain model every port shares.' },
].filter((section) => documentationSectionsFor(versionRootPort.slug).includes(section.route))
: []
// The programs the showcase shows, each with the workflow that runs it.
const ciExamples = versionRootPort ? portCapabilities(versionRootPort.slug, TABLE as CapabilityTable, SOURCES as Record<string, CachedSource>) : []
const versionDomains = versionRootPort ? documentationCards(versionRootPort.slug, buildVersion) : []

/**
Expand Down Expand Up @@ -153,11 +159,12 @@ const homeEntries = PORTS.map((port) => ({
</div>
</section>}
<ApiReferences port={versionRootPort} version={buildVersion} />
<p class="mt-8 text-sm">
<a class="hover:underline" href={withPortRoot(`/${versionRootPort.slug}/`)}>
All {versionRootPort.name} versions and install instructions
</a>
</p>
{ciExamples.length > 0 && <div class="mt-10 text-xs text-muted wrap-anywhere" data-ci-examples>
<p>Example programs, each run by the port's CI:</p>
<ul class="mt-1 space-y-0.5">
{ciExamples.map((item) => <li><a class="hover:underline" href={item.sourceUrl}><code>{item.file}</code></a> in <code>{item.ci.workflow}</code></li>)}
</ul>
</div>}
</DocsLayout>
) : (

Expand Down
27 changes: 24 additions & 3 deletions site/test/core-capabilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,8 +353,8 @@ describe('the showcase component', () => {
expect(lists[0]!.textContent).toContain('Server::new_session')
expect(lists[0]!.textContent).toMatch(/\+17 more/)
expect(doc.querySelector('.core-showcase__apis-title')!.textContent).toBe('APIs used')
expect(doc.body.textContent).toContain('just check')
expect(doc.body.textContent).toContain('The whole program')
expect(doc.body.textContent, 'the CI sentence moved to the page footer').not.toContain('The whole program')
expect(doc.querySelector('.core-showcase__source')).toBeNull()
expect([...doc.querySelectorAll('.core-showcase__links a')].map((a) => a.textContent)).toEqual(['Core Library documentation →', 'More examples →'])
const ts = await render('ts')
expect(portCapabilities('ts', table, cache), 'ts has only its quickstart').toEqual([])
Expand All @@ -364,6 +364,28 @@ describe('the showcase component', () => {
expect(ts.querySelector('[data-apis-for="typed-graph"]'), 'no empty API list when use detection is not qualified').toBeNull()
})

it('draws each capability once on every port, because the script selects panels by id', async () => {
const { Window } = await import('happy-dom')
const { createComponent, renderTemplate } = await import('astro/runtime/server/index.js')
const { loadAstroTemplate, renderAstroTemplate } = await import('./astro-templates')
const Code = createComponent((_result: unknown, props: { code: string }) => renderTemplate`<pre data-code>${props.code}</pre>`)
const template = await loadAstroTemplate('components/CoreShowcase.astro', {
stubs: {
'components/HomeExample.astro': '---\n---\n<div data-home-example />',
'components/core/CorePlayer.astro': '---\n---\n<div data-core-player />',
},
modules: { 'astro-expressive-code/components': { Code } },
})
for (const port of Object.keys(PORT_BY_SLUG)) {
const window = new Window()
window.document.body.innerHTML = await renderAstroTemplate(template, { url: `https://libtmux.org/${port}/latest/`, props: { port } })
const doc = window.document as unknown as Document
const panels = [...doc.querySelectorAll('[data-capability-panel]')].map((panel) => panel.getAttribute('data-capability-panel'))
expect(new Set(panels).size, `${port} panels ${panels.join(', ')}`).toBe(panels.length)
expect([...doc.querySelectorAll('[data-capability-option]')].map((a) => a.getAttribute('data-capability-option')), port).toEqual(panels.length > 1 ? panels : [])
}
})

it('plays a capability whose recording is of its cached program, and keeps a plain block for one without', async () => {
const { Window } = await import('happy-dom')
const { createComponent, renderTemplate } = await import('astro/runtime/server/index.js')
Expand All @@ -387,7 +409,6 @@ describe('the showcase component', () => {
expect(panel.querySelector('[data-core-player]')!.getAttribute('data-core-player'), id).toBe(id)
expect(panel.querySelector('[data-core-player]')!.getAttribute('data-options'), 'no toggles for a program with one view').toBe('false')
expect(panel.querySelector('[data-code]'), `${id} is not also drawn as a plain block`).toBeNull()
expect(panel.textContent).toContain('The whole program')
}
// rs records its send-keys program, but not create-windows, whose values the recorder cannot reach.
const rs = await render('rs')
Expand Down
16 changes: 16 additions & 0 deletions site/test/port-guide-badges.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,20 @@ describe('localizeBadges', () => {
expect(svg).toContain(`aria-label="gem: ${version.startsWith('v') ? version : `v${version}`}"`)
})

it.each([
['lua', 'https://img.shields.io/luarocks/v/tony/libtmux', '0.1.0-1', 'luarocks: v0.1.0-1'],
['elixir', 'https://img.shields.io/hexpm/v/libtmux', '0.1.0', 'hex: v0.1.0'],
['kotlin', 'https://img.shields.io/maven-central/v/io.github.libtmux/libtmux-kotlin', '0.1.0', 'maven-central: v0.1.0'],
['scala', 'https://img.shields.io/maven-central/v/io.github.libtmux/libtmux-scala_3', '0.1.0', 'maven-central: v0.1.0'],
])('renders the %s registry badge from a port entry without packages', (port, url, version, aria) => {
const data = { ports: { [port]: { version } } }
const [svg] = localizeBadges(`![x](${url})`, port, data).files.values()
expect(svg).toContain(`aria-label="${aria}"`)
expect(svg).toContain('fill="#ea7233"')
const [labelled] = localizeBadges(`![x](${url}?label=libtmux)`, port, data).files.values()
expect(labelled).toContain(`aria-label="libtmux: v${version}"`)
})

it('does not double the v of a registry version that has one', () => {
const [svg] = localizeBadges('![a](https://img.shields.io/gem/v/x)', 'go', { ports: { go: { packages: { x: { version: 'v0.1.0' } } } } }).files.values()
expect(svg).toContain('aria-label="gem: v0.1.0"')
Expand All @@ -88,6 +102,8 @@ describe('localizeBadges', () => {

it('refuses a badge it cannot render rather than hotlinking it', () => {
expect(() => localizeBadges('![x](https://img.shields.io/github/stars/o/r)', 'ruby', registry)).toThrow(/unsupported badge/)
expect(() => localizeBadges('![x](https://img.shields.io/hexpm/dt/libtmux)', 'elixir', { ports: { elixir: { version: '0.1.0' } } })).toThrow(/unsupported badge/)
expect(() => localizeBadges('![x](https://img.shields.io/maven-central/v/libtmux)', 'kotlin', { ports: { kotlin: { version: '0.1.0' } } })).toThrow(/unsupported badge/)
expect(() => localizeBadges('![x](https://img.shields.io/gem/v/missing)', 'ruby', registry)).toThrow(/registry data does not carry/)
})
})
Expand Down
13 changes: 13 additions & 0 deletions site/test/port-landing-page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,19 @@ describe('the version root of a port', () => {
expect(links.map((link) => link.href)).toEqual(['/haskell/latest/core/', '/haskell/latest/guides/'])
})

it('lists each example program and its workflow once, in a footer after the cards, and no link back to the version root', async () => {
const document = await renderRoot('ocaml')
const footer = document.querySelector('[data-ci-examples]')!
expect(footer.querySelector('p')!.textContent).toBe("Example programs, each run by the port's CI:")
const items = [...footer.querySelectorAll('li')]
expect(items.length).toBeGreaterThan(1)
expect(items[0]!.textContent).toBe('examples/showcase_graph.ml in .github/workflows/ocaml.yml')
expect(items[0]!.querySelector('a')!.getAttribute('href')).toMatch(/^https:\/\/github\.com\/.+\/blob\/[0-9a-f]{40}\/examples\/showcase_graph\.ml$/)
expect(footer.nextElementSibling, 'nothing follows the footer').toBeNull()
expect(document.body.textContent).not.toContain('versions and install instructions')
expect(document.querySelector('a[href$="/ocaml/"]')).toBeNull()
})

it('offers the selector instead of one long install list', async () => {
const document = await renderRoot('go')
expect(document.querySelectorAll('[data-port-landing]')).toHaveLength(1)
Expand Down
Loading