@astrojs/starlight
Version:
Build beautiful, high-performance documentation websites with Astro
188 lines (175 loc) • 6.87 kB
text/typescript
/**
* These triple-slash directives defines dependencies to various declaration files that will be
* loaded when a user imports the Starlight integration in their Astro configuration file. These
* directives must be first at the top of the file and can only be preceded by this comment.
*/
/// <reference path="./locals.d.ts" />
/// <reference path="./i18n.d.ts" />
/// <reference path="./virtual.d.ts" />
import mdx from '@astrojs/mdx';
import type { AstroIntegration, AstroIntegrationLogger } from 'astro';
import { AstroError } from 'astro/errors';
import { spawn } from 'node:child_process';
import { dirname, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { starlightAsides, starlightDirectivesRestorationIntegration } from './integrations/asides';
import { starlightExpressiveCode } from './integrations/expressive-code/index';
import { starlightSitemap } from './integrations/sitemap';
import { vitePluginStarlightUserConfig } from './integrations/virtual-user-config';
import { rehypeRtlCodeSupport } from './integrations/code-rtl-support';
import {
injectPluginTranslationsTypes,
runPlugins,
type PluginTranslations,
type StarlightUserConfigWithPlugins,
} from './utils/plugins';
import { processI18nConfig } from './utils/i18n';
import type { StarlightConfig } from './types';
import { starlightAutolinkHeadings } from './integrations/heading-links';
export default function StarlightIntegration(
userOpts: StarlightUserConfigWithPlugins
): AstroIntegration {
if (typeof userOpts !== 'object' || userOpts === null || Array.isArray(userOpts))
throw new AstroError(
'Invalid config passed to starlight integration',
`The Starlight integration expects a configuration object with at least a \`title\` property.\n\n` +
`See more details in the [Starlight configuration reference](https://starlight.astro.build/reference/configuration/)\n`
);
const { plugins, ...opts } = userOpts;
let userConfig: StarlightConfig;
let pluginTranslations: PluginTranslations = {};
return {
name: '@astrojs/starlight',
hooks: {
'astro:config:setup': async ({
addMiddleware,
command,
config,
injectRoute,
isRestart,
logger,
updateConfig,
}) => {
// Run plugins to get the updated configuration and any extra Astro integrations to load.
const pluginResult = await runPlugins(opts, plugins, {
command,
config,
isRestart,
logger,
});
// Process the Astro and Starlight configurations for i18n and translations.
const { astroI18nConfig, starlightConfig } = processI18nConfig(
pluginResult.starlightConfig,
config.i18n
);
const { integrations, useTranslations, absolutePathToLang } = pluginResult;
pluginTranslations = pluginResult.pluginTranslations;
userConfig = starlightConfig;
addMiddleware({ entrypoint: '@astrojs/starlight/locals', order: 'pre' });
if (!starlightConfig.disable404Route) {
injectRoute({
pattern: '404',
entrypoint: starlightConfig.prerender
? '@astrojs/starlight/routes/static/404.astro'
: '@astrojs/starlight/routes/ssr/404.astro',
prerender: starlightConfig.prerender,
});
}
injectRoute({
pattern: '[...slug]',
entrypoint: starlightConfig.prerender
? '@astrojs/starlight/routes/static/index.astro'
: '@astrojs/starlight/routes/ssr/index.astro',
prerender: starlightConfig.prerender,
});
// Add built-in integrations only if they are not already added by the user through the
// config or by a plugin.
const allIntegrations = [...config.integrations, ...integrations];
if (!allIntegrations.find(({ name }) => name === 'astro-expressive-code')) {
integrations.push(
...starlightExpressiveCode({ astroConfig: config, starlightConfig, useTranslations })
);
}
if (!allIntegrations.find(({ name }) => name === '@astrojs/sitemap')) {
integrations.push(starlightSitemap(starlightConfig));
}
if (!allIntegrations.find(({ name }) => name === '@astrojs/mdx')) {
integrations.push(mdx({ optimize: true }));
}
// Add Starlight directives restoration integration at the end of the list so that remark
// plugins injected by Starlight plugins through Astro integrations can handle text and
// leaf directives before they are transformed back to their original form.
integrations.push(starlightDirectivesRestorationIntegration());
// Add integrations immediately after Starlight in the config array.
// e.g. if a user has `integrations: [starlight(), tailwind()]`, then the order will be
// `[starlight(), expressiveCode(), sitemap(), mdx(), tailwind()]`.
// This ensures users can add integrations before/after Starlight and we respect that order.
const selfIndex = config.integrations.findIndex((i) => i.name === '@astrojs/starlight');
config.integrations.splice(selfIndex + 1, 0, ...integrations);
updateConfig({
vite: {
plugins: [
vitePluginStarlightUserConfig(command, starlightConfig, config, pluginTranslations),
],
},
markdown: {
remarkPlugins: [
...starlightAsides({
starlightConfig,
astroConfig: config,
useTranslations,
absolutePathToLang,
}),
],
rehypePlugins: [
rehypeRtlCodeSupport({ astroConfig: config }),
// Process headings and add anchor links.
...starlightAutolinkHeadings({
starlightConfig,
astroConfig: config,
useTranslations,
absolutePathToLang,
}),
],
},
scopedStyleStrategy: 'where',
// If not already configured, default to prefetching all links on hover.
prefetch: config.prefetch ?? { prefetchAll: true },
i18n: astroI18nConfig,
});
},
'astro:config:done': ({ injectTypes }) => {
injectPluginTranslationsTypes(pluginTranslations, injectTypes);
},
'astro:build:done': ({ dir, logger }) => {
if (!userConfig.pagefind) return;
const loglevelFlag = getPagefindLoggingFlags(logger.options.level);
const targetDir = fileURLToPath(dir);
const cwd = dirname(fileURLToPath(import.meta.url));
const relativeDir = relative(cwd, targetDir);
return new Promise<void>((resolve) => {
spawn('npx', ['-y', 'pagefind', ...loglevelFlag, '--site', relativeDir], {
stdio: 'inherit',
shell: true,
cwd,
}).on('close', () => resolve());
});
},
},
};
}
/** Map the logging level of Astro’s logger to one of Pagefind’s logging level flags. */
function getPagefindLoggingFlags(level: AstroIntegrationLogger['options']['level']) {
switch (level) {
case 'silent':
case 'error':
return ['--silent'];
case 'warn':
return ['--quiet'];
case 'debug':
return ['--verbose'];
case 'info':
default:
return [];
}
}