@astrojs/starlight 0.26.4 → 0.27.0
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/CHANGELOG.md +29 -0
- package/components/Sidebar.astro +4 -34
- package/components/SidebarPersistState.ts +3 -3
- package/components/SidebarPersister.astro +72 -0
- package/components/SidebarRestorePoint.astro +12 -0
- package/components/SidebarSublist.astro +2 -0
- package/index.ts +11 -7
- package/integrations/virtual-user-config.ts +12 -1
- package/package.json +5 -5
- package/routes/common.astro +16 -0
- package/routes/ssr/404.astro +7 -0
- package/routes/ssr/index.astro +14 -0
- package/{404.astro → routes/static/404.astro} +7 -12
- package/routes/static/index.astro +15 -0
- package/utils/git.ts +90 -2
- package/utils/gitInlined.ts +20 -0
- package/utils/plugins.ts +9 -0
- package/utils/route-data.ts +2 -4
- package/utils/routing.ts +13 -0
- package/utils/user-config.ts +19 -5
- package/virtual.d.ts +4 -0
- package/index.astro +0 -19
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# @astrojs/starlight
|
|
2
2
|
|
|
3
|
+
## 0.27.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#1255](https://github.com/withastro/starlight/pull/1255) [`6f3202b`](https://github.com/withastro/starlight/commit/6f3202b3eb747de8a1cfcba001ab618d5fdee44a) Thanks [@Fryuni](https://github.com/Fryuni)! - Adds support for server-rendered Starlight pages.
|
|
8
|
+
|
|
9
|
+
When building a project with `hybrid` or `server` output mode, a new `prerender` option on Starlight config can be set to `false` to make all Starlight pages be rendered on-demand:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
export default defineConfig({
|
|
13
|
+
output: 'server',
|
|
14
|
+
integrations: [
|
|
15
|
+
starlight({
|
|
16
|
+
prerender: false,
|
|
17
|
+
}),
|
|
18
|
+
],
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- [#2242](https://github.com/withastro/starlight/pull/2242) [`756e85e`](https://github.com/withastro/starlight/commit/756e85e8e814657c42c4a6f9c299b5bef32aee22) Thanks [@delucis](https://github.com/delucis)! - Refactors the logic for persisting and restoring sidebar state across navigations for better performance on slow or busy devices
|
|
25
|
+
|
|
26
|
+
- [#1255](https://github.com/withastro/starlight/pull/1255) [`6f3202b`](https://github.com/withastro/starlight/commit/6f3202b3eb747de8a1cfcba001ab618d5fdee44a) Thanks [@Fryuni](https://github.com/Fryuni)! - Improves performance of computing the last updated times from Git history.
|
|
27
|
+
|
|
28
|
+
Instead of executing `git` for each docs page, it is now executed twice regardless of the number of pages.
|
|
29
|
+
|
|
30
|
+
- [#1255](https://github.com/withastro/starlight/pull/1255) [`6f3202b`](https://github.com/withastro/starlight/commit/6f3202b3eb747de8a1cfcba001ab618d5fdee44a) Thanks [@Fryuni](https://github.com/Fryuni)! - Fixes last updated times on projects with custom `srcDir`
|
|
31
|
+
|
|
3
32
|
## 0.26.4
|
|
4
33
|
|
|
5
34
|
### Patch Changes
|
package/components/Sidebar.astro
CHANGED
|
@@ -2,46 +2,16 @@
|
|
|
2
2
|
import type { Props } from '../props';
|
|
3
3
|
|
|
4
4
|
import MobileMenuFooter from 'virtual:starlight/components/MobileMenuFooter';
|
|
5
|
-
import
|
|
5
|
+
import SidebarPersister from './SidebarPersister.astro';
|
|
6
6
|
import SidebarSublist from './SidebarSublist.astro';
|
|
7
7
|
|
|
8
8
|
const { sidebar } = Astro.props;
|
|
9
|
-
const hash = getSidebarHash(sidebar);
|
|
10
9
|
---
|
|
11
10
|
|
|
12
|
-
<
|
|
11
|
+
<SidebarPersister {...Astro.props}>
|
|
13
12
|
<SidebarSublist sublist={sidebar} />
|
|
14
|
-
</
|
|
13
|
+
</SidebarPersister>
|
|
14
|
+
|
|
15
15
|
<div class="md:sl-hidden">
|
|
16
16
|
<MobileMenuFooter {...Astro.props} />
|
|
17
17
|
</div>
|
|
18
|
-
|
|
19
|
-
{
|
|
20
|
-
/*
|
|
21
|
-
Inline script to restore sidebar state as soon as possible.
|
|
22
|
-
- On smaller viewports, restoring state is skipped as the sidebar is collapsed inside a menu.
|
|
23
|
-
- The state is parsed from session storage and restored.
|
|
24
|
-
- This is a progressive enhancement, so any errors are swallowed silently.
|
|
25
|
-
*/
|
|
26
|
-
}
|
|
27
|
-
<script is:inline>
|
|
28
|
-
(() => {
|
|
29
|
-
try {
|
|
30
|
-
if (!matchMedia('(min-width: 50em)').matches) return;
|
|
31
|
-
const scroller = document.getElementById('starlight__sidebar');
|
|
32
|
-
/** @type {HTMLElement | null} */
|
|
33
|
-
const target = document.querySelector('sl-sidebar-state-persist');
|
|
34
|
-
const state = JSON.parse(sessionStorage.getItem('sl-sidebar-state') || '0');
|
|
35
|
-
if (!scroller || !target || !state || target.dataset.hash !== state.hash) return;
|
|
36
|
-
target
|
|
37
|
-
.querySelectorAll('details')
|
|
38
|
-
.forEach((el, idx) => typeof state.open[idx] === 'boolean' && (el.open = state.open[idx]));
|
|
39
|
-
scroller.scrollTop = state.scroll;
|
|
40
|
-
} catch {}
|
|
41
|
-
})();
|
|
42
|
-
</script>
|
|
43
|
-
<style>
|
|
44
|
-
sl-sidebar-state-persist {
|
|
45
|
-
display: contents;
|
|
46
|
-
}
|
|
47
|
-
</style>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Collect required elements from the DOM.
|
|
2
2
|
const scroller = document.getElementById('starlight__sidebar');
|
|
3
3
|
const target = scroller?.querySelector<HTMLElement>('sl-sidebar-state-persist');
|
|
4
|
-
const details = [...(target?.querySelectorAll('details') || [])];
|
|
5
4
|
|
|
6
5
|
/** Starlight uses this key to store sidebar state in `sessionStorage`. */
|
|
7
6
|
const storageKey = 'sl-sidebar-state';
|
|
@@ -58,8 +57,9 @@ target?.addEventListener('click', (event) => {
|
|
|
58
57
|
// This excludes clicks outside of the `<summary>`, which don’t trigger toggles.
|
|
59
58
|
const toggledDetails = event.target.closest('summary')?.closest('details');
|
|
60
59
|
if (!toggledDetails) return;
|
|
61
|
-
const
|
|
62
|
-
|
|
60
|
+
const restoreElement = toggledDetails.querySelector<HTMLElement>('sl-sidebar-restore');
|
|
61
|
+
const index = parseInt(restoreElement?.dataset.index || '');
|
|
62
|
+
if (isNaN(index)) return;
|
|
63
63
|
setToggleState(!toggledDetails.open, index);
|
|
64
64
|
});
|
|
65
65
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
/*
|
|
3
|
+
This component is designed to wrap the tree of `<SidebarSublist>` components in the sidebar.
|
|
4
|
+
|
|
5
|
+
It does the following:
|
|
6
|
+
- Wraps the tree in an `<sl-sidebar-state-persist>` custom element
|
|
7
|
+
- Before the tree renders, adds an inline script which loads state and defines
|
|
8
|
+
the behaviour for the `<sl-sidebar-restore>` custom element.
|
|
9
|
+
- After the tree renders, adds an inline script which restores the sidebar scroll state.
|
|
10
|
+
|
|
11
|
+
Notes:
|
|
12
|
+
- On smaller viewports, restoring state is skipped as the sidebar is collapsed inside a menu.
|
|
13
|
+
- The state is parsed from session storage and restored.
|
|
14
|
+
- This is a progressive enhancement, so any errors are swallowed silently.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Props } from '../props';
|
|
18
|
+
import { getSidebarHash } from '../utils/navigation';
|
|
19
|
+
|
|
20
|
+
const hash = getSidebarHash(Astro.props.sidebar);
|
|
21
|
+
|
|
22
|
+
declare global {
|
|
23
|
+
interface Window {
|
|
24
|
+
/** Restored scroll position. Briefly stored on the `window` global to pass between inline scripts. */
|
|
25
|
+
_starlightScrollRestore?: number;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
<sl-sidebar-state-persist data-hash={hash}>
|
|
31
|
+
<script is:inline>
|
|
32
|
+
(() => {
|
|
33
|
+
try {
|
|
34
|
+
if (!matchMedia('(min-width: 50em)').matches) return;
|
|
35
|
+
/** @type {HTMLElement | null} */
|
|
36
|
+
const target = document.querySelector('sl-sidebar-state-persist');
|
|
37
|
+
const state = JSON.parse(sessionStorage.getItem('sl-sidebar-state') || '0');
|
|
38
|
+
if (!target || !state || target.dataset.hash !== state.hash) return;
|
|
39
|
+
window._starlightScrollRestore = state.scroll;
|
|
40
|
+
customElements.define(
|
|
41
|
+
'sl-sidebar-restore',
|
|
42
|
+
class SidebarRestore extends HTMLElement {
|
|
43
|
+
connectedCallback() {
|
|
44
|
+
try {
|
|
45
|
+
const idx = parseInt(this.dataset.index || '');
|
|
46
|
+
const details = this.closest('details');
|
|
47
|
+
if (details && typeof state.open[idx] === 'boolean') details.open = state.open[idx];
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
} catch {}
|
|
53
|
+
})();
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<slot />
|
|
57
|
+
|
|
58
|
+
<script is:inline>
|
|
59
|
+
(() => {
|
|
60
|
+
const scroller = document.getElementById('starlight__sidebar');
|
|
61
|
+
if (!window._starlightScrollRestore || !scroller) return;
|
|
62
|
+
scroller.scrollTop = window._starlightScrollRestore;
|
|
63
|
+
delete window._starlightScrollRestore;
|
|
64
|
+
})();
|
|
65
|
+
</script>
|
|
66
|
+
</sl-sidebar-state-persist>
|
|
67
|
+
|
|
68
|
+
<style>
|
|
69
|
+
sl-sidebar-state-persist {
|
|
70
|
+
display: contents;
|
|
71
|
+
}
|
|
72
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** Unique symbol for storing a running index in `locals`. */
|
|
3
|
+
const currentGroupIndexSymbol = Symbol.for('starlight-sidebar-group-index');
|
|
4
|
+
const locals = Astro.locals as Record<typeof currentGroupIndexSymbol, number>;
|
|
5
|
+
|
|
6
|
+
/** The current sidebar group’s index retrieved from `locals` if set, starting at `0`. */
|
|
7
|
+
const index = locals[currentGroupIndexSymbol] || 0;
|
|
8
|
+
// Increment the index for the next instance.
|
|
9
|
+
locals[currentGroupIndexSymbol] = index + 1;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<sl-sidebar-restore data-index={index}></sl-sidebar-restore>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { flattenSidebar, type SidebarEntry } from '../utils/navigation';
|
|
3
3
|
import Icon from '../user-components/Icon.astro';
|
|
4
4
|
import Badge from '../user-components/Badge.astro';
|
|
5
|
+
import SidebarRestorePoint from './SidebarRestorePoint.astro';
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
7
8
|
sublist: SidebarEntry[];
|
|
@@ -35,6 +36,7 @@ const { sublist, nested } = Astro.props;
|
|
|
35
36
|
<details
|
|
36
37
|
open={flattenSidebar(entry.entries).some((i) => i.isCurrent) || !entry.collapsed}
|
|
37
38
|
>
|
|
39
|
+
<SidebarRestorePoint />
|
|
38
40
|
<summary>
|
|
39
41
|
<div class="group-label">
|
|
40
42
|
<span class="large">{entry.label}</span>
|
package/index.ts
CHANGED
|
@@ -50,17 +50,20 @@ export default function StarlightIntegration({
|
|
|
50
50
|
if (!starlightConfig.disable404Route) {
|
|
51
51
|
injectRoute({
|
|
52
52
|
pattern: '404',
|
|
53
|
-
entrypoint:
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
entrypoint: starlightConfig.prerender
|
|
54
|
+
? '@astrojs/starlight/routes/static/404.astro'
|
|
55
|
+
: '@astrojs/starlight/routes/ssr/404.astro',
|
|
56
|
+
prerender: starlightConfig.prerender,
|
|
56
57
|
});
|
|
57
58
|
}
|
|
58
59
|
injectRoute({
|
|
59
60
|
pattern: '[...slug]',
|
|
60
|
-
entrypoint:
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
entrypoint: starlightConfig.prerender
|
|
62
|
+
? '@astrojs/starlight/routes/static/index.astro'
|
|
63
|
+
: '@astrojs/starlight/routes/ssr/index.astro',
|
|
64
|
+
prerender: starlightConfig.prerender,
|
|
63
65
|
});
|
|
66
|
+
|
|
64
67
|
// Add built-in integrations only if they are not already added by the user through the
|
|
65
68
|
// config or by a plugin.
|
|
66
69
|
const allIntegrations = [...config.integrations, ...integrations];
|
|
@@ -73,6 +76,7 @@ export default function StarlightIntegration({
|
|
|
73
76
|
if (!allIntegrations.find(({ name }) => name === '@astrojs/mdx')) {
|
|
74
77
|
integrations.push(mdx({ optimize: true }));
|
|
75
78
|
}
|
|
79
|
+
|
|
76
80
|
// Add Starlight directives restoration integration at the end of the list so that remark
|
|
77
81
|
// plugins injected by Starlight plugins through Astro integrations can handle text and
|
|
78
82
|
// leaf directives before they are transformed back to their original form.
|
|
@@ -87,7 +91,7 @@ export default function StarlightIntegration({
|
|
|
87
91
|
|
|
88
92
|
updateConfig({
|
|
89
93
|
vite: {
|
|
90
|
-
plugins: [vitePluginStarlightUserConfig(starlightConfig, config)],
|
|
94
|
+
plugins: [vitePluginStarlightUserConfig(command, starlightConfig, config)],
|
|
91
95
|
},
|
|
92
96
|
markdown: {
|
|
93
97
|
remarkPlugins: [
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { AstroConfig, ViteUserConfig } from 'astro';
|
|
1
|
+
import type { AstroConfig, HookParameters, ViteUserConfig } from 'astro';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import type { StarlightConfig } from '../utils/user-config';
|
|
5
|
+
import { getAllNewestCommitDate } from '../utils/git';
|
|
5
6
|
|
|
6
7
|
function resolveVirtualModuleId<T extends string>(id: T): `\0${T}` {
|
|
7
8
|
return `\0${id}`;
|
|
@@ -9,6 +10,7 @@ function resolveVirtualModuleId<T extends string>(id: T): `\0${T}` {
|
|
|
9
10
|
|
|
10
11
|
/** Vite plugin that exposes Starlight user config and project context via virtual modules. */
|
|
11
12
|
export function vitePluginStarlightUserConfig(
|
|
13
|
+
command: HookParameters<'astro:config:setup'>['command'],
|
|
12
14
|
opts: StarlightConfig,
|
|
13
15
|
{
|
|
14
16
|
build,
|
|
@@ -29,6 +31,8 @@ export function vitePluginStarlightUserConfig(
|
|
|
29
31
|
const resolveId = (id: string, base = root) =>
|
|
30
32
|
JSON.stringify(id.startsWith('.') ? resolve(fileURLToPath(base), id) : id);
|
|
31
33
|
|
|
34
|
+
const docsPath = resolve(fileURLToPath(srcDir), 'content/docs');
|
|
35
|
+
|
|
32
36
|
const virtualComponentModules = Object.fromEntries(
|
|
33
37
|
Object.entries(opts.components).map(([name, path]) => [
|
|
34
38
|
`virtual:starlight/components/${name}`,
|
|
@@ -45,6 +49,13 @@ export function vitePluginStarlightUserConfig(
|
|
|
45
49
|
srcDir,
|
|
46
50
|
trailingSlash,
|
|
47
51
|
})}`,
|
|
52
|
+
'virtual:starlight/git-info':
|
|
53
|
+
(command !== 'build'
|
|
54
|
+
? `import { makeAPI } from '${new URL('../utils/git.ts', import.meta.url)}';` +
|
|
55
|
+
`const api = makeAPI(${JSON.stringify(docsPath)});`
|
|
56
|
+
: `import { makeAPI } from '${new URL('../utils/gitInlined.ts', import.meta.url)}';` +
|
|
57
|
+
`const api = makeAPI(${JSON.stringify(getAllNewestCommitDate(docsPath))});`) +
|
|
58
|
+
'export const getNewestCommitDate = api.getNewestCommitDate;',
|
|
48
59
|
'virtual:starlight/user-css': opts.customCss.map((id) => `import ${resolveId(id)};`).join(''),
|
|
49
60
|
'virtual:starlight/user-images': opts.logo
|
|
50
61
|
? 'src' in opts.logo
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrojs/starlight",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Build beautiful, high-performance documentation websites with Astro",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"docs",
|
|
@@ -162,8 +162,7 @@
|
|
|
162
162
|
"types": "./integrations/expressive-code/hast.d.ts",
|
|
163
163
|
"default": "./integrations/expressive-code/hast.mjs"
|
|
164
164
|
},
|
|
165
|
-
"./
|
|
166
|
-
"./404.astro": "./404.astro",
|
|
165
|
+
"./routes/*": "./routes/*",
|
|
167
166
|
"./style/markdown.css": "./style/markdown.css"
|
|
168
167
|
},
|
|
169
168
|
"peerDependencies": {
|
|
@@ -174,8 +173,9 @@
|
|
|
174
173
|
"@playwright/test": "^1.45.0",
|
|
175
174
|
"@types/node": "^18.16.19",
|
|
176
175
|
"@vitest/coverage-v8": "^1.6.0",
|
|
177
|
-
"astro": "^4.
|
|
178
|
-
"vitest": "^1.6.0"
|
|
176
|
+
"astro": "^4.15.3",
|
|
177
|
+
"vitest": "^1.6.0",
|
|
178
|
+
"linkedom": "^0.18.4"
|
|
179
179
|
},
|
|
180
180
|
"dependencies": {
|
|
181
181
|
"@astrojs/mdx": "^3.1.3",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { generateRouteData } from '../utils/route-data';
|
|
3
|
+
import type { Route } from '../utils/routing';
|
|
4
|
+
import Page from '../components/Page.astro';
|
|
5
|
+
|
|
6
|
+
export type Props = {
|
|
7
|
+
route: Route;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const { route } = Astro.props;
|
|
11
|
+
|
|
12
|
+
const { Content, headings } = await route.entry.render();
|
|
13
|
+
const routeData = generateRouteData({ props: { ...route, headings }, url: Astro.url });
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<Page {...routeData}><Content frontmatter={route.entry.data} /></Page>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getRouteBySlugParam } from '../../utils/routing';
|
|
3
|
+
import CommonPage from '../common.astro';
|
|
4
|
+
|
|
5
|
+
export const prerender = false;
|
|
6
|
+
|
|
7
|
+
const route = getRouteBySlugParam(Astro.params.slug);
|
|
8
|
+
|
|
9
|
+
if (route === undefined) {
|
|
10
|
+
return new Response(null, { status: 404 });
|
|
11
|
+
}
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<CommonPage route={route} />
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getEntry } from 'astro:content';
|
|
3
3
|
import config from 'virtual:starlight/user-config';
|
|
4
|
-
import EmptyContent from '
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import { BuiltInDefaultLocale } from './utils/i18n';
|
|
4
|
+
import EmptyContent from '../../components/EmptyMarkdown.md';
|
|
5
|
+
import type { Route, StarlightDocsEntry } from '../../utils/routing';
|
|
6
|
+
import { useTranslations } from '../../utils/translations';
|
|
7
|
+
import { BuiltInDefaultLocale } from '../../utils/i18n';
|
|
8
|
+
import CommonPage from '../common.astro';
|
|
10
9
|
|
|
11
10
|
export const prerender = true;
|
|
12
11
|
|
|
@@ -42,11 +41,7 @@ const fallbackEntry: StarlightDocsEntry = {
|
|
|
42
41
|
|
|
43
42
|
const userEntry = await getEntry('docs', '404');
|
|
44
43
|
const entry = userEntry || fallbackEntry;
|
|
45
|
-
const {
|
|
46
|
-
const route = generateRouteData({
|
|
47
|
-
props: { ...entryMeta, entryMeta, headings, entry, id: entry.id, slug: entry.slug },
|
|
48
|
-
url: Astro.url,
|
|
49
|
-
});
|
|
44
|
+
const route: Route = { ...entryMeta, entryMeta, entry, id: entry.id, slug: entry.slug };
|
|
50
45
|
---
|
|
51
46
|
|
|
52
|
-
<
|
|
47
|
+
<CommonPage {route} />
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { InferGetStaticPropsType } from 'astro';
|
|
3
|
+
import { paths } from '../../utils/routing';
|
|
4
|
+
import CommonPage from '../common.astro';
|
|
5
|
+
|
|
6
|
+
export const prerender = true;
|
|
7
|
+
|
|
8
|
+
export async function getStaticPaths() {
|
|
9
|
+
return paths;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<CommonPage route={Astro.props} />
|
package/utils/git.ts
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Git module to be used from the dev server and from the integration.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { basename, dirname, relative, resolve } from 'node:path';
|
|
6
|
+
import { realpathSync } from 'node:fs';
|
|
2
7
|
import { spawnSync } from 'node:child_process';
|
|
3
8
|
|
|
4
|
-
export
|
|
9
|
+
export type GitAPI = {
|
|
10
|
+
getNewestCommitDate: (file: string) => Date;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const makeAPI = (directory: string): GitAPI => {
|
|
14
|
+
return {
|
|
15
|
+
getNewestCommitDate: (file) => getNewestCommitDate(resolve(directory, file)),
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function getNewestCommitDate(file: string): Date {
|
|
5
20
|
const result = spawnSync('git', ['log', '--format=%ct', '--max-count=1', basename(file)], {
|
|
6
21
|
cwd: dirname(file),
|
|
7
22
|
encoding: 'utf-8',
|
|
@@ -22,3 +37,76 @@ export function getNewestCommitDate(file: string) {
|
|
|
22
37
|
const date = new Date(timestamp * 1000);
|
|
23
38
|
return date;
|
|
24
39
|
}
|
|
40
|
+
|
|
41
|
+
function getRepoRoot(directory: string): string {
|
|
42
|
+
const result = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
43
|
+
cwd: directory,
|
|
44
|
+
encoding: 'utf-8',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (result.error) {
|
|
48
|
+
return directory;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
return realpathSync(result.stdout.trim());
|
|
53
|
+
} catch {
|
|
54
|
+
return directory;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getAllNewestCommitDate(directory: string): [string, number][] {
|
|
59
|
+
const repoRoot = getRepoRoot(directory);
|
|
60
|
+
|
|
61
|
+
const gitLog = spawnSync(
|
|
62
|
+
'git',
|
|
63
|
+
[
|
|
64
|
+
'log',
|
|
65
|
+
// Format each history entry as t:<seconds since epoch>
|
|
66
|
+
'--format=t:%ct',
|
|
67
|
+
// In each entry include the name and status for each modified file
|
|
68
|
+
'--name-status',
|
|
69
|
+
'--',
|
|
70
|
+
directory,
|
|
71
|
+
],
|
|
72
|
+
{
|
|
73
|
+
cwd: repoRoot,
|
|
74
|
+
encoding: 'utf-8',
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (gitLog.error) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let runningDate = Date.now();
|
|
83
|
+
const latestDates = new Map<string, number>();
|
|
84
|
+
|
|
85
|
+
for (const logLine of gitLog.stdout.split('\n')) {
|
|
86
|
+
if (logLine.startsWith('t:')) {
|
|
87
|
+
// t:<seconds since epoch>
|
|
88
|
+
runningDate = Number.parseInt(logLine.slice(2)) * 1000;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// - Added files take the format `A\t<file>`
|
|
92
|
+
// - Modified files take the format `M\t<file>`
|
|
93
|
+
// - Deleted files take the format `D\t<file>`
|
|
94
|
+
// - Renamed files take the format `R<count>\t<old>\t<new>`
|
|
95
|
+
// - Copied files take the format `C<count>\t<old>\t<new>`
|
|
96
|
+
// The name of the file as of the commit being processed is always
|
|
97
|
+
// the last part of the log line.
|
|
98
|
+
const tabSplit = logLine.lastIndexOf('\t');
|
|
99
|
+
if (tabSplit === -1) continue;
|
|
100
|
+
const fileName = logLine.slice(tabSplit + 1);
|
|
101
|
+
|
|
102
|
+
const currentLatest = latestDates.get(fileName) || 0;
|
|
103
|
+
latestDates.set(fileName, Math.max(currentLatest, runningDate));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return Array.from(latestDates.entries()).map(([file, date]) => {
|
|
107
|
+
const fileFullPath = resolve(repoRoot, file);
|
|
108
|
+
const fileInDirectory = relative(directory, fileFullPath);
|
|
109
|
+
|
|
110
|
+
return [fileInDirectory, date];
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git module to be used on production build results.
|
|
3
|
+
* The API is based on inlined git information.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { GitAPI, getAllNewestCommitDate } from './git';
|
|
7
|
+
|
|
8
|
+
type InlinedData = ReturnType<typeof getAllNewestCommitDate>;
|
|
9
|
+
|
|
10
|
+
export const makeAPI = (data: InlinedData): GitAPI => {
|
|
11
|
+
const trackedDocsFiles = new Map(data);
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
getNewestCommitDate: (file) => {
|
|
15
|
+
const timestamp = trackedDocsFiles.get(file);
|
|
16
|
+
if (!timestamp) throw new Error(`Failed to retrieve the git history for file "${file}"`);
|
|
17
|
+
return new Date(timestamp);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
};
|
package/utils/plugins.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { AstroIntegration } from 'astro';
|
|
|
2
2
|
import { z } from 'astro/zod';
|
|
3
3
|
import { StarlightConfigSchema, type StarlightUserConfig } from '../utils/user-config';
|
|
4
4
|
import { parseWithFriendlyErrors } from '../utils/error-map';
|
|
5
|
+
import { AstroError } from 'astro/errors';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Runs Starlight plugins in the order that they are configured after validating the user-provided
|
|
@@ -72,6 +73,14 @@ export async function runPlugins(
|
|
|
72
73
|
});
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
if (context.config.output === 'static' && !starlightConfig.prerender) {
|
|
77
|
+
throw new AstroError(
|
|
78
|
+
'Starlight’s `prerender: false` option requires `output: "hybrid"` or `"server"` in your Astro config.',
|
|
79
|
+
'Either set `output` in your Astro config or set `prerender: true` in the Starlight options.\n\n' +
|
|
80
|
+
'Learn more about rendering modes in the Astro docs: https://docs.astro.build/en/basics/rendering-modes/'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
75
84
|
return { integrations, starlightConfig };
|
|
76
85
|
}
|
|
77
86
|
|
package/utils/route-data.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { MarkdownHeading } from 'astro';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
2
|
import project from 'virtual:starlight/project-context';
|
|
4
3
|
import config from 'virtual:starlight/user-config';
|
|
5
4
|
import { generateToC, type TocItem } from './generateToC';
|
|
6
|
-
import { getNewestCommitDate } from '
|
|
5
|
+
import { getNewestCommitDate } from 'virtual:starlight/git-info';
|
|
7
6
|
import { getPrevNextLinks, getSidebar, type SidebarEntry } from './navigation';
|
|
8
7
|
import { ensureTrailingSlash } from './path';
|
|
9
8
|
import type { Route } from './routing';
|
|
@@ -82,11 +81,10 @@ function getLastUpdated({ entry }: PageProps): Date | undefined {
|
|
|
82
81
|
const { lastUpdated: configLastUpdated } = config;
|
|
83
82
|
|
|
84
83
|
if (frontmatterLastUpdated ?? configLastUpdated) {
|
|
85
|
-
const currentFilePath = fileURLToPath(new URL('src/content/docs/' + entry.id, project.root));
|
|
86
84
|
try {
|
|
87
85
|
return frontmatterLastUpdated instanceof Date
|
|
88
86
|
? frontmatterLastUpdated
|
|
89
|
-
: getNewestCommitDate(
|
|
87
|
+
: getNewestCommitDate(entry.id);
|
|
90
88
|
} catch {
|
|
91
89
|
// If the git command fails, ignore the error.
|
|
92
90
|
return undefined;
|
package/utils/routing.ts
CHANGED
|
@@ -100,6 +100,19 @@ function getRoutes(): Route[] {
|
|
|
100
100
|
}
|
|
101
101
|
export const routes = getRoutes();
|
|
102
102
|
|
|
103
|
+
function getParamRouteMapping(): ReadonlyMap<string | undefined, Route> {
|
|
104
|
+
const map = new Map<string | undefined, Route>();
|
|
105
|
+
for (const route of routes) {
|
|
106
|
+
map.set(slugToParam(route.slug), route);
|
|
107
|
+
}
|
|
108
|
+
return map;
|
|
109
|
+
}
|
|
110
|
+
const routesBySlugParam = getParamRouteMapping();
|
|
111
|
+
|
|
112
|
+
export function getRouteBySlugParam(slugParam: string | undefined): Route | undefined {
|
|
113
|
+
return routesBySlugParam.get(slugParam?.replace(/\/$/, '') || undefined);
|
|
114
|
+
}
|
|
115
|
+
|
|
103
116
|
function getPaths(): Path[] {
|
|
104
117
|
return routes.map((route) => ({
|
|
105
118
|
params: { slug: slugToParam(route.slug) },
|
package/utils/user-config.ts
CHANGED
|
@@ -195,7 +195,7 @@ const UserConfigSchema = z.object({
|
|
|
195
195
|
* Set to `false` to disable indexing your site with Pagefind.
|
|
196
196
|
* This will also hide the default search UI if in use.
|
|
197
197
|
*/
|
|
198
|
-
pagefind: z.boolean().
|
|
198
|
+
pagefind: z.boolean().optional(),
|
|
199
199
|
|
|
200
200
|
/** Specify paths to components that should override Starlight’s default components */
|
|
201
201
|
components: ComponentConfigSchema(),
|
|
@@ -209,6 +209,13 @@ const UserConfigSchema = z.object({
|
|
|
209
209
|
/** Disable Starlight's default 404 page. */
|
|
210
210
|
disable404Route: z.boolean().default(false).describe("Disable Starlight's default 404 page."),
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Define whether Starlight pages should be prerendered or not.
|
|
214
|
+
* Defaults to always prerender Starlight pages, even when the project is
|
|
215
|
+
* set to "server" output mode.
|
|
216
|
+
*/
|
|
217
|
+
prerender: z.boolean().default(true),
|
|
218
|
+
|
|
212
219
|
/** Enable displaying a “Built with Starlight” link in your site’s footer. */
|
|
213
220
|
credits: z
|
|
214
221
|
.boolean()
|
|
@@ -216,8 +223,16 @@ const UserConfigSchema = z.object({
|
|
|
216
223
|
.describe('Enable displaying a “Built with Starlight” link in your site’s footer.'),
|
|
217
224
|
});
|
|
218
225
|
|
|
219
|
-
export const StarlightConfigSchema = UserConfigSchema.strict()
|
|
220
|
-
(
|
|
226
|
+
export const StarlightConfigSchema = UserConfigSchema.strict()
|
|
227
|
+
.transform((config) => ({
|
|
228
|
+
...config,
|
|
229
|
+
// Pagefind only defaults to true if prerender is also true.
|
|
230
|
+
pagefind: config.pagefind ?? config.prerender,
|
|
231
|
+
}))
|
|
232
|
+
.refine((config) => !(!config.prerender && config.pagefind), {
|
|
233
|
+
message: 'Pagefind search is not support with prerendering disabled.',
|
|
234
|
+
})
|
|
235
|
+
.transform(({ title, locales, defaultLocale, ...config }, ctx) => {
|
|
221
236
|
const configuredLocales = Object.keys(locales ?? {});
|
|
222
237
|
|
|
223
238
|
// This is a multilingual site (more than one locale configured) or a monolingual site with
|
|
@@ -286,8 +301,7 @@ export const StarlightConfigSchema = UserConfigSchema.strict().transform(
|
|
|
286
301
|
defaultLocale: defaultLocaleConfig,
|
|
287
302
|
locales: undefined,
|
|
288
303
|
} as const;
|
|
289
|
-
}
|
|
290
|
-
);
|
|
304
|
+
});
|
|
291
305
|
|
|
292
306
|
export type StarlightConfig = z.infer<typeof StarlightConfigSchema>;
|
|
293
307
|
export type StarlightUserConfig = z.input<typeof StarlightConfigSchema>;
|
package/virtual.d.ts
CHANGED
|
@@ -14,6 +14,10 @@ declare module 'virtual:starlight/project-context' {
|
|
|
14
14
|
export default ProjectContext;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
declare module 'virtual:starlight/git-info' {
|
|
18
|
+
export function getNewestCommitDate(file: string): Date;
|
|
19
|
+
}
|
|
20
|
+
|
|
17
21
|
declare module 'virtual:starlight/user-css' {}
|
|
18
22
|
|
|
19
23
|
declare module 'virtual:starlight/user-images' {
|
package/index.astro
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import type { InferGetStaticPropsType } from 'astro';
|
|
3
|
-
import { generateRouteData } from './utils/route-data';
|
|
4
|
-
import { paths } from './utils/routing';
|
|
5
|
-
|
|
6
|
-
import Page from './components/Page.astro';
|
|
7
|
-
|
|
8
|
-
export const prerender = true;
|
|
9
|
-
|
|
10
|
-
export async function getStaticPaths() {
|
|
11
|
-
return paths;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
|
15
|
-
const { Content, headings } = await Astro.props.entry.render();
|
|
16
|
-
const route = generateRouteData({ props: { ...Astro.props, headings }, url: Astro.url });
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
<Page {...route}><Content frontmatter={Astro.props.entry.data} /></Page>
|