@astrojs/starlight 0.35.2 → 0.36.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 CHANGED
@@ -1,5 +1,67 @@
1
1
  # @astrojs/starlight
2
2
 
3
+ ## 0.36.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#3427](https://github.com/withastro/starlight/pull/3427) [`c3b2d0f`](https://github.com/withastro/starlight/commit/c3b2d0fc37bb9b7b6abc6c11b760a4114690ccd4) Thanks [@delucis](https://github.com/delucis)! - Fixes styling of labels that wrap across multiple lines in `<Tabs>` component
8
+
9
+ ⚠️ **Potentially breaking change:** Tab labels now have a narrower line-height and additional vertical padding. If you have custom CSS targetting the `<Tabs>` component, you may want to double check the visual appearance of your tabs when updating.
10
+
11
+ If you want to preserve the previous styling, you can add the following custom CSS to your site:
12
+
13
+ ```css
14
+ .tab > [role='tab'] {
15
+ line-height: var(--sl-line-height);
16
+ padding-block: 0;
17
+ }
18
+ ```
19
+
20
+ - [#3380](https://github.com/withastro/starlight/pull/3380) [`3364af3`](https://github.com/withastro/starlight/commit/3364af31e535d62ee7b045e6d9cf97c7e58df981) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Makes head entry parsing stricter in Starlight config and content frontmatter.
21
+
22
+ **⚠️ Potentially breaking change:** Previously Starlight would accept a head entry for a `meta` tag defining some `content` which generates invalid HTML as `<meta>` is a void element which cannot have any child nodes. Now, it is an error to define a `meta` tag including some `content`.
23
+
24
+ If you see errors after updating, look for head entries in the Starlight configuration in the `astro.config.mjs` file or in the frontmatter of your content files that include a `content` property for a `meta` tag. To fix the error, move the `content` property to the `attrs` object with at least an additional attribute to identify the kind of metadata it represents:
25
+
26
+ ```diff
27
+ head: {
28
+ tag: 'meta',
29
+ - content: 'foo',
30
+ attrs: {
31
+ name: 'my-meta',
32
+ + content: 'foo',
33
+ },
34
+ },
35
+ ```
36
+
37
+ - [#3340](https://github.com/withastro/starlight/pull/3340) [`2018c31`](https://github.com/withastro/starlight/commit/2018c31b0f559d51478bfbf9f12cfba76b4e74fc) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Adds missing vertical spacing between Markdown content and UI Framework components using [client directives](https://docs.astro.build/en/reference/directives-reference/#client-directives).
38
+
39
+ **⚠️ Potentially breaking change:** By default, Starlight applies some vertical spacing (`--sl-content-gap-y`) between Markdown content blocks. This change introduces similar spacing between Markdown content blocks and UI Framework components using client directives which was not present before.
40
+
41
+ If you were relying on the previous behavior, you can manually override the spacing by manually specifying the top margin on the component using [custom CSS](https://starlight.astro.build/guides/css-and-tailwind/#custom-css-styles), e.g. by relying on a CSS class to target the component.
42
+
43
+ ```css
44
+ .my-custom-component {
45
+ margin-top: 0;
46
+ }
47
+ ```
48
+
49
+ ### Patch Changes
50
+
51
+ - [#3423](https://github.com/withastro/starlight/pull/3423) [`a0d0670`](https://github.com/withastro/starlight/commit/a0d0670bce5827b90fd0102fa3517814367760e7) Thanks [@andersk](https://github.com/andersk)! - Fixes HTML validity in sidebar groups by ensuring `<summary>` is the first child of `<details>`
52
+
53
+ ## 0.35.3
54
+
55
+ ### Patch Changes
56
+
57
+ - [#3416](https://github.com/withastro/starlight/pull/3416) [`fcc0633`](https://github.com/withastro/starlight/commit/fcc0633ab14fc602f5c540972446e22c79746042) Thanks [@randomguy-2650](https://github.com/randomguy-2650)! - Updates German UI translations to be more idiomatic.
58
+
59
+ - [#1640](https://github.com/withastro/starlight/pull/1640) [`d1b3828`](https://github.com/withastro/starlight/commit/d1b3828cdfe8114884ab65049581e37624422ac4) Thanks [@hippotastic](https://github.com/hippotastic)! - Refactors various internal systems, improving code quality and maintainability.
60
+
61
+ - [#3421](https://github.com/withastro/starlight/pull/3421) [`97e8103`](https://github.com/withastro/starlight/commit/97e8103c5793d583931575dbe17924ef88a5b6fd) Thanks [@andersk](https://github.com/andersk)! - Removes an invalid `value` attribute from the language and theme selectors
62
+
63
+ - [#3422](https://github.com/withastro/starlight/pull/3422) [`9200fac`](https://github.com/withastro/starlight/commit/9200fac71ebe6e7bbbecee3d1893e744db01dc6e) Thanks [@andersk](https://github.com/andersk)! - Refactors collapsible sidebar sections and “on this page” dropdown to use `<span>` instead of `<div>`
64
+
3
65
  ## 0.35.2
4
66
 
5
67
  ### Patch Changes
@@ -18,7 +18,6 @@ function localizedPathname(locale: string | undefined): string {
18
18
  <Select
19
19
  icon="translate"
20
20
  label={Astro.locals.t('languageSelect.accessibleLabel')}
21
- value={localizedPathname(Astro.locals.starlightRoute.locale)}
22
21
  options={Object.entries(config.locales).map(([code, locale]) => ({
23
22
  value: localizedPathname(code),
24
23
  selected: code === Astro.locals.starlightRoute.locale,
@@ -11,10 +11,10 @@ const { toc } = Astro.locals.starlightRoute;
11
11
  <nav aria-labelledby="starlight__on-this-page--mobile">
12
12
  <details id="starlight__mobile-toc">
13
13
  <summary id="starlight__on-this-page--mobile" class="sl-flex">
14
- <div class="toggle sl-flex">
14
+ <span class="toggle sl-flex">
15
15
  {Astro.locals.t('tableOfContents.onThisPage')}
16
16
  <Icon name={'right-caret'} class="caret" size="1rem" />
17
- </div>
17
+ </span>
18
18
  <span class="display-current" />
19
19
  </summary>
20
20
  <div class="dropdown">
@@ -3,7 +3,6 @@ import Icon from '../user-components/Icon.astro';
3
3
 
4
4
  interface Props {
5
5
  label: string;
6
- value: string;
7
6
  icon: Parameters<typeof Icon>[0]['name'];
8
7
  width?: string;
9
8
  options: Array<{
@@ -26,7 +25,7 @@ interface Props {
26
25
  <label style={`--sl-select-width: ${Astro.props.width}`}>
27
26
  <span class="sr-only">{Astro.props.label}</span>
28
27
  <Icon name={Astro.props.icon} class="icon label-icon" />
29
- <select value={Astro.props.value} autocomplete="off">
28
+ <select autocomplete="off">
30
29
  {
31
30
  Astro.props.options.map(({ value, selected, label }) => (
32
31
  <option value={value} selected={selected} set:html={label} />
@@ -19,11 +19,11 @@ interface SidebarState {
19
19
  * and `hash` are read from the current page.
20
20
  */
21
21
  const getState = (): SidebarState => {
22
- let open = [];
22
+ let open: SidebarState['open'] = [];
23
23
  const hash = target?.dataset.hash || '';
24
24
  try {
25
25
  const rawStoredState = sessionStorage.getItem(storageKey);
26
- const storedState = JSON.parse(rawStoredState || '{}');
26
+ const storedState = JSON.parse(rawStoredState || '{}') as SidebarState;
27
27
  if (Array.isArray(storedState.open) && storedState.hash === hash) open = storedState.open;
28
28
  } catch {}
29
29
  return {
@@ -37,9 +37,8 @@ const { sublist, nested } = Astro.props;
37
37
  <details
38
38
  open={flattenSidebar(entry.entries).some((i) => i.isCurrent) || !entry.collapsed}
39
39
  >
40
- <SidebarRestorePoint />
41
40
  <summary>
42
- <div class="group-label">
41
+ <span class="group-label">
43
42
  <span class="large">{entry.label}</span>
44
43
  {entry.badge && (
45
44
  <Badge
@@ -48,9 +47,10 @@ const { sublist, nested } = Astro.props;
48
47
  text={entry.badge.text}
49
48
  />
50
49
  )}
51
- </div>
50
+ </span>
52
51
  <Icon name="right-caret" class="caret" size="1.25rem" />
53
52
  </summary>
53
+ <SidebarRestorePoint />
54
54
  <Astro.self sublist={entry.entries} nested />
55
55
  </details>
56
56
  )}
@@ -7,7 +7,6 @@ import Select from './Select.astro';
7
7
  <Select
8
8
  icon="laptop"
9
9
  label={Astro.locals.t('themeSelect.accessibleLabel')}
10
- value="auto"
11
10
  options={[
12
11
  { label: Astro.locals.t('themeSelect.dark'), selected: false, value: 'dark' },
13
12
  { label: Astro.locals.t('themeSelect.light'), selected: false, value: 'light' },
@@ -15,7 +15,9 @@
15
15
 
16
16
  export * from 'astro-expressive-code';
17
17
 
18
- // @ts-ignore - Types are provided by the separate `expressive-code.d.ts` file
18
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
19
+ // @ts-ignore - Types are provided by the separate `expressive-code.d.ts` file and this may not be
20
+ // a type error for some users running `tsc --noEmit` depending on their `tsconfig.json` settings.
19
21
  export function defineEcConfig(config) {
20
22
  return config;
21
23
  }
@@ -3,7 +3,7 @@ import { AstroError } from 'astro/errors';
3
3
  export function throwInvalidAsideIconError(icon: string) {
4
4
  throw new AstroError(
5
5
  'Invalid aside icon',
6
- `An aside custom icon must be set to the name of one of Starlight\’s built-in icons, but received \`${icon}\`.\n\n` +
6
+ `An aside custom icon must be set to the name of one of Starlights built-in icons, but received \`${icon}\`.\n\n` +
7
7
  'See https://starlight.astro.build/reference/icons/#all-icons for a list of available icons.'
8
8
  );
9
9
  }
@@ -29,22 +29,22 @@ interface AsidesOptions {
29
29
  }
30
30
 
31
31
  /** Hacky function that generates an mdast HTML tree ready for conversion to HTML by rehype. */
32
- function h(el: string, attrs: Properties = {}, children: any[] = []): P {
32
+ function h(el: string, attrs: Properties = {}, children: unknown[] = []): P {
33
33
  const { tagName, properties } = _h(el, attrs);
34
34
  return {
35
35
  type: 'paragraph',
36
36
  data: { hName: tagName, hProperties: properties },
37
- children,
37
+ children: children as P['children'],
38
38
  };
39
39
  }
40
40
 
41
41
  /** Hacky function that generates an mdast SVG tree ready for conversion to HTML by rehype. */
42
- function s(el: string, attrs: Properties = {}, children: any[] = []): P {
42
+ function s(el: string, attrs: Properties = {}, children: unknown[] = []): P {
43
43
  const { tagName, properties } = _s(el, attrs);
44
44
  return {
45
45
  type: 'paragraph',
46
46
  data: { hName: tagName, hProperties: properties },
47
- children,
47
+ children: children as P['children'],
48
48
  };
49
49
  }
50
50
 
@@ -93,14 +93,16 @@ function transformUnhandledDirective(
93
93
  }
94
94
 
95
95
  /** Hacky function that generates the children of an mdast SVG tree. */
96
- function makeSvgChildNodes(children: Result['children']): any[] {
96
+ function makeSvgChildNodes(children: Result['children']): P[] {
97
97
  const nodes: P[] = [];
98
98
  for (const child of children) {
99
99
  if (child.type !== 'element') continue;
100
100
  nodes.push({
101
101
  type: 'paragraph',
102
102
  data: { hName: child.tagName, hProperties: child.properties },
103
- children: makeSvgChildNodes(child.children),
103
+ // We are explicitly casting to the expected type here due to the hacky nature of this
104
+ // function which only works with SVG elements.
105
+ children: makeSvgChildNodes(child.children) as unknown as P['children'],
104
106
  });
105
107
  }
106
108
  return nodes;
@@ -102,7 +102,7 @@ export const starlightExpressiveCode = ({
102
102
  ? (starlightConfig.expressiveCode as AstroExpressiveCodeOptions)
103
103
  : {};
104
104
 
105
- let docsPath = getCollectionPosixPath('docs', astroConfig.srcDir);
105
+ const docsPath = getCollectionPosixPath('docs', astroConfig.srcDir);
106
106
 
107
107
  return [
108
108
  astroExpressiveCode({
@@ -3,10 +3,7 @@ import type { StarlightConfig } from '../../types';
3
3
  import type { createTranslationSystemFromFs } from '../../utils/translations-fs';
4
4
  import { localeToLang } from '../shared/localeToLang';
5
5
 
6
- export function addTranslations(
7
- config: StarlightConfig,
8
- useTranslations: ReturnType<typeof createTranslationSystemFromFs>
9
- ) {
6
+ export function addTranslations(config: StarlightConfig, useTranslations: UseTranslations) {
10
7
  addTranslationsForLocale(config.defaultLocale.locale, config, useTranslations);
11
8
  if (config.isMultilingual) {
12
9
  for (const locale in config.locales) {
@@ -19,7 +16,7 @@ export function addTranslations(
19
16
  function addTranslationsForLocale(
20
17
  locale: string | undefined,
21
18
  config: StarlightConfig,
22
- useTranslations: ReturnType<typeof createTranslationSystemFromFs>
19
+ useTranslations: UseTranslations
23
20
  ) {
24
21
  const lang = localeToLang(config, locale);
25
22
  const t = useTranslations(lang);
@@ -35,3 +32,5 @@ function addTranslationsForLocale(
35
32
  pluginFramesTexts.overrideTexts(lang, { [ecId]: translation });
36
33
  });
37
34
  }
35
+
36
+ type UseTranslations = Awaited<ReturnType<typeof createTranslationSystemFromFs>>;
@@ -55,7 +55,7 @@ export default function rehypeAutolinkHeadings(
55
55
  {
56
56
  type: 'element',
57
57
  tagName: 'a',
58
- properties: { class: 'sl-anchor-link', href: '#' + node.properties.id },
58
+ properties: { class: 'sl-anchor-link', href: '#' + String(node.properties.id) },
59
59
  children: [AnchorLinkIcon, h('span', { class: 'sr-only' }, accessibleLabel)],
60
60
  }
61
61
  );
@@ -7,7 +7,7 @@ export function getSitemapConfig(opts: StarlightConfig): SitemapOptions {
7
7
  sitemapConfig.i18n = {
8
8
  defaultLocale: opts.defaultLocale.locale || 'root',
9
9
  locales: Object.fromEntries(
10
- Object.entries(opts.locales).map(([locale, config]) => [locale, config?.lang!])
10
+ Object.entries(opts.locales).map(([locale, config]) => [locale, config!.lang!])
11
11
  ),
12
12
  };
13
13
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrojs/starlight",
3
- "version": "0.35.2",
3
+ "version": "0.36.0",
4
4
  "description": "Build beautiful, high-performance documentation websites with Astro",
5
5
  "keywords": [
6
6
  "docs",
@@ -15,7 +15,7 @@ const route = await getRoute(Astro);
15
15
  */
16
16
  const renderResult = await render(route.entry);
17
17
 
18
- await attachRouteDataAndRunMiddleware(Astro, await useRouteData(Astro, route, renderResult));
18
+ await attachRouteDataAndRunMiddleware(Astro, useRouteData(Astro, route, renderResult));
19
19
 
20
20
  const { Content, entry } = Astro.locals.starlightRoute;
21
21
  ---
package/schema.ts CHANGED
@@ -30,7 +30,7 @@ const StarlightFrontmatterSchema = (context: SchemaContext) =>
30
30
  editUrl: z.union([z.string().url(), z.boolean()]).optional().default(true),
31
31
 
32
32
  /** Set custom `<head>` tags just for this page. */
33
- head: HeadConfigSchema(),
33
+ head: HeadConfigSchema({ source: 'content' }),
34
34
 
35
35
  /** Override global table of contents configuration for this page. */
36
36
  tableOfContents: TableOfContentsSchema().optional(),
@@ -123,7 +123,7 @@ type BaseSchemaWithoutEffects =
123
123
  type BaseSchema = BaseSchemaWithoutEffects | z.ZodEffects<BaseSchemaWithoutEffects>;
124
124
 
125
125
  /** Type that extends Starlight’s default schema with an optional, user-defined schema. */
126
- type ExtendedSchema<T extends BaseSchema | never = never> = [T] extends [never]
126
+ type ExtendedSchema<T extends BaseSchema = never> = [T] extends [never]
127
127
  ? DefaultSchema
128
128
  : T extends BaseSchema
129
129
  ? z.ZodIntersection<DefaultSchema, T>
@@ -155,7 +155,7 @@ interface DocsSchemaOpts<T extends BaseSchema> {
155
155
  }
156
156
 
157
157
  /** Content collection schema for Starlight’s `docs` collection. */
158
- export function docsSchema<T extends BaseSchema | never = never>(
158
+ export function docsSchema<T extends BaseSchema = never>(
159
159
  ...args: [DocsSchemaOpts<T>?]
160
160
  ): (context: SchemaContext) => ExtendedSchema<T> {
161
161
  const [options = {}] = args;
@@ -4,7 +4,9 @@ import type { StarlightExpressiveCodeOptions } from '../integrations/expressive-
4
4
  export const ExpressiveCodeSchema = () =>
5
5
  z
6
6
  .union([
7
- z.custom<StarlightExpressiveCodeOptions>((value) => typeof value === 'object' && value),
7
+ z.custom<StarlightExpressiveCodeOptions>(
8
+ (value) => typeof value === 'object' && (value as StarlightExpressiveCodeOptions)
9
+ ),
8
10
  z.boolean(),
9
11
  ])
10
12
  .describe(
package/schemas/head.ts CHANGED
@@ -1,16 +1,47 @@
1
1
  import { z } from 'astro/zod';
2
+ import yaml from 'js-yaml';
2
3
 
3
- export const HeadConfigSchema = () =>
4
+ export const HeadConfigSchema = ({
5
+ source,
6
+ }: {
7
+ /**
8
+ * Depending on the content being validated, either a user's config or a page's frontmatter,
9
+ * different error messages will be shown.
10
+ */
11
+ source: 'config' | 'content';
12
+ }) =>
4
13
  z
5
14
  .array(
6
- z.object({
7
- /** Name of the HTML tag to add to `<head>`, e.g. `'meta'`, `'link'`, or `'script'`. */
8
- tag: z.enum(['title', 'base', 'link', 'style', 'meta', 'script', 'noscript', 'template']),
9
- /** Attributes to set on the tag, e.g. `{ rel: 'stylesheet', href: '/custom.css' }`. */
10
- attrs: z.record(z.union([z.string(), z.boolean(), z.undefined()])).optional(),
11
- /** Content to place inside the tag (optional). */
12
- content: z.string().optional(),
13
- })
15
+ z
16
+ .object({
17
+ /** Name of the HTML tag to add to `<head>`, e.g. `'meta'`, `'link'`, or `'script'`. */
18
+ tag: z.enum(['title', 'base', 'link', 'style', 'meta', 'script', 'noscript', 'template']),
19
+ /** Attributes to set on the tag, e.g. `{ rel: 'stylesheet', href: '/custom.css' }`. */
20
+ attrs: z.record(z.union([z.string(), z.boolean(), z.undefined()])).optional(),
21
+ /** Content to place inside the tag (optional). */
22
+ content: z.string().optional(),
23
+ })
24
+ .superRefine((config, ctx) => {
25
+ if (config.tag !== 'meta' || config.content === undefined) return;
26
+ const { content, ...rest } = config;
27
+ const correctTag = {
28
+ ...rest,
29
+ attrs: { ...(config.attrs ?? { name: 'identifier' }), content: config.content },
30
+ };
31
+ const code =
32
+ source === 'config' ? JSON.stringify(correctTag, null, 2) : yaml.dump([correctTag]);
33
+ ctx.addIssue({
34
+ code: 'custom',
35
+ message:
36
+ `The \`head\` configuration includes a \`meta\` tag with \`content\` which is invalid HTML.\n` +
37
+ `You should instead use a \`content\` attribute ` +
38
+ (Object.keys(rest.attrs ?? {}).length === 0
39
+ ? 'with an additional attribute such as `name`, `property`, or `http-equiv` to identify the kind of metadata it represents '
40
+ : '') +
41
+ `in the \`attrs\` object:\n\n` +
42
+ code,
43
+ });
44
+ })
14
45
  )
15
46
  .default([]);
16
47
 
package/schemas/i18n.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from 'astro/zod';
2
2
 
3
- interface i18nSchemaOpts<T extends z.AnyZodObject = z.ZodObject<{}>> {
3
+ interface i18nSchemaOpts<T extends z.AnyZodObject = z.SomeZodObject> {
4
4
  /**
5
5
  * Extend Starlight’s i18n schema with additional fields.
6
6
  *
@@ -41,7 +41,7 @@ type ExtendedSchema<T extends z.AnyZodObject> = T extends z.AnyZodObject
41
41
  : DefaultI18nSchema;
42
42
 
43
43
  /** Content collection schema for Starlight’s optional `i18n` collection. */
44
- export function i18nSchema<T extends z.AnyZodObject = z.ZodObject<{}>>({
44
+ export function i18nSchema<T extends z.AnyZodObject = z.SomeZodObject>({
45
45
  extend = z.object({}) as T,
46
46
  }: i18nSchemaOpts<T> = {}): ExtendedSchema<T> {
47
47
  return defaultI18nSchema().merge(extend).passthrough() as ExtendedSchema<T>;
@@ -2,7 +2,15 @@
2
2
  .sl-markdown-content
3
3
  :not(a, strong, em, del, span, input, code, br)
4
4
  + :not(a, strong, em, del, span, input, code, br, :where(.not-content *)) {
5
- margin-top: 1rem;
5
+ margin-top: var(--sl-content-gap-y);
6
+ }
7
+
8
+ /* Vertical spacing between Markdown content blocks and UI Framework components using client directives. */
9
+ .sl-markdown-content
10
+ :not(a, strong, em, del, span, input, code, br)
11
+ + :is(astro-island, astro-slot)
12
+ > :not(a, strong, em, del, span, input, code, br, :where(.not-content *)):first-child {
13
+ margin-top: var(--sl-content-gap-y);
6
14
  }
7
15
 
8
16
  /* Headings after non-headings have more spacing. */
package/style/props.css CHANGED
@@ -101,6 +101,7 @@
101
101
  --sl-sidebar-pad-x: 1rem;
102
102
  --sl-content-width: 45rem;
103
103
  --sl-content-pad-x: 1rem;
104
+ --sl-content-gap-y: 1rem;
104
105
  --sl-main-pad: 0 0 3vh 0;
105
106
  --sl-menu-button-size: 2rem;
106
107
  --sl-nav-gap: var(--sl-content-pad-x);
@@ -15,7 +15,7 @@
15
15
  "tableOfContents.overview": "Überblick",
16
16
  "i18n.untranslatedContent": "Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.",
17
17
  "page.editLink": "Seite bearbeiten",
18
- "page.lastUpdated": "Zuletzt bearbeitet:",
18
+ "page.lastUpdated": "Zuletzt aktualisiert:",
19
19
  "page.previousLink": "Vorherige Seite",
20
20
  "page.nextLink": "Nächste Seite",
21
21
  "page.draft": "Dieser Inhalt ist ein Entwurf und wird nicht in den Produktions-Builds enthalten sein.",
@@ -115,13 +115,15 @@ if (isSynced) {
115
115
  }
116
116
 
117
117
  .tab {
118
+ display: flex;
118
119
  margin-bottom: -2px;
119
120
  }
120
121
  .tab > [role='tab'] {
121
122
  display: flex;
122
123
  align-items: center;
123
124
  gap: 0.5rem;
124
- padding: 0 1.25rem;
125
+ line-height: var(--sl-line-height-headings);
126
+ padding: 0.275rem 1.25rem;
125
127
  text-decoration: none;
126
128
  border-bottom: 2px solid var(--sl-color-gray-5);
127
129
  color: var(--sl-color-gray-3);
@@ -1,5 +1,4 @@
1
- const collectionNames = ['docs', 'i18n'] as const;
2
- export type StarlightCollection = (typeof collectionNames)[number];
1
+ export type StarlightCollection = 'docs' | 'i18n';
3
2
 
4
3
  /**
5
4
  * We still rely on the content collection folder structure to be fixed for now:
@@ -12,7 +12,7 @@ import type { UserI18nKeys, UserI18nSchema } from './translations';
12
12
  */
13
13
  export const I18nextNamespace = 'starlight' as const;
14
14
 
15
- export function createTranslationSystem<T extends i18nSchemaOutput>(
15
+ export async function createTranslationSystem<T extends i18nSchemaOutput>(
16
16
  config: Pick<StarlightConfig, 'defaultLocale' | 'locales'>,
17
17
  userTranslations: Record<string, T>,
18
18
  pluginTranslations: Record<string, T> = {}
@@ -42,7 +42,7 @@ export function createTranslationSystem<T extends i18nSchemaOutput>(
42
42
  }
43
43
 
44
44
  const i18n = i18next.createInstance();
45
- i18n.init({
45
+ await i18n.init({
46
46
  resources: translations,
47
47
  fallbackLng:
48
48
  config.defaultLocale.lang || config.defaultLocale?.locale || BuiltInDefaultLocale.lang,
@@ -72,7 +72,7 @@ export function createTranslationSystem<T extends i18nSchemaOutput>(
72
72
  lang ??= config.defaultLocale?.lang || BuiltInDefaultLocale.lang;
73
73
 
74
74
  const t = i18n.getFixedT(lang, I18nextNamespace) as I18nT;
75
- t.all = () => i18n.getResourceBundle(lang, I18nextNamespace);
75
+ t.all = () => i18n.getResourceBundle(lang, I18nextNamespace) as ReturnType<I18nT['all']>;
76
76
  t.exists = (key, options) => i18n.exists(key, { lng: lang, ns: I18nextNamespace, ...options });
77
77
  t.dir = (dirLang = lang) => i18n.dir(dirLang);
78
78
 
@@ -25,7 +25,7 @@ export function parseWithFriendlyErrors<T extends z.Schema>(
25
25
  input: z.input<T>,
26
26
  message: string
27
27
  ): z.output<T> {
28
- return processParsedData(schema.safeParse(input, { errorMap }), message);
28
+ return processParsedData<T>(schema.safeParse(input, { errorMap }), message);
29
29
  }
30
30
 
31
31
  /**
@@ -42,10 +42,13 @@ export async function parseAsyncWithFriendlyErrors<T extends z.Schema>(
42
42
  input: z.input<T>,
43
43
  message: string
44
44
  ): Promise<z.output<T>> {
45
- return processParsedData(await schema.safeParseAsync(input, { errorMap }), message);
45
+ return processParsedData<T>(await schema.safeParseAsync(input, { errorMap }), message);
46
46
  }
47
47
 
48
- function processParsedData(parsedData: z.SafeParseReturnType<any, any>, message: string) {
48
+ function processParsedData<T extends z.Schema>(
49
+ parsedData: z.SafeParseReturnType<T, T>,
50
+ message: string
51
+ ) {
49
52
  if (!parsedData.success) {
50
53
  throw new AstroError(message, parsedData.error.issues.map((i) => i.message).join('\n'));
51
54
  }
@@ -60,7 +63,7 @@ const errorMap: z.ZodErrorMap = (baseError, ctx) => {
60
63
  // raise a single error when `key` does not match:
61
64
  // > Did not match union.
62
65
  // > key: Expected `'tutorial' | 'blog'`, received 'foo'
63
- let typeOrLiteralErrByPath: Map<string, TypeOrLiteralErrByPathEntry> = new Map();
66
+ const typeOrLiteralErrByPath: Map<string, TypeOrLiteralErrByPathEntry> = new Map();
64
67
  for (const unionError of baseError.unionErrors.map((e) => e.errors).flat()) {
65
68
  if (unionError.code === 'invalid_type' || unionError.code === 'invalid_literal') {
66
69
  const flattenedErrorPath = flattenErrorPath(unionError.path);
@@ -69,7 +72,7 @@ const errorMap: z.ZodErrorMap = (baseError, ctx) => {
69
72
  } else {
70
73
  typeOrLiteralErrByPath.set(flattenedErrorPath, {
71
74
  code: unionError.code,
72
- received: (unionError as any).received,
75
+ received: unionError.received,
73
76
  expected: [unionError.expected],
74
77
  });
75
78
  }
@@ -130,7 +133,7 @@ const errorMap: z.ZodErrorMap = (baseError, ctx) => {
130
133
  baseErrorPath,
131
134
  getTypeOrLiteralMsg({
132
135
  code: baseError.code,
133
- received: (baseError as any).received,
136
+ received: baseError.received,
134
137
  expected: [baseError.expected],
135
138
  })
136
139
  ),
package/utils/head.ts CHANGED
@@ -7,7 +7,7 @@ import { fileWithBase } from './base';
7
7
  import { formatCanonical } from './canonical';
8
8
  import { localizedUrl } from './localizedUrl';
9
9
 
10
- const HeadSchema = HeadConfigSchema();
10
+ const HeadSchema = HeadConfigSchema({ source: 'content' });
11
11
 
12
12
  /** Get the head for the current page. */
13
13
  export function getHead(
package/utils/i18n.ts CHANGED
@@ -169,7 +169,7 @@ function getLocaleInfo(lang: string) {
169
169
  label: label[0]?.toLocaleUpperCase(locale) + label.slice(1),
170
170
  dir: getLocaleDir(locale),
171
171
  };
172
- } catch (error) {
172
+ } catch {
173
173
  throw new AstroError(
174
174
  `Failed to get locale informations for the '${lang}' locale.`,
175
175
  'Make sure to provide a valid BCP-47 tags (e.g. en, ar, or zh-CN).'
@@ -184,9 +184,11 @@ function getLocaleInfo(lang: string) {
184
184
  function getLocaleDir(locale: Intl.Locale): 'ltr' | 'rtl' {
185
185
  if ('textInfo' in locale) {
186
186
  // @ts-expect-error - `textInfo` is not typed but is available in v8 based environments.
187
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
187
188
  return locale.textInfo.direction;
188
189
  } else if ('getTextInfo' in locale) {
189
190
  // @ts-expect-error - `getTextInfo` is not typed but is available in some non-v8 based environments.
191
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
190
192
  return locale.getTextInfo().direction;
191
193
  }
192
194
  // Firefox does not support `textInfo` or `getTextInfo` yet so we fallback to a well-known list
@@ -254,7 +254,7 @@ function treeify(routes: Route[], locale: string | undefined, baseDir: string):
254
254
  const isLeaf = index === parts.length - 1;
255
255
 
256
256
  // Handle directory index pages by renaming them to `index`
257
- if (isLeaf && currentNode.hasOwnProperty(part)) {
257
+ if (isLeaf && Object.hasOwn(currentNode, part)) {
258
258
  currentNode = currentNode[part] as Dir;
259
259
  part = 'index';
260
260
  }
package/utils/plugins.ts CHANGED
@@ -52,14 +52,14 @@ export async function runPlugins(
52
52
  // Merge the translations injected by the plugin.
53
53
  for (const [locale, localeTranslations] of Object.entries(translations)) {
54
54
  pluginTranslations[locale] ??= {};
55
- Object.assign(pluginTranslations[locale]!, localeTranslations);
55
+ Object.assign(pluginTranslations[locale], localeTranslations);
56
56
  }
57
57
  },
58
58
  });
59
59
  }
60
60
  }
61
61
 
62
- const useTranslations = createTranslationSystemFromFs(
62
+ const useTranslations = await createTranslationSystemFromFs(
63
63
  starlightConfig,
64
64
  context.config,
65
65
  pluginTranslations
@@ -319,7 +319,9 @@ const configSetupHookSchema = z
319
319
  * }
320
320
  * }
321
321
  */
322
- useTranslations: z.any() as z.Schema<ReturnType<typeof createTranslationSystemFromFs>>,
322
+ useTranslations: z.any() as z.Schema<
323
+ Awaited<ReturnType<typeof createTranslationSystemFromFs>>
324
+ >,
323
325
  /**
324
326
  * A callback function to get the language for a given absolute file path. The returned
325
327
  * language can be used with the `useTranslations` helper to get UI strings for that
@@ -436,7 +438,7 @@ export type StarlightPlugin = z.input<typeof starlightPluginSchema>;
436
438
  export type HookParameters<
437
439
  Hook extends keyof StarlightPlugin['hooks'],
438
440
  HookFn = StarlightPlugin['hooks'][Hook],
439
- > = HookFn extends (...args: any) => any ? Parameters<HookFn>[0] : never;
441
+ > = HookFn extends (...args: any[]) => unknown ? Parameters<HookFn>[0] : never;
440
442
 
441
443
  export type StarlightUserConfigWithPlugins = StarlightUserConfig & {
442
444
  /**
@@ -32,11 +32,11 @@ export async function getRoute(context: APIContext): Promise<Route> {
32
32
  );
33
33
  }
34
34
 
35
- export async function useRouteData(
35
+ export function useRouteData(
36
36
  context: APIContext,
37
37
  route: Route,
38
38
  { Content, headings }: RenderResult
39
- ): Promise<StarlightRouteData> {
39
+ ): StarlightRouteData {
40
40
  const routeData = generateRouteData({ props: { ...route, headings }, context });
41
41
  return { ...routeData, Content };
42
42
  }
@@ -118,7 +118,7 @@ function getEditUrl({ entry }: PageProps): URL | undefined {
118
118
  export function getSiteTitle(lang: string): string {
119
119
  const defaultLang = config.defaultLocale.lang as string;
120
120
  if (lang && config.title[lang]) {
121
- return config.title[lang] as string;
121
+ return config.title[lang];
122
122
  }
123
123
  return config.title[defaultLang] as string;
124
124
  }
@@ -187,12 +187,12 @@ async function getStarlightPageFrontmatter(frontmatter: StarlightPageFrontmatter
187
187
  // well as containing the metadata properties and this ensures we handle those correctly.
188
188
  z.custom(
189
189
  (value) =>
190
- value &&
191
- (typeof value === 'function' || typeof value === 'object') &&
192
- 'src' in value &&
193
- 'width' in value &&
194
- 'height' in value &&
195
- 'format' in value,
190
+ (value &&
191
+ (typeof value === 'function' || typeof value === 'object') &&
192
+ 'src' in value &&
193
+ 'width' in value &&
194
+ 'height' in value &&
195
+ 'format' in value) as ReturnType<ImageFunction>,
196
196
  'Invalid image passed to `<StarlightPage>` component. Expected imported `ImageMetadata` object.'
197
197
  )) as ImageFunction,
198
198
  });
@@ -16,13 +16,13 @@ const contentCollectionFileExtensions = ['.json', '.yaml', '.yml'];
16
16
  *
17
17
  * @see [`./translations.ts`](./translations.ts)
18
18
  */
19
- export function createTranslationSystemFromFs<T extends i18nSchemaOutput>(
19
+ export async function createTranslationSystemFromFs<T extends i18nSchemaOutput>(
20
20
  opts: Pick<StarlightConfig, 'defaultLocale' | 'locales'>,
21
21
  { srcDir }: Pick<AstroConfig, 'srcDir'>,
22
22
  pluginTranslations: Record<string, T> = {}
23
23
  ) {
24
24
  /** All translation data from the i18n collection, keyed by `id`, which matches locale. */
25
- let userTranslations: Record<string, i18nSchemaOutput> = {};
25
+ const userTranslations: Record<string, i18nSchemaOutput> = {};
26
26
  try {
27
27
  const i18nDir = new URL('content/i18n/', srcDir);
28
28
  // Load the user’s i18n directory
@@ -34,11 +34,12 @@ export function createTranslationSystemFromFs<T extends i18nSchemaOutput>(
34
34
  const id = filePath.name;
35
35
  const url = new URL(filePath.base, i18nDir);
36
36
  const content = fs.readFileSync(new URL(file, i18nDir), 'utf-8');
37
- const data =
37
+ const data = (
38
38
  filePath.ext === '.json'
39
39
  ? JSON.parse(content)
40
- : yaml.load(content, { filename: fileURLToPath(url) });
41
- userTranslations[id] = data as i18nSchemaOutput;
40
+ : yaml.load(content, { filename: fileURLToPath(url) })
41
+ ) as i18nSchemaOutput;
42
+ userTranslations[id] = data;
42
43
  }
43
44
  } catch (e: unknown) {
44
45
  if (e instanceof Error && 'code' in e && e.code === 'ENOENT') {
@@ -8,6 +8,7 @@ import type { RemoveIndexSignature } from './types';
8
8
  import { getCollectionPathFromRoot } from './collection';
9
9
  import { stripExtension, stripLeadingSlash } from './path';
10
10
 
11
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
11
12
  // @ts-ignore - This may be a type error in projects without an i18n collection and running
12
13
  // `tsc --noEmit` in their project. Note that it is not possible to inline this type in
13
14
  // `UserI18nSchema` because this would break types for users having multiple data collections.
@@ -28,6 +29,7 @@ async function loadTranslations() {
28
29
  const warn = console.warn;
29
30
  console.warn = () => {};
30
31
  const userTranslations: Record<string, UserI18nSchema> = Object.fromEntries(
32
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
31
33
  // @ts-ignore — may be a type error in projects without an i18n collection
32
34
  (await getCollection('i18n')).map(({ id, data, filePath }) => {
33
35
  const lang =
@@ -49,7 +51,7 @@ async function loadTranslations() {
49
51
  * const t = useTranslations('en');
50
52
  * const label = t('search.label'); // => 'Search'
51
53
  */
52
- export const useTranslations = createTranslationSystem(
54
+ export const useTranslations = await createTranslationSystem(
53
55
  config,
54
56
  await loadTranslations(),
55
57
  pluginTranslations
@@ -149,7 +149,7 @@ const UserConfigSchema = z.object({
149
149
  * ],
150
150
  * })
151
151
  */
152
- head: HeadConfigSchema(),
152
+ head: HeadConfigSchema({ source: 'config' }),
153
153
 
154
154
  /**
155
155
  * Provide CSS files to customize the look and feel of your Starlight site.