@zebradil/docs-kit 0.10.0 → 0.11.1
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 +14 -3
- package/package.json +4 -3
- package/packages/docs-kit/Banner.astro +44 -0
- package/packages/docs-kit/SiteTitle.astro +46 -0
- package/packages/docs-kit/index.mjs +23 -6
- package/packages/docs-kit/kit.test.mjs +29 -0
- package/packages/docs-kit/versions.mjs +103 -0
- package/schema/site.schema.json +26 -0
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ The repository root is the npm package [`@zebradil/docs-kit`](https://www.npmjs.
|
|
|
44
44
|
<!-- x-release-please-start-version -->
|
|
45
45
|
```jsonc
|
|
46
46
|
// package.json
|
|
47
|
-
{ "private": true, "type": "module", "dependencies": { "@zebradil/docs-kit": "0.
|
|
47
|
+
{ "private": true, "type": "module", "dependencies": { "@zebradil/docs-kit": "0.11.1" } }
|
|
48
48
|
```
|
|
49
49
|
<!-- x-release-please-end -->
|
|
50
50
|
|
|
@@ -71,6 +71,17 @@ analytics:
|
|
|
71
71
|
data-website-id: 00000000-0000-0000-0000-000000000000
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
To publish the docs of past releases next to the current ones, set `versions:` in `site.yaml`:
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
versions:
|
|
78
|
+
minors: 3 # the 3 newest minor versions
|
|
79
|
+
majors: 3 # the newest minor of each of the 3 newest majors; below 1.0 each minor counts as a major
|
|
80
|
+
pin: [v1.4] # always, e.g. a long-term support line
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The default branch stays at the site root. Each selected minor version is rebuilt from its newest patch tag `vX.Y.Z` with the current kit at `/vX.Y/`, from the tag's `src/content/docs`, `src/assets` and `public/`, with `site.yaml` from the default branch. A version switcher appears in the header and marks the newest release, a banner on every other version, the default branch's included, links to the latest release, and release pages are kept out of search engines. Links between pages stay within a version only when they are relative. The workflow builds the versions only when it deploys; a release that no longer builds is skipped with a warning. To try it locally, run `npx astro build && npx --no-install docs-kit-versions` from `docs/` and serve `dist/`. See [ADR 0008](docs/adr/0008-versioned-docs-from-release-tags.md).
|
|
84
|
+
|
|
74
85
|
For coding agents and LLMs, every site serves [`/llms.txt`](https://llmstxt.org), an index of all pages, and `/llms-full.txt`, all pages in one file. Each page's Markdown source is also served at its URL with `.md` appended (`/guides/configure/` → `/guides/configure.md`), with relative links made absolute. GitHub Pages cannot vary a response on the `Accept` header, so the `.md` URLs are the only Markdown route.
|
|
75
86
|
|
|
76
87
|
### Publishing with GitHub Actions
|
|
@@ -86,7 +97,7 @@ on:
|
|
|
86
97
|
permissions: { contents: read, pages: write, id-token: write }
|
|
87
98
|
jobs:
|
|
88
99
|
docs:
|
|
89
|
-
uses: Zebradil/docs-kit/.github/workflows/docs.yml@v0.
|
|
100
|
+
uses: Zebradil/docs-kit/.github/workflows/docs.yml@v0.11.1
|
|
90
101
|
```
|
|
91
102
|
<!-- x-release-please-end -->
|
|
92
103
|
|
|
@@ -94,7 +105,7 @@ Keep `uses:` on its own indented line: Renovate's github-actions manager finds r
|
|
|
94
105
|
|
|
95
106
|
The caller must grant `pages: write` and `id-token: write`: a called workflow can only narrow the caller's token permissions, never widen them, and the deploy job needs both.
|
|
96
107
|
|
|
97
|
-
On every run it installs `docs/` with `npm ci`, builds the CLI with `reference.cli.build` (run from the repository root) when set, regenerates the CLI reference with `help2md` into `docs/src/content/docs/reference/cli`, and builds the site with `npx astro build` (which validates `site.yaml`). On a pull request it fails if the regenerated reference differs from the committed one and prints the diff. On a push to the default branch it deploys `docs/dist` to GitHub Pages.
|
|
108
|
+
On every run it installs `docs/` with `npm ci`, builds the CLI with `reference.cli.build` (run from the repository root) when set, regenerates the CLI reference with `help2md` into `docs/src/content/docs/reference/cli`, and builds the site with `npx astro build` (which validates `site.yaml`). On a pull request it fails if the regenerated reference differs from the committed one and prints the diff. On a push to the default branch it builds the release versions when `versions:` is set and deploys `docs/dist` to GitHub Pages. A tag pushed with a workflow's `GITHUB_TOKEN` triggers no other workflow, so a new release shows up with the next deploy; to publish it at once, run the docs workflow on the default branch after the tag exists, for example from the release job.
|
|
98
109
|
|
|
99
110
|
Inputs, all optional:
|
|
100
111
|
|
package/package.json
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"./content": "./packages/docs-kit/content.mjs"
|
|
15
15
|
},
|
|
16
16
|
"bin": {
|
|
17
|
-
"help2md": "refgen/help2md.mjs"
|
|
17
|
+
"help2md": "refgen/help2md.mjs",
|
|
18
|
+
"docs-kit-versions": "packages/docs-kit/versions.mjs"
|
|
18
19
|
},
|
|
19
20
|
"files": [
|
|
20
21
|
"packages/docs-kit",
|
|
@@ -36,8 +37,8 @@
|
|
|
36
37
|
"@fontsource/ibm-plex-mono": "5.3.0",
|
|
37
38
|
"@fontsource/outfit": "5.3.0",
|
|
38
39
|
"ajv": "8.20.0",
|
|
39
|
-
"astro": "7.4.
|
|
40
|
+
"astro": "7.4.11",
|
|
40
41
|
"yaml": "2.9.1"
|
|
41
42
|
},
|
|
42
|
-
"version": "0.
|
|
43
|
+
"version": "0.11.1"
|
|
43
44
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Default from '@astrojs/starlight/components/Banner.astro';
|
|
3
|
+
import { root, version } from 'virtual:docs-kit/site';
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
{/* Filled from versions.json at runtime: which release is the latest changes after a release's docs are built. */}
|
|
7
|
+
<kit-version-banner class="kit-version-banner" data-root={root} data-version={version} data-pagefind-ignore hidden>
|
|
8
|
+
These docs are for <span></span>. <a>Read the docs of the latest release</a>.
|
|
9
|
+
</kit-version-banner>
|
|
10
|
+
<Default />
|
|
11
|
+
|
|
12
|
+
<script>
|
|
13
|
+
customElements.define(
|
|
14
|
+
'kit-version-banner',
|
|
15
|
+
class extends HTMLElement {
|
|
16
|
+
async connectedCallback() {
|
|
17
|
+
const { root, version } = this.dataset as { root: string; version: string };
|
|
18
|
+
const response = await fetch(`${root}versions.json`).catch(() => undefined);
|
|
19
|
+
if (!response?.ok) return;
|
|
20
|
+
const versions: { name: string; path: string; latest?: true }[] = await response.json();
|
|
21
|
+
const latest = versions.find((v) => v.latest);
|
|
22
|
+
const current = version && `${version}/`;
|
|
23
|
+
if (!latest || latest.path === current) return;
|
|
24
|
+
const branch = versions.find((v) => v.path === '')?.name;
|
|
25
|
+
this.querySelector('span')!.textContent = version || `the development version (${branch})`;
|
|
26
|
+
this.querySelector('a')!.href = root + latest.path;
|
|
27
|
+
this.hidden = false;
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<style>
|
|
34
|
+
.kit-version-banner:not([hidden]) {
|
|
35
|
+
display: block;
|
|
36
|
+
padding: var(--sl-nav-pad-y) var(--sl-nav-pad-x);
|
|
37
|
+
background-color: var(--sl-color-bg-accent);
|
|
38
|
+
color: var(--sl-color-text-invert);
|
|
39
|
+
text-align: center;
|
|
40
|
+
}
|
|
41
|
+
.kit-version-banner a {
|
|
42
|
+
color: inherit;
|
|
43
|
+
}
|
|
44
|
+
</style>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Default from '@astrojs/starlight/components/SiteTitle.astro';
|
|
3
|
+
import Select from '@astrojs/starlight/components/Select.astro';
|
|
4
|
+
import { root, version } from 'virtual:docs-kit/site';
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<Default />
|
|
8
|
+
{/* Filled from versions.json at runtime, so the docs of a past release list releases made after it. */}
|
|
9
|
+
<kit-versions data-root={root} data-version={version} hidden>
|
|
10
|
+
<Select icon="open-book" label="Docs version" options={[]} width="auto" />
|
|
11
|
+
</kit-versions>
|
|
12
|
+
|
|
13
|
+
<script>
|
|
14
|
+
customElements.define(
|
|
15
|
+
'kit-versions',
|
|
16
|
+
class extends HTMLElement {
|
|
17
|
+
async connectedCallback() {
|
|
18
|
+
const { root, version } = this.dataset as { root: string; version: string };
|
|
19
|
+
const select = this.querySelector('select')!;
|
|
20
|
+
const response = await fetch(`${root}versions.json`).catch(() => undefined);
|
|
21
|
+
if (!response?.ok) return;
|
|
22
|
+
const versions: { name: string; path: string; latest?: true }[] = await response.json();
|
|
23
|
+
const current = version && `${version}/`;
|
|
24
|
+
// The same page in another version: the URL path below the current version's base.
|
|
25
|
+
const page = location.pathname.slice(root.length + current.length);
|
|
26
|
+
for (const { name, path, latest } of versions) {
|
|
27
|
+
select.add(new Option(latest ? `${name} (latest release)` : name, path, false, path === current));
|
|
28
|
+
}
|
|
29
|
+
select.addEventListener('change', async () => {
|
|
30
|
+
const target = root + select.value;
|
|
31
|
+
const exists = await fetch(target + page, { method: 'HEAD' }).then((r) => r.ok, () => false);
|
|
32
|
+
location.href = exists ? target + page : target;
|
|
33
|
+
});
|
|
34
|
+
this.hidden = false;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<style>
|
|
41
|
+
kit-versions:not([hidden]) {
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
margin-inline-start: 1rem;
|
|
45
|
+
}
|
|
46
|
+
</style>
|
|
@@ -16,7 +16,11 @@ export default function docsKit({ site: sitePath = 'site.yaml' } = {}) {
|
|
|
16
16
|
// Hosting is GitHub Pages per repo: https://github.com/<owner>/<repo> is served at https://<owner>.github.io/<repo>,
|
|
17
17
|
// or at the root of its custom domain.
|
|
18
18
|
const [owner, repo] = new URL(site.repo).pathname.split('/').filter(Boolean);
|
|
19
|
-
const
|
|
19
|
+
const root = site.domain ? '' : `/${repo}`;
|
|
20
|
+
// Set by docs-kit-versions when it builds a release's docs into <root>/<version>/.
|
|
21
|
+
const version = process.env.DOCS_KIT_VERSION ?? '';
|
|
22
|
+
if (!/^[\w.-]*$/.test(version)) throw new Error(`DOCS_KIT_VERSION '${version}' is not a path segment`);
|
|
23
|
+
const base = version ? `${root}/${version}` : root;
|
|
20
24
|
|
|
21
25
|
// Starlight links public/favicon.svg itself; these are linked only when the project ships them.
|
|
22
26
|
const icons = [
|
|
@@ -25,11 +29,13 @@ export default function docsKit({ site: sitePath = 'site.yaml' } = {}) {
|
|
|
25
29
|
]
|
|
26
30
|
.filter(([file]) => existsSync(`public/${file}`))
|
|
27
31
|
.map(([file, attrs]) => ({ tag: 'link', attrs: { ...attrs, href: `${base}/${file}` } }));
|
|
32
|
+
// A release's docs stay readable but out of search results, which should point at the current docs.
|
|
33
|
+
const noindex = version ? [{ tag: 'meta', attrs: { name: 'robots', content: 'noindex' } }] : [];
|
|
28
34
|
|
|
29
35
|
return defineConfig({
|
|
30
36
|
site: site.domain ? `https://${site.domain}` : `https://${owner.toLowerCase()}.github.io`,
|
|
31
37
|
base: base || '/',
|
|
32
|
-
vite: { plugins: [siteModule(site)] },
|
|
38
|
+
vite: { plugins: [siteModule(site, { version, root: `${root}/` })] },
|
|
33
39
|
integrations: [
|
|
34
40
|
starlight({
|
|
35
41
|
title: site.name,
|
|
@@ -37,12 +43,15 @@ export default function docsKit({ site: sitePath = 'site.yaml' } = {}) {
|
|
|
37
43
|
logo: site.logo && (site.logo.dark ? { light: site.logo.light, dark: site.logo.dark } : { src: site.logo.light }),
|
|
38
44
|
head: [
|
|
39
45
|
...icons,
|
|
46
|
+
...noindex,
|
|
40
47
|
...(site.analytics
|
|
41
48
|
? [{ tag: 'script', attrs: { src: site.analytics.script, defer: true, ...site.analytics.attributes } }]
|
|
42
49
|
: []),
|
|
43
50
|
],
|
|
44
51
|
social: [{ icon: 'github', label: 'GitHub', href: site.repo }],
|
|
45
|
-
|
|
52
|
+
// A release's pages live in a tag, which GitHub cannot edit.
|
|
53
|
+
editLink: version ? {} : { baseUrl: `${site.repo}/edit/main/docs/` },
|
|
54
|
+
components: site.versions ? { SiteTitle: kitPath('SiteTitle.astro'), Banner: kitPath('Banner.astro') } : {},
|
|
46
55
|
customCss: [kitPath('layout.css'), kitPath(`themes/${site.theme ?? 'default'}.css`)],
|
|
47
56
|
sidebar: [
|
|
48
57
|
{ label: 'Getting started', slug: 'getting-started' },
|
|
@@ -66,12 +75,20 @@ export default function docsKit({ site: sitePath = 'site.yaml' } = {}) {
|
|
|
66
75
|
});
|
|
67
76
|
}
|
|
68
77
|
|
|
69
|
-
/**
|
|
70
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Exposes the validated site.yaml to the kit's components as `virtual:docs-kit/site`, with the docs version being built
|
|
80
|
+
* ('' for the default branch) and the site root URL path, where versions.json lives.
|
|
81
|
+
*/
|
|
82
|
+
function siteModule(site, { version, root }) {
|
|
71
83
|
const id = 'virtual:docs-kit/site';
|
|
84
|
+
const code = [
|
|
85
|
+
`export default ${JSON.stringify(site)};`,
|
|
86
|
+
`export const version = ${JSON.stringify(version)};`,
|
|
87
|
+
`export const root = ${JSON.stringify(root)};`,
|
|
88
|
+
].join('\n');
|
|
72
89
|
return {
|
|
73
90
|
name: 'docs-kit-site',
|
|
74
91
|
resolveId: (source) => (source === id ? `\0${id}` : undefined),
|
|
75
|
-
load: (resolved) => (resolved === `\0${id}` ?
|
|
92
|
+
load: (resolved) => (resolved === `\0${id}` ? code : undefined),
|
|
76
93
|
};
|
|
77
94
|
}
|
|
@@ -5,6 +5,7 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { test } from 'node:test';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import docsKit from './index.mjs';
|
|
8
|
+
import { minorVersions, selectVersions } from './versions.mjs';
|
|
8
9
|
|
|
9
10
|
const path = (p) => fileURLToPath(new URL(p, import.meta.url));
|
|
10
11
|
|
|
@@ -43,3 +44,31 @@ test('accepts analytics with data-* attributes only', () => {
|
|
|
43
44
|
test('rejects an invalid site.yaml with readable errors', () => {
|
|
44
45
|
assert.throws(() => docsKit({ site: path('../../schema/testdata/broken.yaml') }), /must be one of: default, mono, porridge/);
|
|
45
46
|
});
|
|
47
|
+
|
|
48
|
+
test('serves a release version below the site root', (t) => {
|
|
49
|
+
t.after(() => delete process.env.DOCS_KIT_VERSION);
|
|
50
|
+
process.env.DOCS_KIT_VERSION = 'v1.2';
|
|
51
|
+
const config = docsKit({ site: path('../../examples/site.yaml') });
|
|
52
|
+
assert.equal(config.base, '/kasha/v1.2');
|
|
53
|
+
process.env.DOCS_KIT_VERSION = '../x';
|
|
54
|
+
assert.throws(() => docsKit({ site: path('../../examples/site.yaml') }), /not a path segment/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('picks the newest patch of each minor version, newest minor first', () => {
|
|
58
|
+
const tags = ['v1.2.0', 'v1.10.0', 'v1.2.3', 'v1.2.10', 'v2.0.0-rc.1', 'v0.9.1', 'docs-v3.0.0', 'v1.10.1', ''];
|
|
59
|
+
assert.deepEqual(minorVersions(tags), [
|
|
60
|
+
{ name: 'v1.10', tag: 'v1.10.1', line: '1' },
|
|
61
|
+
{ name: 'v1.2', tag: 'v1.2.10', line: '1' },
|
|
62
|
+
{ name: 'v0.9', tag: 'v0.9.1', line: '0.9' },
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('selects the newest minors, the newest minor of each newest major, and pinned ones', () => {
|
|
67
|
+
const tags = ['v3.0.0', 'v2.5.0', 'v2.4.0', 'v2.3.0', 'v1.9.0', 'v1.8.0', 'v0.4.0', 'v0.3.0'];
|
|
68
|
+
const names = (options) => selectVersions(minorVersions(tags), options).map((v) => v.name);
|
|
69
|
+
assert.deepEqual(names({ majors: 3, minors: 3 }), ['v3.0', 'v2.5', 'v2.4', 'v1.9']);
|
|
70
|
+
assert.deepEqual(names({ majors: 0, minors: 1, pin: ['v1.8', 'v9.9'] }), ['v3.0', 'v1.8']);
|
|
71
|
+
// Below 1.0 each minor is its own release line.
|
|
72
|
+
assert.deepEqual(names({ majors: 5, minors: 0 }), ['v3.0', 'v2.5', 'v1.9', 'v0.4', 'v0.3']);
|
|
73
|
+
assert.deepEqual(names({}), ['v3.0', 'v2.5', 'v2.4', 'v1.9']);
|
|
74
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Builds the docs of past releases next to the default branch's site in dist/: the newest patch tag (vX.Y.Z) of each
|
|
3
|
+
// minor version that `versions:` in site.yaml selects, at dist/vX.Y/, and the list of all of them in dist/versions.json.
|
|
4
|
+
// Each release's pages, assets and public files come from its tag; site.yaml, dependencies and the kit come from the
|
|
5
|
+
// working tree, so every version is built with the current kit and theme.
|
|
6
|
+
// Usage, from the docs directory after `astro build`: docs-kit-versions [--default-branch main]
|
|
7
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
8
|
+
import { cpSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { join, relative, resolve } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { parseArgs } from 'node:util';
|
|
12
|
+
import { parse } from 'yaml';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Release tags grouped by minor version, newest first, each with its newest patch and its release line: the major
|
|
16
|
+
* version, or `0.<minor>` below 1.0, where semver lets every minor break. Pre-releases are skipped.
|
|
17
|
+
*/
|
|
18
|
+
export function minorVersions(tags) {
|
|
19
|
+
const newest = new Map();
|
|
20
|
+
for (const tag of tags) {
|
|
21
|
+
const match = /^v(\d+)\.(\d+)\.(\d+)$/.exec(tag);
|
|
22
|
+
if (!match) continue;
|
|
23
|
+
const [major, minor, patch] = match.slice(1).map(Number);
|
|
24
|
+
const name = `v${major}.${minor}`;
|
|
25
|
+
if (!newest.has(name) || newest.get(name).patch < patch) newest.set(name, { name, tag, major, minor, patch });
|
|
26
|
+
}
|
|
27
|
+
return [...newest.values()]
|
|
28
|
+
.sort((a, b) => b.major - a.major || b.minor - a.minor)
|
|
29
|
+
.map(({ name, tag, major, minor }) => ({ name, tag, line: major ? `${major}` : `0.${minor}` }));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The minor versions to publish, newest first: the `minors` newest, the newest of each of the `majors` newest release
|
|
34
|
+
* lines, and the pinned ones.
|
|
35
|
+
*/
|
|
36
|
+
export function selectVersions(candidates, { majors = 3, minors = 3, pin = [] } = {}) {
|
|
37
|
+
const lines = [...new Set(candidates.map((c) => c.line))].slice(0, majors);
|
|
38
|
+
const newestOfLine = lines.map((line) => candidates.find((c) => c.line === line));
|
|
39
|
+
return candidates.filter((c, i) => i < minors || newestOfLine.includes(c) || pin.includes(c.name));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Copied from a tag into the build: content replaces the working tree's, the others are laid over it so that files
|
|
43
|
+
// site.yaml names (logo, art) stay present.
|
|
44
|
+
const fromTag = ['src/content/docs', 'src/assets', 'public'];
|
|
45
|
+
const skipped = /^(node_modules|dist|\.astro)(\/|$)/;
|
|
46
|
+
|
|
47
|
+
function main() {
|
|
48
|
+
const { values } = parseArgs({ options: { 'default-branch': { type: 'string', default: 'main' } } });
|
|
49
|
+
const site = parse(readFileSync('site.yaml', 'utf8'));
|
|
50
|
+
if (!site.versions) throw new Error('site.yaml has no `versions:`; nothing to build');
|
|
51
|
+
|
|
52
|
+
const git = (...args) => execFileSync('git', args, { encoding: 'utf8' });
|
|
53
|
+
// `<tag>:./<path>` resolves the path against the working directory, wherever the docs directory is in the repo.
|
|
54
|
+
const has = (tag, path) => spawnSync('git', ['cat-file', '-e', `${tag}:./${path}`]).status === 0;
|
|
55
|
+
const candidates = minorVersions(git('tag', '--list', 'v*').split('\n')).filter(({ tag }) => has(tag, fromTag[0]));
|
|
56
|
+
for (const name of (site.versions.pin ?? []).filter((n) => !candidates.some((c) => c.name === n))) {
|
|
57
|
+
console.log(`::warning::Pinned docs version ${name} has no vX.Y.Z tag with ${fromTag[0]}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const built = [];
|
|
61
|
+
for (const { name, tag } of selectVersions(candidates, site.versions)) {
|
|
62
|
+
const out = resolve('dist', name);
|
|
63
|
+
// Inside the docs directory, so the build finds node_modules by walking up; a symlink confuses Astro's paths.
|
|
64
|
+
mkdirSync('.astro', { recursive: true });
|
|
65
|
+
const work = mkdtempSync(resolve('.astro/version-'));
|
|
66
|
+
try {
|
|
67
|
+
cpSync('.', work, { recursive: true, filter: (src) => !skipped.test(relative('.', src)) });
|
|
68
|
+
rmSync(join(work, fromTag[0]), { recursive: true });
|
|
69
|
+
// Pathspecs are relative to the working directory, and so are the archived paths.
|
|
70
|
+
const archive = execFileSync('git', ['archive', tag, '--', ...fromTag.filter((p) => has(tag, p))], {
|
|
71
|
+
maxBuffer: 1 << 30,
|
|
72
|
+
});
|
|
73
|
+
execFileSync('tar', ['-x', '-C', work], { input: archive });
|
|
74
|
+
console.log(`Building ${name} from ${tag}`);
|
|
75
|
+
const astro = resolve('node_modules/.bin/astro');
|
|
76
|
+
const result = spawnSync(astro, ['build', '--outDir', out], {
|
|
77
|
+
cwd: work,
|
|
78
|
+
env: { ...process.env, DOCS_KIT_VERSION: name },
|
|
79
|
+
stdio: 'inherit',
|
|
80
|
+
});
|
|
81
|
+
if (result.status !== 0) throw new Error(`astro build exited with ${result.status ?? result.signal}`);
|
|
82
|
+
built.push(name);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// One release that no longer builds with the current kit must not block publishing the others.
|
|
85
|
+
console.log(`::warning::Skipped the ${name} docs (${tag}): ${error.message}`);
|
|
86
|
+
rmSync(out, { recursive: true, force: true });
|
|
87
|
+
} finally {
|
|
88
|
+
rmSync(work, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const latest = candidates[0]?.name;
|
|
93
|
+
const versions = [
|
|
94
|
+
{ name: values['default-branch'], path: '' },
|
|
95
|
+
...built.map((name) => ({ name, path: `${name}/`, ...(name === latest && { latest: true }) })),
|
|
96
|
+
];
|
|
97
|
+
mkdirSync('dist', { recursive: true });
|
|
98
|
+
writeFileSync('dist/versions.json', `${JSON.stringify(versions, null, 2)}\n`);
|
|
99
|
+
console.log(`Wrote dist/versions.json: ${versions.map((v) => v.name).join(', ')}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// npx runs this through a symlink in node_modules/.bin; tests import it for minorVersions.
|
|
103
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) main();
|
package/schema/site.schema.json
CHANGED
|
@@ -461,6 +461,32 @@
|
|
|
461
461
|
}
|
|
462
462
|
}
|
|
463
463
|
},
|
|
464
|
+
"versions": {
|
|
465
|
+
"description": "Publishes the docs of past releases next to the default branch's, which stay at the site root. Each selected minor version is served at <site>/vX.Y/ from its newest patch tag (vX.Y.Z), with a version switcher in the header that marks the newest release. A release's pages, src/assets and public/ come from its tag; site.yaml and the kit come from the default branch, so every version gets the current theme. Pre-releases and tags without the docs directory's src/content/docs are skipped.",
|
|
466
|
+
"type": "object",
|
|
467
|
+
"additionalProperties": false,
|
|
468
|
+
"properties": {
|
|
469
|
+
"minors": {
|
|
470
|
+
"description": "Publishes the newest this many minor versions.",
|
|
471
|
+
"type": "integer",
|
|
472
|
+
"minimum": 0,
|
|
473
|
+
"maximum": 20,
|
|
474
|
+
"default": 3
|
|
475
|
+
},
|
|
476
|
+
"majors": {
|
|
477
|
+
"description": "Publishes the newest minor version of each of the newest this many major versions. Below 1.0 every minor version counts as a major one, since semver lets it break.",
|
|
478
|
+
"type": "integer",
|
|
479
|
+
"minimum": 0,
|
|
480
|
+
"maximum": 20,
|
|
481
|
+
"default": 3
|
|
482
|
+
},
|
|
483
|
+
"pin": {
|
|
484
|
+
"description": "Minor versions published whatever 'minors' and 'majors' select, e.g. a long-term support line.",
|
|
485
|
+
"type": "array",
|
|
486
|
+
"items": { "type": "string", "pattern": "^v\\d+\\.\\d+$" }
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
},
|
|
464
490
|
"reference": {
|
|
465
491
|
"description": "How refgen produces the generated pages under reference/. Omit when the project has no CLI.",
|
|
466
492
|
"type": "object",
|