Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial Starlight Plugins support #942

Merged
merged 32 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6d38e2a
feat: add plugins mechanism with `config` and `updateConfig` helpers
HiDeoo Oct 20, 2023
98de654
feat: add plugin `logger`
HiDeoo Oct 20, 2023
99de570
test: ensure zod transform are not applied to plugin supplied config
HiDeoo Oct 20, 2023
1d5526b
feat: add plugin `addIntegration` helper
HiDeoo Oct 21, 2023
119d95b
feat: expose `StarlightPlugin` type
HiDeoo Oct 21, 2023
fe26abb
test: ensure plugins are not exposed to config virtual module
HiDeoo Oct 21, 2023
b845aee
feat: add search demo plugin
HiDeoo Oct 21, 2023
652b37a
docs: add plugin documentation
HiDeoo Oct 22, 2023
ba26df7
test: add plugin validation test
HiDeoo Oct 22, 2023
6da9b51
Merge branch 'main' into hd-plugins
HiDeoo Nov 4, 2023
68af963
refactor: use astro integration shape for plugins
HiDeoo Nov 4, 2023
2e23c7f
refactor: plugin validation process
HiDeoo Nov 5, 2023
1c98628
feat: pass down user configured plugins list to plugins
HiDeoo Nov 5, 2023
887f1dd
feat: pass down `astroConfig`, `command` and `isRestart` to plugins
HiDeoo Nov 5, 2023
62ce7f7
chore: fix demo color contrast
HiDeoo Nov 5, 2023
d348e3c
chore: add preact to the demo
HiDeoo Nov 5, 2023
340085b
Merge branch 'main' into pr/942
delucis Nov 22, 2023
dfb585e
test: fix failing test
HiDeoo Nov 23, 2023
6f3d844
fix: built-in integration check
HiDeoo Nov 23, 2023
4281c36
feat: pass down plugin configured integrations list with astro config
HiDeoo Nov 23, 2023
860020f
test: add test-plugin-utils
HiDeoo Nov 24, 2023
1150659
docs: apply chris suggestions
HiDeoo Nov 24, 2023
eab3c95
docs: fix plugin shape
HiDeoo Nov 24, 2023
34c675a
Merge branch 'main' into hd-plugins
HiDeoo Nov 28, 2023
e7b9d99
chore: remove demo plugin
HiDeoo Nov 28, 2023
d3ef311
docs: revert showcase.mdx
HiDeoo Nov 28, 2023
0682f56
docs: remove plugin showcase references
HiDeoo Nov 28, 2023
ef0954e
chore: add changeset
HiDeoo Nov 28, 2023
1066c6f
docs: apply chris suggestions
HiDeoo Nov 29, 2023
5594ec3
docs: remove innacurate statement
HiDeoo Nov 29, 2023
c7eae7c
Merge branch 'main' into pr/942
delucis Nov 29, 2023
1016921
Fix user config access in `done` hook
delucis Nov 29, 2023
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
7 changes: 7 additions & 0 deletions .changeset/lovely-keys-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astrojs/starlight': minor
---

Add support for Starlight plugins.

Check the [plugins](https://starlight.astro.build/reference/plugins/) reference page to learn more regarding creating plugins.
HiDeoo marked this conversation as resolved.
Show resolved Hide resolved
15 changes: 15 additions & 0 deletions docs/src/content/docs/reference/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -519,3 +519,18 @@ starlight({
```

See the [Overrides Reference](/reference/overrides/) for details of all the components that you can override.

### `plugins`

**type:** [`StarlightPlugin[]`](/reference/plugins/#quick-api-reference)

Extend Starlight with custom plugins.
Plugins apply changes to your project to modify or add to Starlight's features.

```js
starlight({
plugins: [starlightPlugin()],
});
```

See the [Plugins Reference](/reference/plugins/) for details about creating your own plugins.
158 changes: 158 additions & 0 deletions docs/src/content/docs/reference/plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
---
title: Plugins Reference
description: An overview of the Starlight plugin API.
tableOfContents:
maxHeadingLevel: 4
---

Starlight plugins can customize Starlight configuration, UI, and behavior, while also being easy to share and reuse.
This reference page documents the API that plugins have access to.

Lean more about using a Starlight plugin in the [Configuration Reference](/reference/configuration/#plugins).

## Quick API Reference

HiDeoo marked this conversation as resolved.
Show resolved Hide resolved
```ts
interface StarlightPlugin {
name: string;
hooks: {
setup: (options: {
config: StarlightUserConfig;
updateConfig: (newConfig: StarlightUserConfig) => void;
addIntegration: (integration: AstroIntegration) => void;
astroConfig: AstroConfig;
command: 'dev' | 'build' | 'preview';
isRestart: boolean;
logger: AstroIntegrationLogger;
}) => void | Promise<void>;
};
}
```

## `name`

**type:** `string`

A plugin must provide a unique name that describes it. The name is used when [logging messages](#logger) related to this plugin and may be used by other plugins to detect the presence of this plugin.

## `hooks`

Hooks are functions which Starlight calls to run plugin code at specific times. Currently, Starlight supports a single `setup` hook.

### `hooks.setup`

Plugin setup function called when Starlight is initialized (during the [`astro:config:setup`](https://docs.astro.build/en/reference/integrations-reference/#astroconfigsetup) integration hook).
The `setup` hook can be used to update the Starlight configuration or add Astro integrations.

This hook is called with the following options:

#### `config`

**type:** `StarlightUserConfig`

A read-only copy of the user-supplied [Starlight configuration](/reference/configuration).
This configuration may have been updated by other plugins configured before the current one.

#### `updateConfig`

**type:** `(newConfig: StarlightUserConfig) => void`

A callback function to update the user-supplied [Starlight configuration](/reference/configuration).
You only need to provide the root-level configuration keys that you want to update but no deep merge is performed. In order to update nested configuration values, you must provide the entire nested object.
HiDeoo marked this conversation as resolved.
Show resolved Hide resolved

For example, to add a new [`social`](/reference/configuration/#social) media account to the configuration without overriding the existing ones:
HiDeoo marked this conversation as resolved.
Show resolved Hide resolved

```ts {6-11}
// plugin.ts
export default {
name: 'add-twitter-plugin',
hooks: {
setup({ config, updateConfig }) {
updateConfig({
social: {
...config.social,
twitter: 'astrodotbuild',
HiDeoo marked this conversation as resolved.
Show resolved Hide resolved
},
});
},
},
};
```

#### `addIntegration`

**type:** `(integration: AstroIntegration) => void`

A callback function to add an [Astro integration](https://docs.astro.build/en/reference/integrations-reference/) required by the plugin.
For example, to add the [React Astro integration](https://docs.astro.build/en/guides/integrations-guide/react/):
HiDeoo marked this conversation as resolved.
Show resolved Hide resolved

```ts {14}
HiDeoo marked this conversation as resolved.
Show resolved Hide resolved
// plugin.ts
import react from '@astrojs/react';

export default {
name: 'plugin-using-react',
hooks: {
plugin({ addIntegration, astroConfig }) {
const isReactLoaded = astroConfig.integrations.find(
({ name }) => name === '@astrojs/react'
);

// Only add the React integration if it's not already loaded.
if (!isReactLoaded) {
addIntegration(react());
}
},
},
};
```

#### `astroConfig`

**type:** `AstroConfig`

A read-only copy of the user-supplied [Astro configuration](https://docs.astro.build/en/reference/configuration-reference/).
This configuration is resolved before any other integrations have run.
HiDeoo marked this conversation as resolved.
Show resolved Hide resolved

#### `command`

**type:** `'dev' | 'build' | 'preview'`

The command used to run Starlight:

- `dev` - Project is executed with `astro dev`
- `build` - Project is executed with `astro build`
- `preview` - Project is executed with `astro preview`

#### `isRestart`

**type:** `boolean`

`false` when the dev server starts, `true` when a reload is triggered.
delucis marked this conversation as resolved.
Show resolved Hide resolved
Common reasons for a restart include a user editing their `astro.config.mjs` while the dev server is running.

#### `logger`

**type:** `AstroIntegrationLogger`

An instance of the [Astro integration logger](https://docs.astro.build/en/reference/integrations-reference/#astrointegrationlogger) that you can use to write logs.
All logged messages will be prefixed with the plugin name.

```ts {6}
// plugin.ts
export default {
name: 'long-process-plugin',
hooks: {
plugin({ logger }) {
logger.info('Starting long process…');
// Some long process…
},
},
};
```

The example above will log a message that includes the provided info message:

```shell
[long-process-plugin] Starting long process…
```
126 changes: 126 additions & 0 deletions packages/starlight/__tests__/plugins/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { describe, expect, test } from 'vitest';
import config from 'virtual:starlight/user-config';
import { getSidebar } from '../../utils/navigation';
import { runPlugins } from '../../utils/plugins';
import { createTestPluginContext } from '../test-plugin-utils';

test('reads and updates a configuration option', () => {
expect(config.title).toBe('Plugins - Custom');
});

test('overwrites a configuration option', () => {
expect(getSidebar('/', undefined)).toMatchObject([{ href: '/showcase', label: 'Showcase' }]);
});

test('runs plugins in the order that they are configured and always passes down the latest user config', () => {
expect(config.description).toBe('plugin 1 - plugin 2 - plugin 3');
});

test('receives the user provided configuration without any Zod `transform`s applied', () => {
/**
* If the `transform` associated to the favicon schema was applied, the favicon `href` would be
* `invalid.svg`.
* @see {@link file://./vitest.config.ts} for more details in the `test-plugin-1` plugin.
*/
expect(config.favicon.href).toBe('valid.svg');
});

test('receives the user provided configuration including the plugins list', async () => {
expect.assertions(1);

await runPlugins(
{ title: 'Test Docs' },
[
{ name: 'test-plugin-1', hooks: { setup: () => {} } },
{ name: 'test-plugin-2', hooks: { setup: () => {} } },
{
name: 'test-plugin-3',
hooks: {
setup: ({ config }) => {
expect(config.plugins?.map(({ name }) => name)).toMatchObject([
'test-plugin-1',
'test-plugin-2',
'test-plugin-3',
]);
},
},
},
],
createTestPluginContext()
);
});

describe('validation', () => {
test('validates starlight configuration before running plugins', async () => {
expect(
async () =>
await runPlugins(
// @ts-expect-error - invalid sidebar config.
{ title: 'Test Docs', sidebar: true },
[],
createTestPluginContext()
)
).rejects.toThrowError(/Invalid config passed to starlight integration/);
});

test('validates plugins configuration before running them', async () => {
expect(
async () =>
await runPlugins(
{ title: 'Test Docs' },
// @ts-expect-error - invalid plugin with no `hooks` defined.
[{ name: 'invalid-plugin' }],
createTestPluginContext()
)
).rejects.toThrowError(/Invalid plugins config passed to starlight integration/);
});

test('validates configuration updates from plugins do not update the `plugins` config key', async () => {
expect(
async () =>
await runPlugins(
{ title: 'Test Docs' },
[
{
name: 'test-plugin',
hooks: {
setup: ({ updateConfig }) => {
// @ts-expect-error - plugins cannot update the `plugins` config key.
updateConfig({ plugins: [{ name: 'invalid-plugin' }] });
},
},
},
],
createTestPluginContext()
)
).rejects.toThrowError(
/The 'test-plugin' plugin tried to update the 'plugins' config key which is not supported./
);
});

test('validates configuration updates from plugins', async () => {
expect(
async () =>
await runPlugins(
{ title: 'Test Docs' },
[
{
name: 'test-plugin',
hooks: {
setup: ({ updateConfig }) => {
// @ts-expect-error - invalid sidebar config update.
updateConfig({ description: true });
},
},
},
],
createTestPluginContext()
)
).rejects.toThrowError(/Invalid config update provided by the 'test-plugin' plugin/);
});
});

test('does not expose plugins to the config virtual module', () => {
// @ts-expect-error - plugins are not serializable and thus not in the config virtual module.
expect(config.plugins).not.toBeDefined();
});
77 changes: 77 additions & 0 deletions packages/starlight/__tests__/plugins/integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { AstroIntegration } from 'astro';
import { expect, test } from 'vitest';
import { runPlugins } from '../../utils/plugins';
import { createTestPluginContext } from '../test-plugin-utils';

test('returns all integrations added by plugins without deduping them', async () => {
const integration1: AstroIntegration = {
name: 'test-integration-1',
hooks: {},
};

const integration2: AstroIntegration = {
name: 'test-integration-2',
hooks: {},
};

const { integrations } = await runPlugins(
{ title: 'Test Docs' },
[
{
name: 'test-plugin-1',
hooks: {
setup({ addIntegration, updateConfig }) {
updateConfig({ description: 'test' });
addIntegration(integration1);
},
},
},
{
name: 'test-plugin-2',
hooks: {
setup({ addIntegration }) {
addIntegration(integration1);
addIntegration(integration2);
},
},
},
],
createTestPluginContext()
);

expect(integrations).toMatchObject([
{ name: 'test-integration-1' },
{ name: 'test-integration-1' },
{ name: 'test-integration-2' },
]);
});

test('receives the Astro config with a list of integrations including the ones added by previous plugins', async () => {
expect.assertions(1);

await runPlugins(
{ title: 'Test Docs' },
[
{
name: 'test-plugin-1',
hooks: {
setup({ addIntegration }) {
addIntegration({
name: 'test-integration',
hooks: {},
});
},
},
},
{
name: 'test-plugin-2',
hooks: {
setup({ astroConfig }) {
expect(astroConfig.integrations).toMatchObject([{ name: 'test-integration' }]);
},
},
},
],
createTestPluginContext()
);
});
Loading