@astrojs/starlight 0.31.1 → 0.32.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/components/Banner.astro +1 -3
  3. package/components/ContentPanel.astro +0 -4
  4. package/components/DraftContentNotice.astro +0 -1
  5. package/components/EditLink.astro +1 -2
  6. package/components/FallbackContentNotice.astro +0 -1
  7. package/components/Footer.astro +3 -5
  8. package/components/Head.astro +1 -2
  9. package/components/Header.astro +5 -6
  10. package/components/Hero.astro +1 -2
  11. package/components/LanguageSelect.astro +2 -3
  12. package/components/LastUpdated.astro +1 -3
  13. package/components/MarkdownContent.astro +0 -1
  14. package/components/MobileMenuFooter.astro +3 -4
  15. package/components/MobileMenuToggle.astro +0 -1
  16. package/components/MobileTableOfContents.astro +1 -2
  17. package/components/Page.astro +32 -32
  18. package/components/PageFrame.astro +2 -3
  19. package/components/PageSidebar.astro +3 -5
  20. package/components/PageTitle.astro +1 -2
  21. package/components/Pagination.astro +1 -2
  22. package/components/Search.astro +17 -3
  23. package/components/Sidebar.astro +3 -5
  24. package/components/SidebarPersister.astro +1 -2
  25. package/components/SidebarSublist.astro +2 -1
  26. package/components/SiteTitle.astro +1 -2
  27. package/components/SkipLink.astro +0 -1
  28. package/components/SocialIcons.astro +0 -1
  29. package/components/StarlightPage.astro +7 -3
  30. package/components/TableOfContents.astro +1 -2
  31. package/components/ThemeProvider.astro +0 -1
  32. package/components/ThemeSelect.astro +0 -1
  33. package/components/TwoColumnContent.astro +1 -5
  34. package/index.ts +7 -9
  35. package/integrations/asides.ts +4 -7
  36. package/integrations/expressive-code/index.ts +15 -10
  37. package/integrations/shared/{pathToLocale.ts → absolutePathToLang.ts} +7 -5
  38. package/integrations/virtual-user-config.ts +27 -0
  39. package/locals.d.ts +26 -0
  40. package/locals.ts +37 -2
  41. package/package.json +5 -3
  42. package/props.ts +13 -1
  43. package/route-data.ts +11 -0
  44. package/routes/common.astro +5 -11
  45. package/routes/ssr/index.astro +1 -1
  46. package/routes/static/404.astro +1 -41
  47. package/routes/static/index.astro +1 -4
  48. package/schemas/pagefind.ts +97 -33
  49. package/types.ts +1 -0
  50. package/utils/i18n.ts +0 -20
  51. package/utils/navigation.ts +19 -36
  52. package/utils/plugins.ts +316 -141
  53. package/utils/{route-data.ts → routing/data.ts} +56 -30
  54. package/utils/{routing.ts → routing/index.ts} +6 -44
  55. package/utils/routing/middleware.ts +81 -0
  56. package/utils/routing/types.ts +96 -0
  57. package/utils/slugs.ts +2 -10
  58. package/utils/starlight-page.ts +2 -10
  59. package/utils/user-config.ts +8 -0
  60. package/virtual-internal.d.ts +4 -0
@@ -1,6 +1,6 @@
1
1
  import { AstroError } from 'astro/errors';
2
- import config from 'virtual:starlight/user-config';
3
2
  import project from 'virtual:starlight/project-context';
3
+ import config from 'virtual:starlight/user-config';
4
4
  import type { Badge, I18nBadge, I18nBadgeConfig } from '../schemas/badge';
5
5
  import type { PrevNextLinkConfig } from '../schemas/prevNextLink';
6
6
  import type {
@@ -10,6 +10,7 @@ import type {
10
10
  SidebarItem,
11
11
  SidebarLinkItem,
12
12
  } from '../schemas/sidebar';
13
+ import { getCollectionPathFromRoot } from './collection';
13
14
  import { createPathFormatter } from './createPathFormatter';
14
15
  import { formatPath } from './format-path';
15
16
  import { BuiltInDefaultLocale, pickLang } from './i18n';
@@ -19,10 +20,16 @@ import {
19
20
  stripExtension,
20
21
  stripLeadingAndTrailingSlashes,
21
22
  } from './path';
22
- import { getLocaleRoutes, routes, type Route } from './routing';
23
+ import { getLocaleRoutes, routes } from './routing';
24
+ import type {
25
+ SidebarGroup,
26
+ SidebarLink,
27
+ PaginationLinks,
28
+ Route,
29
+ SidebarEntry,
30
+ } from './routing/types';
23
31
  import { localeToLang, localizedId, slugToPathname } from './slugs';
24
32
  import type { StarlightConfig } from './user-config';
25
- import { getCollectionPathFromRoot } from './collection';
26
33
 
27
34
  const DirKey = Symbol('DirKey');
28
35
  const SlugKey = Symbol('SlugKey');
@@ -31,25 +38,6 @@ const neverPathFormatter = createPathFormatter({ trailingSlash: 'never' });
31
38
 
32
39
  const docsCollectionPathFromRoot = getCollectionPathFromRoot('docs', project);
33
40
 
34
- export interface Link {
35
- type: 'link';
36
- label: string;
37
- href: string;
38
- isCurrent: boolean;
39
- badge: Badge | undefined;
40
- attrs: LinkHTMLAttributes;
41
- }
42
-
43
- interface Group {
44
- type: 'group';
45
- label: string;
46
- entries: (Link | Group)[];
47
- collapsed: boolean;
48
- badge: Badge | undefined;
49
- }
50
-
51
- export type SidebarEntry = Link | Group;
52
-
53
41
  /**
54
42
  * A representation of the route structure. For each object entry:
55
43
  * if it’s a folder, the key is the directory name, and value is the directory
@@ -107,7 +95,7 @@ function groupFromAutogenerateConfig(
107
95
  locale: string | undefined,
108
96
  routes: Route[],
109
97
  currentPathname: string
110
- ): Group {
98
+ ): SidebarGroup {
111
99
  const { collapsed: subgroupCollapsed, directory } = item.autogenerate;
112
100
  const localeDir = locale ? locale + '/' + directory : directory;
113
101
  const dirDocs = routes.filter((doc) => {
@@ -186,7 +174,7 @@ function makeSidebarLink(
186
174
  label: string,
187
175
  badge?: Badge,
188
176
  attrs?: LinkHTMLAttributes
189
- ): Link {
177
+ ): SidebarLink {
190
178
  if (!isAbsolute(href)) {
191
179
  href = formatPath(href);
192
180
  }
@@ -203,7 +191,7 @@ function makeLink({
203
191
  href: string;
204
192
  badge?: Badge | undefined;
205
193
  attrs?: LinkHTMLAttributes | undefined;
206
- }): Link {
194
+ }): SidebarLink {
207
195
  return { type: 'link', ...opts, badge, isCurrent: false, attrs };
208
196
  }
209
197
 
@@ -275,7 +263,7 @@ function treeify(routes: Route[], locale: string | undefined, baseDir: string):
275
263
  }
276
264
 
277
265
  /** Create a link entry for a given content collection entry. */
278
- function linkFromRoute(route: Route): Link {
266
+ function linkFromRoute(route: Route): SidebarLink {
279
267
  return makeSidebarLink(
280
268
  slugToPathname(route.slug),
281
269
  route.entry.data.sidebar.label || route.entry.data.title,
@@ -315,7 +303,7 @@ function groupFromDir(
315
303
  currentPathname: string,
316
304
  locale: string | undefined,
317
305
  collapsed: boolean
318
- ): Group {
306
+ ): SidebarGroup {
319
307
  const entries = sortDirEntries(Object.entries(dir)).map(([key, dirOrRoute]) =>
320
308
  dirToItem(dirOrRoute, `${fullPath}/${key}`, key, currentPathname, locale, collapsed)
321
309
  );
@@ -451,7 +439,7 @@ function recursivelyBuildSidebarIdentity(sidebar: SidebarEntry[]): string {
451
439
  }
452
440
 
453
441
  /** Turn the nested tree structure of a sidebar into a flat list of all the links. */
454
- export function flattenSidebar(sidebar: SidebarEntry[]): Link[] {
442
+ export function flattenSidebar(sidebar: SidebarEntry[]): SidebarLink[] {
455
443
  return sidebar.flatMap((entry) =>
456
444
  entry.type === 'group' ? flattenSidebar(entry.entries) : entry
457
445
  );
@@ -465,12 +453,7 @@ export function getPrevNextLinks(
465
453
  prev?: PrevNextLinkConfig;
466
454
  next?: PrevNextLinkConfig;
467
455
  }
468
- ): {
469
- /** Link to previous page in the sidebar. */
470
- prev: Link | undefined;
471
- /** Link to next page in the sidebar. */
472
- next: Link | undefined;
473
- } {
456
+ ): PaginationLinks {
474
457
  const entries = flattenSidebar(sidebar);
475
458
  const currentIndex = entries.findIndex((entry) => entry.isCurrent);
476
459
  const prev = applyPrevNextLinkConfig(entries[currentIndex - 1], paginationEnabled, config.prev);
@@ -484,10 +467,10 @@ export function getPrevNextLinks(
484
467
 
485
468
  /** Apply a prev/next link config to a navigation link. */
486
469
  function applyPrevNextLinkConfig(
487
- link: Link | undefined,
470
+ link: SidebarLink | undefined,
488
471
  paginationEnabled: boolean,
489
472
  config: PrevNextLinkConfig | undefined
490
- ): Link | undefined {
473
+ ): SidebarLink | undefined {
491
474
  // Explicitly remove the link.
492
475
  if (config === false) return undefined;
493
476
  // Use the generated link if any.
package/utils/plugins.ts CHANGED
@@ -1,8 +1,15 @@
1
- import type { AstroIntegration, HookParameters } from 'astro';
1
+ import type { AstroIntegration, HookParameters as AstroHookParameters } from 'astro';
2
+ import { AstroError } from 'astro/errors';
2
3
  import { z } from 'astro/zod';
3
- import { StarlightConfigSchema, type StarlightUserConfig } from '../utils/user-config';
4
4
  import { parseWithFriendlyErrors } from '../utils/error-map';
5
+ import {
6
+ StarlightConfigSchema,
7
+ type StarlightConfig,
8
+ type StarlightUserConfig,
9
+ } from '../utils/user-config';
5
10
  import type { UserI18nSchema } from './translations';
11
+ import { createTranslationSystemFromFs } from './translations-fs';
12
+ import { absolutePathToLang as getAbsolutePathFromLang } from '../integrations/shared/absolutePathToLang';
6
13
 
7
14
  /**
8
15
  * Runs Starlight plugins in the order that they are configured after validating the user-provided
@@ -30,22 +37,61 @@ export async function runPlugins(
30
37
  'Invalid plugins config passed to starlight integration'
31
38
  );
32
39
 
33
- // A list of Astro integrations added by the various plugins.
34
- const integrations: AstroIntegration[] = [];
35
40
  // A list of translations injected by the various plugins keyed by locale.
36
41
  const pluginTranslations: PluginTranslations = {};
42
+ // A list of route middleware added by the various plugins.
43
+ const routeMiddlewareConfigs: Array<z.output<typeof routeMiddlewareConfigSchema>> = [];
44
+
45
+ for (const {
46
+ hooks: { 'i18n:setup': i18nSetup },
47
+ } of pluginsConfig) {
48
+ if (i18nSetup) {
49
+ await i18nSetup({
50
+ injectTranslations(translations) {
51
+ // Merge the translations injected by the plugin.
52
+ for (const [locale, localeTranslations] of Object.entries(translations)) {
53
+ pluginTranslations[locale] ??= {};
54
+ Object.assign(pluginTranslations[locale]!, localeTranslations);
55
+ }
56
+ },
57
+ });
58
+ }
59
+ }
60
+
61
+ const useTranslations = createTranslationSystemFromFs(
62
+ starlightConfig,
63
+ context.config,
64
+ pluginTranslations
65
+ );
66
+
67
+ function absolutePathToLang(path: string) {
68
+ return getAbsolutePathFromLang(path, { astroConfig: context.config, starlightConfig });
69
+ }
70
+
71
+ // A list of Astro integrations added by the various plugins.
72
+ const integrations: AstroIntegration[] = [];
37
73
 
38
74
  for (const {
39
75
  name,
40
- hooks: { setup },
76
+ hooks: { 'config:setup': configSetup, setup: deprecatedSetup },
41
77
  } of pluginsConfig) {
78
+ // A refinement in the schema ensures that at least one of the two hooks is defined.
79
+ const setup = (configSetup ?? deprecatedSetup)!;
80
+
42
81
  await setup({
43
82
  config: pluginsUserConfig ? { ...userConfig, plugins: pluginsUserConfig } : userConfig,
44
83
  updateConfig(newConfig) {
45
84
  // Ensure that plugins do not update the `plugins` config key.
46
85
  if ('plugins' in newConfig) {
47
- throw new Error(
48
- `The '${name}' plugin tried to update the 'plugins' config key which is not supported.`
86
+ throw new AstroError(
87
+ `The \`${name}\` plugin tried to update the \`plugins\` config key which is not supported.`
88
+ );
89
+ }
90
+ if ('routeMiddleware' in newConfig) {
91
+ throw new AstroError(
92
+ `The \`${name}\` plugin tried to update the \`routeMiddleware\` config key which is not supported.`,
93
+ 'Use the `addRouteMiddleware()` utility instead.\n' +
94
+ 'See https://starlight.astro.build/reference/plugins/#addroutemiddleware for more details.'
49
95
  );
50
96
  }
51
97
 
@@ -65,6 +111,9 @@ export async function runPlugins(
65
111
  // Collect any Astro integrations added by the plugin.
66
112
  integrations.push(integration);
67
113
  },
114
+ addRouteMiddleware(middlewareConfig) {
115
+ routeMiddlewareConfigs.push(middlewareConfig);
116
+ },
68
117
  astroConfig: {
69
118
  ...context.config,
70
119
  integrations: [...context.config.integrations, ...integrations],
@@ -72,22 +121,37 @@ export async function runPlugins(
72
121
  command: context.command,
73
122
  isRestart: context.isRestart,
74
123
  logger: context.logger.fork(name),
75
- injectTranslations(translations) {
76
- // Merge the translations injected by the plugin.
77
- for (const [locale, localeTranslations] of Object.entries(translations)) {
78
- pluginTranslations[locale] ??= {};
79
- Object.assign(pluginTranslations[locale]!, localeTranslations);
80
- }
81
- },
124
+ useTranslations,
125
+ absolutePathToLang,
82
126
  });
83
127
  }
84
128
 
85
- return { integrations, starlightConfig, pluginTranslations };
129
+ applyPluginMiddleware(routeMiddlewareConfigs, starlightConfig);
130
+
131
+ return { integrations, starlightConfig, pluginTranslations, useTranslations, absolutePathToLang };
132
+ }
133
+
134
+ /** Updates `routeMiddleware` in the Starlight config to add plugin middlewares in the correct order. */
135
+ function applyPluginMiddleware(
136
+ routeMiddlewareConfigs: { entrypoint: string; order: 'default' | 'pre' | 'post' }[],
137
+ starlightConfig: StarlightConfig
138
+ ) {
139
+ const middlewareBuckets = routeMiddlewareConfigs.reduce<
140
+ Record<'pre' | 'default' | 'post', string[]>
141
+ >(
142
+ (buckets, { entrypoint, order = 'default' }) => {
143
+ buckets[order].push(entrypoint);
144
+ return buckets;
145
+ },
146
+ { pre: [], default: [], post: [] }
147
+ );
148
+ starlightConfig.routeMiddleware.unshift(...middlewareBuckets.pre);
149
+ starlightConfig.routeMiddleware.push(...middlewareBuckets.default, ...middlewareBuckets.post);
86
150
  }
87
151
 
88
152
  export function injectPluginTranslationsTypes(
89
153
  translations: PluginTranslations,
90
- injectTypes: HookParameters<'astro:config:done'>['injectTypes']
154
+ injectTypes: AstroHookParameters<'astro:config:done'>['injectTypes']
91
155
  ) {
92
156
  const allKeys = new Set<string>();
93
157
 
@@ -118,140 +182,246 @@ const astroIntegrationSchema = z.object({
118
182
  hooks: z.object({}).passthrough().default({}),
119
183
  }) as z.Schema<AstroIntegration>;
120
184
 
185
+ const routeMiddlewareConfigSchema = z.object({
186
+ entrypoint: z.string(),
187
+ order: z.enum(['pre', 'post', 'default']).default('default'),
188
+ });
189
+
121
190
  const baseStarlightPluginSchema = z.object({
122
191
  /** Name of the Starlight plugin. */
123
192
  name: z.string(),
124
193
  });
125
194
 
195
+ const configSetupHookSchema = z
196
+ .function(
197
+ z.tuple([
198
+ z.object({
199
+ /**
200
+ * A read-only copy of the user-supplied Starlight configuration.
201
+ *
202
+ * Note that this configuration may have been updated by other plugins configured
203
+ * before this one.
204
+ */
205
+ config: z.any() as z.Schema<
206
+ // The configuration passed to plugins should contains the list of plugins.
207
+ StarlightUserConfig & { plugins?: z.input<typeof baseStarlightPluginSchema>[] }
208
+ >,
209
+ /**
210
+ * A callback function to update the user-supplied Starlight configuration.
211
+ *
212
+ * You only need to provide the configuration values that you want to update but no deep
213
+ * merge is performed.
214
+ *
215
+ * @example
216
+ * {
217
+ * name: 'My Starlight Plugin',
218
+ * hooks: {
219
+ * 'config:setup'({ updateConfig }) {
220
+ * updateConfig({
221
+ * description: 'Custom description',
222
+ * });
223
+ * }
224
+ * }
225
+ * }
226
+ */
227
+ updateConfig: z.function(
228
+ z.tuple([
229
+ z.record(z.any()) as z.Schema<Partial<Omit<StarlightUserConfig, 'routeMiddleware'>>>,
230
+ ]),
231
+ z.void()
232
+ ),
233
+ /**
234
+ * A callback function to add an Astro integration required by this plugin.
235
+ *
236
+ * @see https://docs.astro.build/en/reference/integrations-reference/
237
+ *
238
+ * @example
239
+ * {
240
+ * name: 'My Starlight Plugin',
241
+ * hooks: {
242
+ * 'config:setup'({ addIntegration }) {
243
+ * addIntegration({
244
+ * name: 'My Plugin Astro Integration',
245
+ * hooks: {
246
+ * 'astro:config:setup': () => {
247
+ * // …
248
+ * },
249
+ * },
250
+ * });
251
+ * }
252
+ * }
253
+ * }
254
+ */
255
+ addIntegration: z.function(z.tuple([astroIntegrationSchema]), z.void()),
256
+ /**
257
+ * A callback function to register additional route middleware handlers.
258
+ *
259
+ * If the order of execution is important, a plugin can use the `order` option to enforce
260
+ * running first or last.
261
+ *
262
+ * @example
263
+ * {
264
+ * name: 'My Starlight Plugin',
265
+ * hooks: {
266
+ * setup({ addRouteMiddleware }) {
267
+ * addRouteMiddleware({ entrypoint: '@me/my-plugin/route-middleware' });
268
+ * },
269
+ * },
270
+ * }
271
+ */
272
+ addRouteMiddleware: z.function(z.tuple([routeMiddlewareConfigSchema]), z.void()),
273
+ /**
274
+ * A read-only copy of the user-supplied Astro configuration.
275
+ *
276
+ * Note that this configuration is resolved before any other integrations have run.
277
+ *
278
+ * @see https://docs.astro.build/en/reference/integrations-reference/#config-option
279
+ */
280
+ astroConfig: z.any() as z.Schema<StarlightPluginContext['config']>,
281
+ /**
282
+ * The command used to run Starlight.
283
+ *
284
+ * @see https://docs.astro.build/en/reference/integrations-reference/#command-option
285
+ */
286
+ command: z.any() as z.Schema<StarlightPluginContext['command']>,
287
+ /**
288
+ * `false` when the dev server starts, `true` when a reload is triggered.
289
+ *
290
+ * @see https://docs.astro.build/en/reference/integrations-reference/#isrestart-option
291
+ */
292
+ isRestart: z.any() as z.Schema<StarlightPluginContext['isRestart']>,
293
+ /**
294
+ * An instance of the Astro integration logger with all logged messages prefixed with the
295
+ * plugin name.
296
+ *
297
+ * @see https://docs.astro.build/en/reference/integrations-reference/#astrointegrationlogger
298
+ */
299
+ logger: z.any() as z.Schema<StarlightPluginContext['logger']>,
300
+ /**
301
+ * A callback function to generate a utility function to access UI strings for a given
302
+ * language.
303
+ *
304
+ * @see https://starlight.astro.build/guides/i18n/#using-ui-translations
305
+ *
306
+ * @example
307
+ * {
308
+ * name: 'My Starlight Plugin',
309
+ * hooks: {
310
+ * 'config:setup'({ useTranslations, logger }) {
311
+ * const t = useTranslations('en');
312
+ * logger.info(t('builtWithStarlight.label'));
313
+ * // ^ Logs 'Built with Starlight' to the console.
314
+ * }
315
+ * }
316
+ * }
317
+ */
318
+ useTranslations: z.any() as z.Schema<ReturnType<typeof createTranslationSystemFromFs>>,
319
+ /**
320
+ * A callback function to get the language for a given absolute file path. The returned
321
+ * language can be used with the `useTranslations` helper to get UI strings for that
322
+ * language.
323
+ *
324
+ * This can be particularly useful in remark or rehype plugins to get the language for
325
+ * the current file being processed and use it to get the appropriate UI strings for that
326
+ * language.
327
+ *
328
+ * @example
329
+ * {
330
+ * name: 'My Starlight Plugin',
331
+ * hooks: {
332
+ * 'config:setup'({ absolutePathToLang, useTranslations, logger }) {
333
+ * const lang = absolutePathToLang('/absolute/path/to/project/src/content/docs/fr/index.mdx');
334
+ * const t = useTranslations(lang);
335
+ * logger.info(t('aside.tip'));
336
+ * // ^ Logs 'Astuce' to the console.
337
+ * }
338
+ * }
339
+ * }
340
+ */
341
+ absolutePathToLang: z.function(z.tuple([z.string()]), z.string()),
342
+ }),
343
+ ]),
344
+ z.union([z.void(), z.promise(z.void())])
345
+ )
346
+ .optional();
347
+
126
348
  /**
127
349
  * A plugin `config` and `updateConfig` argument are purposely not validated using the Starlight
128
350
  * user config schema but properly typed for user convenience because we do not want to run any of
129
351
  * the Zod `transform`s used in the user config schema when running plugins.
130
352
  */
131
- const starlightPluginSchema = baseStarlightPluginSchema.extend({
132
- /** The different hooks available to the plugin. */
133
- hooks: z.object({
134
- /**
135
- * Plugin setup function called with an object containing various values that can be used by
136
- * the plugin to interact with Starlight.
137
- */
138
- setup: z.function(
139
- z.tuple([
140
- z.object({
141
- /**
142
- * A read-only copy of the user-supplied Starlight configuration.
143
- *
144
- * Note that this configuration may have been updated by other plugins configured
145
- * before this one.
146
- */
147
- config: z.any() as z.Schema<
148
- // The configuration passed to plugins should contains the list of plugins.
149
- StarlightUserConfig & { plugins?: z.input<typeof baseStarlightPluginSchema>[] }
150
- >,
151
- /**
152
- * A callback function to update the user-supplied Starlight configuration.
153
- *
154
- * You only need to provide the configuration values that you want to update but no deep
155
- * merge is performed.
156
- *
157
- * @example
158
- * {
159
- * name: 'My Starlight Plugin',
160
- * hooks: {
161
- * setup({ updateConfig }) {
162
- * updateConfig({
163
- * description: 'Custom description',
164
- * });
165
- * }
166
- * }
167
- * }
168
- */
169
- updateConfig: z.function(
170
- z.tuple([z.record(z.any()) as z.Schema<Partial<StarlightUserConfig>>]),
171
- z.void()
172
- ),
173
- /**
174
- * A callback function to add an Astro integration required by this plugin.
175
- *
176
- * @see https://docs.astro.build/en/reference/integrations-reference/
177
- *
178
- * @example
179
- * {
180
- * name: 'My Starlight Plugin',
181
- * hooks: {
182
- * setup({ addIntegration }) {
183
- * addIntegration({
184
- * name: 'My Plugin Astro Integration',
185
- * hooks: {
186
- * 'astro:config:setup': () => {
187
- * //
188
- * },
189
- * },
190
- * });
191
- * }
192
- * }
193
- * }
194
- */
195
- addIntegration: z.function(z.tuple([astroIntegrationSchema]), z.void()),
196
- /**
197
- * A read-only copy of the user-supplied Astro configuration.
198
- *
199
- * Note that this configuration is resolved before any other integrations have run.
200
- *
201
- * @see https://docs.astro.build/en/reference/integrations-reference/#config-option
202
- */
203
- astroConfig: z.any() as z.Schema<StarlightPluginContext['config']>,
204
- /**
205
- * The command used to run Starlight.
206
- *
207
- * @see https://docs.astro.build/en/reference/integrations-reference/#command-option
208
- */
209
- command: z.any() as z.Schema<StarlightPluginContext['command']>,
210
- /**
211
- * `false` when the dev server starts, `true` when a reload is triggered.
212
- *
213
- * @see https://docs.astro.build/en/reference/integrations-reference/#isrestart-option
214
- */
215
- isRestart: z.any() as z.Schema<StarlightPluginContext['isRestart']>,
216
- /**
217
- * An instance of the Astro integration logger with all logged messages prefixed with the
218
- * plugin name.
219
- *
220
- * @see https://docs.astro.build/en/reference/integrations-reference/#astrointegrationlogger
221
- */
222
- logger: z.any() as z.Schema<StarlightPluginContext['logger']>,
223
- /**
224
- * A callback function to add or update translations strings.
225
- *
226
- * @see https://starlight.astro.build/guides/i18n/#extend-translation-schema
227
- *
228
- * @example
229
- * {
230
- * name: 'My Starlight Plugin',
231
- * hooks: {
232
- * setup({ injectTranslations }) {
233
- * injectTranslations({
234
- * en: {
235
- * 'myPlugin.doThing': 'Do the thing',
236
- * },
237
- * fr: {
238
- * 'myPlugin.doThing': 'Faire le truc',
239
- * },
240
- * });
241
- * }
242
- * }
243
- * }
244
- */
245
- injectTranslations: z.function(
246
- z.tuple([z.record(z.string(), z.record(z.string(), z.string()))]),
247
- z.void()
248
- ),
249
- }),
250
- ]),
251
- z.union([z.void(), z.promise(z.void())])
252
- ),
253
- }),
254
- });
353
+ const starlightPluginSchema = baseStarlightPluginSchema
354
+ .extend({
355
+ /** The different hooks available to the plugin. */
356
+ hooks: z.object({
357
+ /**
358
+ * Plugin internationalization setup function allowing to inject translations strings for the
359
+ * plugin in various locales. These translations will be available in the `config:setup` hook
360
+ * and plugin UI.
361
+ */
362
+ 'i18n:setup': z
363
+ .function(
364
+ z.tuple([
365
+ z.object({
366
+ /**
367
+ * A callback function to add or update translations strings.
368
+ *
369
+ * @see https://starlight.astro.build/guides/i18n/#extend-translation-schema
370
+ *
371
+ * @example
372
+ * {
373
+ * name: 'My Starlight Plugin',
374
+ * hooks: {
375
+ * 'i18n:setup'({ injectTranslations }) {
376
+ * injectTranslations({
377
+ * en: {
378
+ * 'myPlugin.doThing': 'Do the thing',
379
+ * },
380
+ * fr: {
381
+ * 'myPlugin.doThing': 'Faire le truc',
382
+ * },
383
+ * });
384
+ * }
385
+ * }
386
+ * }
387
+ */
388
+ injectTranslations: z.function(
389
+ z.tuple([z.record(z.string(), z.record(z.string(), z.string()))]),
390
+ z.void()
391
+ ),
392
+ }),
393
+ ]),
394
+ z.union([z.void(), z.promise(z.void())])
395
+ )
396
+ .optional(),
397
+ /**
398
+ * Plugin configuration setup function called with an object containing various values that
399
+ * can be used by the plugin to interact with Starlight.
400
+ */
401
+ 'config:setup': configSetupHookSchema,
402
+ /**
403
+ * @deprecated Use the `config:setup` hook instead as `setup` will be removed in a future
404
+ * version.
405
+ */
406
+ setup: configSetupHookSchema,
407
+ }),
408
+ })
409
+ .superRefine((plugin, ctx) => {
410
+ if (!plugin.hooks['config:setup'] && !plugin.hooks.setup) {
411
+ ctx.addIssue({
412
+ code: z.ZodIssueCode.custom,
413
+ message: 'A plugin must define at least a `config:setup` hook.',
414
+ });
415
+ } else if (plugin.hooks['config:setup'] && plugin.hooks.setup) {
416
+ ctx.addIssue({
417
+ code: z.ZodIssueCode.custom,
418
+ message:
419
+ 'A plugin cannot define both a `config:setup` and `setup` hook. ' +
420
+ 'As `setup` is deprecated and will be removed in a future version, ' +
421
+ 'consider using `config:setup` instead.',
422
+ });
423
+ }
424
+ });
255
425
 
256
426
  const starlightPluginsConfigSchema = z.array(starlightPluginSchema).default([]);
257
427
 
@@ -259,6 +429,11 @@ type StarlightPluginsUserConfig = z.input<typeof starlightPluginsConfigSchema>;
259
429
 
260
430
  export type StarlightPlugin = z.input<typeof starlightPluginSchema>;
261
431
 
432
+ export type HookParameters<
433
+ Hook extends keyof StarlightPlugin['hooks'],
434
+ HookFn = StarlightPlugin['hooks'][Hook],
435
+ > = HookFn extends (...args: any) => any ? Parameters<HookFn>[0] : never;
436
+
262
437
  export type StarlightUserConfigWithPlugins = StarlightUserConfig & {
263
438
  /**
264
439
  * A list of plugins to extend Starlight with.
@@ -273,7 +448,7 @@ export type StarlightUserConfigWithPlugins = StarlightUserConfig & {
273
448
  };
274
449
 
275
450
  export type StarlightPluginContext = Pick<
276
- Parameters<NonNullable<AstroIntegration['hooks']['astro:config:setup']>>[0],
451
+ AstroHookParameters<'astro:config:setup'>,
277
452
  'command' | 'config' | 'isRestart' | 'logger'
278
453
  >;
279
454