Skip to content

Commit 239698c

Browse files
HiDeoodelucistrueberryless
authored
Ensure CSS layer order in custom pages (#3351)
Co-authored-by: delucis <357379+delucis@users.noreply.github.com> Co-authored-by: Felix Schneider <99918022+trueberryless@users.noreply.github.com> Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
1 parent 28810f0 commit 239698c

File tree

9 files changed

+132
-7
lines changed

9 files changed

+132
-7
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@astrojs/starlight': minor
3+
---
4+
5+
Ensures that Starlight CSS layer order is predictable in custom pages using the `<StarlightPage>` component.
6+
7+
Previously, due to how [import order](https://docs.astro.build/en/guides/styling/#import-order) works in Astro, the `<StarlightPage>` component had to be the first import in custom pages to set up [cascade layers](https://starlight.astro.build/guides/css-and-tailwind/#cascade-layers) used internally by Starlight to manage the order of its styles.
8+
9+
With this change, this restriction no longer applies and Starlight’s styles will be applied correctly regardless of the import order of the `<StarlightPage>` component.

docs/src/content/docs/guides/pages.mdx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,13 @@ Read more in the [“Pages” guide in the Astro docs](https://docs.astro.build/
7777

7878
To use the Starlight layout in custom pages, wrap your page content with the [`<StarlightPage>` component](#starlightpage-component).
7979
This can be helpful if you are generating content dynamically but still want to use Starlight’s design.
80-
This component must be the first import in your file to set up [cascade layers](/guides/css-and-tailwind/#cascade-layers) and ensure a predictable CSS order.
8180

8281
To add anchor links to headings that match Starlight’s Markdown anchor link styles, you can use the [`<AnchorHeading>` component](#anchorheading-component) in your custom pages.
8382

8483
```astro
8584
---
8685
// src/pages/custom-page/example.astro
87-
// Import the `<StarlightPage>` component first to set up cascade layers.
8886
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
89-
90-
// Import any other components you want to use in your custom page.
9187
import AnchorHeading from '@astrojs/starlight/components/AnchorHeading.astro';
9288
import CustomComponent from './CustomComponent.astro';
9389
---
@@ -109,7 +105,6 @@ The `<StarlightPage />` component renders a full page of content using Starlight
109105

110106
```astro
111107
---
112-
// Import the `<StarlightPage>` component first to set up cascade layers.
113108
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
114109
---
115110
@@ -118,8 +113,6 @@ import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
118113
</StarlightPage>
119114
```
120115

121-
Due to how [import order](https://docs.astro.build/en/guides/styling/#import-order) works in Astro, the `<StarlightPage />` component must be the first import in your file to set up [cascade layers](/guides/css-and-tailwind/#cascade-layers) used internally by Starlight to manage the order of its styles.
122-
123116
The `<StarlightPage />` component accepts the following props.
124117

125118
##### `frontmatter`

packages/starlight/__e2e__/basics.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import fs from 'node:fs/promises';
12
import { expect, testFactory, type Locator } from './test-utils';
23

34
const test = testFactory('./fixtures/basics/');
@@ -467,6 +468,31 @@ test.describe('components', () => {
467468
});
468469
});
469470

471+
test.describe('css layer order', () => {
472+
test('ensures that the StarlightPage component is always imported first to ensure a predictable CSS layer order in custom pages', async ({
473+
page,
474+
makeServer,
475+
}) => {
476+
const starlight = await makeServer('dev', { mode: 'dev' });
477+
await starlight.goto('/starlight-page-css-layer-order');
478+
479+
const firstStyleContent = await page.evaluate(
480+
() => document.head.querySelector('style')?.textContent ?? ''
481+
);
482+
483+
const expectedLayersOrder = await fs.readFile(
484+
new URL('../style/layers.css', import.meta.url),
485+
'utf-8'
486+
);
487+
488+
// Ensure that the first style block in the head contains the expected layers order rather
489+
// the styles of the link button wrapped in a `@layer` block at-rule automatically declaring
490+
// a new layer and thus potentially breaking the intended layers order as the initial order
491+
// in which layers are declared indicates which layer has precedence.
492+
expect(firstStyleContent).toBe(expectedLayersOrder);
493+
});
494+
});
495+
470496
async function expectSelectedTab(tabs: Locator, label: string, panel?: string) {
471497
expect(
472498
(
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
import { LinkButton } from '@astrojs/starlight/components';
3+
4+
import AnchorHeading from '@astrojs/starlight/components/AnchorHeading.astro';
5+
6+
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
7+
8+
/**
9+
* This page is used to test the CSS layer order in custom pages using the StarlightPage> component
10+
* where we cannot ensure a correct import order of CSS.
11+
* Note that the order of imports in this file is important and should not be changed.
12+
*/
13+
---
14+
15+
<StarlightPage frontmatter={{ title: 'A custom page' }}>
16+
<AnchorHeading level="2" id="a-sub-heading">A Sub heading</AnchorHeading>
17+
18+
<p>Custom page content and a <a href="/tabs">link</a> to another page.</p>
19+
20+
<p>
21+
<LinkButton href="/tabs">Tabs link button</LinkButton>
22+
</p>
23+
</StarlightPage>

packages/starlight/__tests__/test-config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getViteConfig } from 'astro/config';
55
import { vitePluginStarlightUserConfig } from '../integrations/virtual-user-config';
66
import { runPlugins, type StarlightUserConfigWithPlugins } from '../utils/plugins';
77
import { createTestPluginContext } from './test-plugin-utils';
8+
import { vitePluginStarlightCssLayerOrder } from '../integrations/vite-layer-order';
89

910
const testLegacyCollections = process.env.LEGACY_COLLECTIONS === 'true';
1011

@@ -29,6 +30,7 @@ export async function defineVitestConfig(
2930
);
3031
return getViteConfig({
3132
plugins: [
33+
vitePluginStarlightCssLayerOrder(),
3234
vitePluginStarlightUserConfig(
3335
command,
3436
starlightConfig,

packages/starlight/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { fileURLToPath } from 'node:url';
1616
import { starlightAsides, starlightDirectivesRestorationIntegration } from './integrations/asides';
1717
import { starlightExpressiveCode } from './integrations/expressive-code/index';
1818
import { starlightSitemap } from './integrations/sitemap';
19+
import { vitePluginStarlightCssLayerOrder } from './integrations/vite-layer-order';
1920
import { vitePluginStarlightUserConfig } from './integrations/virtual-user-config';
2021
import { rehypeRtlCodeSupport } from './integrations/code-rtl-support';
2122
import {
@@ -118,6 +119,7 @@ export default function StarlightIntegration(
118119
updateConfig({
119120
vite: {
120121
plugins: [
122+
vitePluginStarlightCssLayerOrder(),
121123
vitePluginStarlightUserConfig(command, starlightConfig, config, pluginTranslations),
122124
],
123125
},
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { ViteUserConfig } from 'astro';
2+
import MagicString from 'magic-string';
3+
4+
const starlightPageImportSource = '@astrojs/starlight/components/StarlightPage.astro';
5+
6+
/**
7+
* Vite plugin that ensures the StarlightPage component is always imported first when imported in
8+
* an Astro file.
9+
*
10+
* This is necessary to ensure a predictable CSS layer order which is defined by the `<Page />`
11+
* imported by the `<StarlightPage />` component. If a user imports any other component using
12+
* cascade layers before the `<StarlightPage />` component, it will result in undesired layers
13+
* being created before we explicitly set the expected layer order.
14+
*/
15+
export function vitePluginStarlightCssLayerOrder(): VitePlugin {
16+
return {
17+
name: 'vite-plugin-starlight-css-layer-order',
18+
enforce: 'pre',
19+
transform(code, id) {
20+
if (
21+
!id.endsWith('.astro') ||
22+
id.endsWith(starlightPageImportSource) ||
23+
code.indexOf('StarlightPage.astro') === -1
24+
) {
25+
return;
26+
}
27+
28+
let ast: ReturnType<typeof this.parse>;
29+
30+
try {
31+
ast = this.parse(code);
32+
} catch {
33+
return;
34+
}
35+
36+
let hasStarlightPageImport = false;
37+
38+
for (const node of ast.body) {
39+
if (node.type !== 'ImportDeclaration') continue;
40+
if (node.source.value !== starlightPageImportSource) continue;
41+
42+
const importDefaultSpecifier = node.specifiers.find(
43+
(specifier) => specifier.type === 'ImportDefaultSpecifier'
44+
);
45+
if (!importDefaultSpecifier) continue;
46+
47+
hasStarlightPageImport = true;
48+
break;
49+
}
50+
51+
if (!hasStarlightPageImport) return;
52+
53+
// Format path to unix style path.
54+
const filename = id.replace(/\\/g, '/');
55+
const ms = new MagicString(code, { filename });
56+
ms.prepend(`import "${starlightPageImportSource}";\n`);
57+
58+
return {
59+
code: ms.toString(),
60+
map: ms.generateMap({ hires: 'boundary' }),
61+
};
62+
},
63+
};
64+
}
65+
66+
type VitePlugin = NonNullable<ViteUserConfig['plugins']>[number];

packages/starlight/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@
212212
"i18next": "^23.11.5",
213213
"js-yaml": "^4.1.0",
214214
"klona": "^2.0.6",
215+
"magic-string": "^0.30.17",
215216
"mdast-util-directive": "^3.0.0",
216217
"mdast-util-to-markdown": "^2.1.0",
217218
"mdast-util-to-string": "^4.0.0",

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)