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 9 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
3 changes: 3 additions & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
import starlightSearchDemo from '@astrojs/starlight-search-demo';

export const locales = {
root: { label: 'English', lang: 'en' },
Expand All @@ -23,6 +24,8 @@ export default defineConfig({
integrations: [
starlight({
title: 'Starlight',
// TODO(HiDeoo) - Remove this once the search demo is no longer needed.
plugins: [starlightSearchDemo({ apiKey: '123456' })],
logo: {
light: '/src/assets/logo-light.svg',
dark: '/src/assets/logo-dark.svg',
Expand Down
1 change: 1 addition & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
},
"dependencies": {
"@astrojs/starlight": "workspace:*",
"@astrojs/starlight-search-demo": "workspace:*",
"@types/culori": "^2.0.0",
"astro": "^3.2.3",
"culori": "^3.2.0",
Expand Down
17 changes: 17 additions & 0 deletions docs/src/content/docs/reference/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -454,3 +454,20 @@ 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.
Starlight Plugins are an easy way to extend Starlight's functionality.
delucis marked this conversation as resolved.
Show resolved Hide resolved

Visit the [Starlight Plugins Showcase](/showcase/#community-plugins) to see a list of community plugins.
delucis marked this conversation as resolved.
Show resolved Hide resolved

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

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

Starlight Plugins provide an API to customize Starlight configuration, UI and behavior while also being easy to share and reuse.
This page is for anyone interested in creating their own Starlight plugins.
delucis marked this conversation as resolved.
Show resolved Hide resolved

Lean more about using a Starlight plugin in the [Configuration Reference](/reference/configuration/#plugins) or visit the [Starlight Plugins Showcase](/showcase/#community-plugins) to see a list of community plugins.
delucis marked this conversation as resolved.
Show resolved Hide resolved

## Quick API Reference

HiDeoo marked this conversation as resolved.
Show resolved Hide resolved
```ts
interface StarlightPlugin {
name: string;
plugin: (options: {
config: StarlightUserConfig;
updateConfig: (newConfig: StarlightUserConfig) => void;
addIntegration: (integration: AstroIntegration) => void;
logger: AstroIntegrationLogger;
}) => void | Promise<void>;
}
delucis marked this conversation as resolved.
Show resolved Hide resolved
```

## 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 configuration values that you want to update but no deep merge is performed.
delucis 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 {5-10}
// plugin.ts
export default {
name: 'add-twitter-plugin',
plugin({ config, updateConfig }) {
updateConfig({
social: {
...config.social,
twitter: 'astrodotbuild',
},
});
},
delucis 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 {7}
// plugin.ts
import react from '@astrojs/react';

export default {
name: 'plugin-using-react',
plugin({ addIntegration }) {
addIntegration(react());
},
};
```

### `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.
Note that all logged messages will be prefixed with the plugin name.
delucis marked this conversation as resolved.
Show resolved Hide resolved

```ts {5}
// plugin.ts
export default {
name: 'long-process-plugin',
plugin({ logger }) {
logger.info('Starting long process…');
// Some long process…
},
delucis marked this conversation as resolved.
Show resolved Hide resolved
};
```

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

```shell
[long-process-plugin] Starting long process…
```
10 changes: 8 additions & 2 deletions docs/src/content/docs/showcase.mdx
delucis marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: Discover sites built with Starlight and community tools that extend
---

:::tip[Add your own!]
Have you built a Starlight site or a tool for Starlight?
Have you built a Starlight site, plugin, or a tool for Starlight?
Open a PR adding a link to this page!
:::

Expand All @@ -20,9 +20,15 @@ See all the [public project repos using Starlight on GitHub](https://github.com/

## Community plugins

These community [plugins](/reference/configuration/#plugins) work alongside Starlight to extend its functionality.

// TODO(HiDeoo) This new structure is a proposal and should only be used once we have at least a plugin to showcase.

## Community tools and integrations

import { CardGrid, LinkCard } from '@astrojs/starlight/components';

These community tools, plugins, and integrations work alongside Starlight to extend its functionality.
These community tools and integrations can be used to add features to your Starlight site.

<CardGrid>
<LinkCard
Expand Down
68 changes: 68 additions & 0 deletions packages/starlight-search-demo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { StarlightPlugin } from '@astrojs/starlight/types';
import type { AstroUserConfig, ViteUserConfig } from 'astro';

/** The Starlight Search Demo plugin. */
export default function starlightSearchDemo(opts: StarlightSearchDemoConfig): StarlightPlugin {
return {
name: 'starlight-search-demo',
plugin({ addIntegration, config, logger, updateConfig }) {
// If the user has already has a custom override for the Search component, don't override it.
if (config.components?.Search) {
if (!opts.ignoreComponentOverridesWarning) {
logger.warn(
`It looks like you already have a \`Search\` component override. To render \`@astrojs/starlight-search-demo\`, you can either:
- Remove the existing override for the \`Search\` component from your configuration
- Import and render \`@astrojs/starlight-search-demo/overrides/Search.astro\` in your custom override\n`
);
}
} else {
// Otherwise, add the Search component override to the user's configuration.
updateConfig({
components: {
...config.components,
Search: '@astrojs/starlight-search-demo/overrides/Search.astro',
},
});
}

// Add a custom Astro integration that will inject a Vite plugin to expose the Starlight
// Search Demo config via virtual modules.
addIntegration({
name: 'starlight-search-demo-integration',
hooks: {
'astro:config:setup': ({ updateConfig }) => {
updateConfig({
vite: {
plugins: [vitePluginStarlightSearchDemo(opts)],
},
} satisfies AstroUserConfig);
},
},
});
},
};
}

/** Vite plugin that exposes the Starlight Search Demo config via virtual modules. */
function vitePluginStarlightSearchDemo(config: StarlightSearchDemoConfig): VitePlugin {
const moduleId = 'virtual:starlight-search-demo-config';
const resolvedModuleId = `\0${moduleId}`;
const moduleContent = `export default ${JSON.stringify(config)}`;

return {
name: 'vite-plugin-starlight-search-demo-config',
load(id) {
return id === resolvedModuleId ? moduleContent : undefined;
},
resolveId(id) {
return id === moduleId ? resolvedModuleId : undefined;
},
};
}

export interface StarlightSearchDemoConfig {
apiKey: string;
ignoreComponentOverridesWarning?: boolean;
}

type VitePlugin = NonNullable<ViteUserConfig['plugins']>[number];
23 changes: 23 additions & 0 deletions packages/starlight-search-demo/overrides/Search.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
import config from 'virtual:starlight-search-demo-config';
---

<input
autocapitalize="none"
enterkeyhint="search"
placeholder={`Search Demo with API Key '${config.apiKey}'`}
type="text"
/>

<style>
input {
background-color: crimson;
border: 2px solid darkred;
color: floralwhite;
width: 100%;
}

input::placeholder {
color: salmon;
}
</style>
23 changes: 23 additions & 0 deletions packages/starlight-search-demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@astrojs/starlight-search-demo",
"version": "0.0.1",
"private": true,
"description": "Demo Starlight Plugin adding a 3rd party search engine to Starlight",
"author": "Chris Swithinbank <swithinbank@gmail.com>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/withastro/starlight",
"directory": "packages/starlight-search-demo"
},
"bugs": "https://github.com/withastro/starlight/issues",
"homepage": "https://starlight.astro.build",
"type": "module",
"exports": {
".": "./index.ts",
"./overrides/Search.astro": "./overrides/Search.astro"
},
"peerDependencies": {
"@astrojs/starlight": ">=0.11.1"
}
}
5 changes: 5 additions & 0 deletions packages/starlight-search-demo/virtual.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
declare module 'virtual:starlight-search-demo-config' {
const StarlightSearchDemoConfig: import('./index').StarlightSearchDemoConfig;

export default StarlightSearchDemoConfig;
}
45 changes: 45 additions & 0 deletions packages/starlight/__tests__/plugins/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { expect, test } from 'vitest';
import config from 'virtual:starlight/user-config';
import { getSidebar } from '../../utils/navigation';
import { runPlugins } from '../../utils/plugins';
import { TestAstroIntegrationLogger } from '../test-config';

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' }]);
HiDeoo marked this conversation as resolved.
Show resolved Hide resolved
});

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('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();
});

test('validates plugins configuration before running them', async () => {
expect(
async () =>
await runPlugins(
{
title: 'Test Docs',
// @ts-expect-error - invalid integration with no `plugin` callback.
plugins: [{ name: 'plugin-with-invalid-integration' }],
},
new TestAstroIntegrationLogger()
)
).rejects.toThrowError(/Invalid plugins config/);
});
45 changes: 45 additions & 0 deletions packages/starlight/__tests__/plugins/integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { AstroIntegration } from 'astro';
import { expect, test } from 'vitest';
import { runPlugins } from '../../utils/plugins';
import { TestAstroIntegrationLogger } from '../test-config';

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',
plugins: [
{
name: 'test-plugin-1',
plugin({ addIntegration, updateConfig }) {
updateConfig({ description: 'test' });
addIntegration(integration1);
},
},
{
name: 'test-plugin-1',
plugin({ addIntegration }) {
addIntegration(integration1);
addIntegration(integration2);
},
},
],
},
new TestAstroIntegrationLogger()
);

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