Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/olive-weeks-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/starlight': patch
---

Prevents potential build issues with the Astro Cloudflare adapter due to the dependency on Node.js builtins.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// @ts-check
import starlight from '@astrojs/starlight';
import { defineConfig } from 'astro/config';
import { preventNodeBuiltinDependencyPlugin } from './src/noNodeModule';

export default defineConfig({
integrations: [
starlight({
title: 'No Node Builtins',
pagefind: false,
}),
],
vite: {
plugins: [preventNodeBuiltinDependencyPlugin()],
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@e2e/no-node-builtins",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/starlight": "workspace:*",
"astro": "^5.6.1"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineCollection } from 'astro:content';
import { docsLoader } from '@astrojs/starlight/loaders';
import { docsSchema } from '@astrojs/starlight/schema';

export const collections = {
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Home Page
---

Home page content
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { ViteUserConfig } from 'astro';
import { builtinModules } from 'node:module';

const nodeModules = builtinModules.map((name) => [name, `node:${name}`]).flat();

/**
* A Vite plugin used to verify that the final bundle does not have a hard dependency on Node.js
* builtins due to Starlight.
* This is to ensure that Starlight can run on platforms like Cloudflare.
*
* @see https://github.com/withastro/astro/blob/8491aa56e8685677af8458ff1c5a80d6461413f8/packages/astro/test/test-plugins.js
*/
export function preventNodeBuiltinDependencyPlugin(): NonNullable<
ViteUserConfig['plugins']
>[number] {
return {
name: 'verify-no-node-stuff',
generateBundle() {
nodeModules.forEach((name) => {
const importers = this.getModuleInfo(name)?.importers || [];
const starlightPath = new URL('../../../../', import.meta.url).pathname;
const nodeStarlightImport = importers.find((importer) =>
importer.startsWith(starlightPath)
);

if (nodeStarlightImport) {
throw new Error(
'A node builtin dependency imported by Starlight was found in the production bundle:\n\n' +
` - Node builtin: '${name}'\n` +
` - Importer: ${nodeStarlightImport}\n`
);
}
});
},
};
}
10 changes: 10 additions & 0 deletions packages/starlight/__e2e__/no-node-builtins.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { expect, testFactory } from './test-utils';

const test = testFactory('./fixtures/no-node-builtins/');

test('builds without any dependencies on Node.js builtins', async ({ page, getProdServer }) => {
const starlight = await getProdServer();
await starlight.goto('/');

await expect(page.getByText('Home page content')).toBeVisible();
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createTranslationSystemFromFs } from '../../utils/translations-fs';
import { StarlightConfigSchema, type StarlightUserConfig } from '../../utils/user-config';
import { absolutePathToLang as getAbsolutePathFromLang } from '../../integrations/shared/absolutePathToLang';
import { starlightAutolinkHeadings } from '../../integrations/heading-links';
import { getCollectionPosixPath } from '../../utils/collection-fs';

const starlightConfig = StarlightConfigSchema.parse({
title: 'Anchor Links Tests',
Expand All @@ -23,7 +24,10 @@ const useTranslations = createTranslationSystemFromFs(
);

function absolutePathToLang(path: string) {
return getAbsolutePathFromLang(path, { astroConfig, starlightConfig });
return getAbsolutePathFromLang(path, {
docsPath: getCollectionPosixPath('docs', astroConfig.srcDir),
starlightConfig,
});
}

const processor = await createMarkdownProcessor({
Expand Down
6 changes: 5 additions & 1 deletion packages/starlight/__tests__/remark-rehype/asides.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { createTranslationSystemFromFs } from '../../utils/translations-fs';
import { StarlightConfigSchema, type StarlightUserConfig } from '../../utils/user-config';
import { BuiltInDefaultLocale } from '../../utils/i18n';
import { absolutePathToLang as getAbsolutePathFromLang } from '../../integrations/shared/absolutePathToLang';
import { getCollectionPosixPath } from '../../utils/collection-fs';

const starlightConfig = StarlightConfigSchema.parse({
title: 'Asides Tests',
Expand All @@ -26,7 +27,10 @@ const useTranslations = createTranslationSystemFromFs(
);

function absolutePathToLang(path: string) {
return getAbsolutePathFromLang(path, { astroConfig, starlightConfig });
return getAbsolutePathFromLang(path, {
docsPath: getCollectionPosixPath('docs', astroConfig.srcDir),
starlightConfig,
});
}

const processor = await createMarkdownProcessor({
Expand Down
4 changes: 3 additions & 1 deletion packages/starlight/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ export default function StarlightIntegration(
// config or by a plugin.
const allIntegrations = [...config.integrations, ...integrations];
if (!allIntegrations.find(({ name }) => name === 'astro-expressive-code')) {
integrations.push(...starlightExpressiveCode({ starlightConfig, useTranslations }));
integrations.push(
...starlightExpressiveCode({ astroConfig: config, starlightConfig, useTranslations })
);
}
if (!allIntegrations.find(({ name }) => name === '@astrojs/sitemap')) {
integrations.push(starlightSitemap(starlightConfig));
Expand Down
9 changes: 9 additions & 0 deletions packages/starlight/integrations/asides-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { AstroError } from 'astro/errors';

export function throwInvalidAsideIconError(icon: string) {
throw new AstroError(
'Invalid aside icon',
`An aside custom icon must be set to the name of one of Starlight\’s built-in icons, but received \`${icon}\`.\n\n` +
'See https://starlight.astro.build/reference/icons/#all-icons for a list of available icons.'
);
}
10 changes: 1 addition & 9 deletions packages/starlight/integrations/asides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { getRemarkRehypeDocsCollectionPath, shouldTransformFile } from './remark
import { Icons } from '../components/Icons';
import { fromHtml } from 'hast-util-from-html';
import type { Element } from 'hast';
import { AstroError } from 'astro/errors';
import { throwInvalidAsideIconError } from './asides-error';

interface AsidesOptions {
starlightConfig: Pick<StarlightConfig, 'defaultLocale' | 'locales'>;
Expand Down Expand Up @@ -253,14 +253,6 @@ function remarkAsides(options: AsidesOptions): Plugin<[], Root> {

type RemarkPlugins = NonNullable<NonNullable<AstroUserConfig['markdown']>['remarkPlugins']>;

export function throwInvalidAsideIconError(icon: string) {
throw new AstroError(
'Invalid aside icon',
`An aside custom icon must be set to the name of one of Starlight\’s built-in icons, but received \`${icon}\`.\n\n` +
'See https://starlight.astro.build/reference/icons/#all-icons for a list of available icons.'
);
}

export function starlightAsides(options: AsidesOptions): RemarkPlugins {
return [remarkDirective, remarkAsides(options)];
}
Expand Down
132 changes: 12 additions & 120 deletions packages/starlight/integrations/expressive-code/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
import {
astroExpressiveCode,
type AstroExpressiveCodeOptions,
type CustomConfigPreprocessors,
} from 'astro-expressive-code';
import { addClassName } from 'astro-expressive-code/hast';
import type { AstroIntegration } from 'astro';
import { astroExpressiveCode, type AstroExpressiveCodeOptions } from 'astro-expressive-code';
import type { AstroConfig, AstroIntegration } from 'astro';
import type { HookParameters, StarlightConfig } from '../../types';
import { absolutePathToLang } from '../shared/absolutePathToLang';
import { slugToLocale } from '../shared/slugToLocale';
import { localeToLang } from '../shared/localeToLang';
import {
applyStarlightUiThemeColors,
preprocessThemes,
type ThemeObjectOrBundledThemeName,
} from './theming';
import { addTranslations } from './translations';
import { getStarlightEcConfigPreprocessor } from './preprocessor';
import { type ThemeObjectOrBundledThemeName } from './theming';
import { getCollectionPosixPath } from '../../utils/collection-fs';

export type StarlightExpressiveCodeOptions = Omit<AstroExpressiveCodeOptions, 'themes'> & {
/**
Expand Down Expand Up @@ -63,114 +52,13 @@ export type StarlightExpressiveCodeOptions = Omit<AstroExpressiveCodeOptions, 't
};

type StarlightEcIntegrationOptions = {
astroConfig: AstroConfig;
starlightConfig: StarlightConfig;
useTranslations: HookParameters<'config:setup'>['useTranslations'];
};

/**
* Create an Expressive Code configuration preprocessor based on Starlight config.
* Used internally to set up Expressive Code and by the `<Code>` component.
*/
export function getStarlightEcConfigPreprocessor({
starlightConfig,
useTranslations,
}: StarlightEcIntegrationOptions): CustomConfigPreprocessors['preprocessAstroIntegrationConfig'] {
return (input): AstroExpressiveCodeOptions => {
const astroConfig = input.astroConfig;
const ecConfig = input.ecConfig as StarlightExpressiveCodeOptions;

const {
themes: themesInput,
cascadeLayer,
customizeTheme,
styleOverrides: { textMarkers: textMarkersStyleOverrides, ...otherStyleOverrides } = {},
useStarlightDarkModeSwitch,
useStarlightUiThemeColors = ecConfig.themes === undefined,
plugins = [],
...rest
} = ecConfig;

// Handle the `themes` option
const themes = preprocessThemes(themesInput);
if (useStarlightUiThemeColors === true && themes.length < 2) {
console.warn(
`*** Warning: Using the config option "useStarlightUiThemeColors: true" ` +
`with a single theme is not recommended. For better color contrast, ` +
`please provide at least one dark and one light theme.\n`
);
}

// Add the `not-content` class to all rendered blocks to prevent them from being affected
// by Starlight's default content styles
plugins.push({
name: 'Starlight Plugin',
hooks: {
postprocessRenderedBlock: ({ renderData }) => {
addClassName(renderData.blockAst, 'not-content');
},
},
});

// Add Expressive Code UI translations for all defined locales
addTranslations(starlightConfig, useTranslations);

return {
themes,
customizeTheme: (theme) => {
if (useStarlightUiThemeColors) {
applyStarlightUiThemeColors(theme);
}
if (customizeTheme) {
theme = customizeTheme(theme) ?? theme;
}
return theme;
},
defaultLocale: starlightConfig.defaultLocale?.lang ?? starlightConfig.defaultLocale?.locale,
themeCssSelector: (theme, { styleVariants }) => {
// If one dark and one light theme are available, and the user has not disabled it,
// generate theme CSS selectors compatible with Starlight's dark mode switch
if (useStarlightDarkModeSwitch !== false && styleVariants.length >= 2) {
const baseTheme = styleVariants[0]?.theme;
const altTheme = styleVariants.find((v) => v.theme.type !== baseTheme?.type)?.theme;
if (theme === baseTheme || theme === altTheme) return `[data-theme='${theme.type}']`;
}
// Return the default selector
return `[data-theme='${theme.name}']`;
},
cascadeLayer: cascadeLayer ?? 'starlight.components',
styleOverrides: {
borderRadius: '0px',
borderWidth: '1px',
codePaddingBlock: '0.75rem',
codePaddingInline: '1rem',
codeFontFamily: 'var(--__sl-font-mono)',
codeFontSize: 'var(--sl-text-code)',
codeLineHeight: 'var(--sl-line-height)',
uiFontFamily: 'var(--__sl-font)',
textMarkers: {
lineDiffIndicatorMarginLeft: '0.25rem',
defaultChroma: '45',
backgroundOpacity: '60%',
...textMarkersStyleOverrides,
},
...otherStyleOverrides,
},
getBlockLocale: ({ file }) => {
if (file.url) {
const locale = slugToLocale(file.url.pathname.slice(1), starlightConfig);
return localeToLang(starlightConfig, locale);
}
// Note that EC cannot use the `absolutePathToLang` helper passed down to plugins as this callback
// is also called in the context of the `<Code>` component.
return absolutePathToLang(file.path, { starlightConfig, astroConfig });
},
plugins,
...rest,
};
};
}

export const starlightExpressiveCode = ({
astroConfig,
starlightConfig,
useTranslations,
}: StarlightEcIntegrationOptions): AstroIntegration[] => {
Expand Down Expand Up @@ -213,19 +101,23 @@ export const starlightExpressiveCode = ({
typeof starlightConfig.expressiveCode === 'object'
? (starlightConfig.expressiveCode as AstroExpressiveCodeOptions)
: {};

let docsPath = getCollectionPosixPath('docs', astroConfig.srcDir);

return [
astroExpressiveCode({
...configArgs,
customConfigPreprocessors: {
preprocessAstroIntegrationConfig: getStarlightEcConfigPreprocessor({
docsPath,
starlightConfig,
useTranslations,
}),
preprocessComponentConfig: `
import starlightConfig from 'virtual:starlight/user-config'
import { useTranslations, getStarlightEcConfigPreprocessor } from '@astrojs/starlight/internal'

export default getStarlightEcConfigPreprocessor({ starlightConfig, useTranslations })
export default getStarlightEcConfigPreprocessor({ docsPath: ${JSON.stringify(docsPath)}, starlightConfig, useTranslations })
`,
},
}),
Expand Down
Loading