summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuiz Ferraz2024-09-06 19:38:30 -0300
committerGitHub2024-09-06 19:38:30 -0300
commit6f3202b3eb747de8a1cfcba001ab618d5fdee44a (patch)
treeca4988028b6a7b8fa5ec4f30e0197b4db199c37e
parent20cbf3b6a4d1598a62fdb176ebaa849bc7b978f7 (diff)
downloadIT.starlight-6f3202b3eb747de8a1cfcba001ab618d5fdee44a.tar.gz
IT.starlight-6f3202b3eb747de8a1cfcba001ab618d5fdee44a.tar.bz2
IT.starlight-6f3202b3eb747de8a1cfcba001ab618d5fdee44a.zip
Add support for SSR (#1255)
Co-authored-by: Patrick Jakubik <37963339+patryk-smc@users.noreply.github.com> Co-authored-by: Roni Costa <622159+ronildo@users.noreply.github.com> Co-authored-by: Ryan Russell <523300+ryanrussell@users.noreply.github.com> Co-authored-by: HiDeoo <494699+HiDeoo@users.noreply.github.com> Co-authored-by: Murali Manohar Varma <67296473+mmv-dev@users.noreply.github.com> Co-authored-by: Marcelo Cardoso <33183880+marcelovicentegc@users.noreply.github.com> Co-authored-by: Aaron Siddhartha Mondal <28633256+aaronmondal@users.noreply.github.com> Co-authored-by: Chris Swithinbank <357379+delucis@users.noreply.github.com> Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com>
-rw-r--r--.changeset/ninety-singers-film.md7
-rw-r--r--.changeset/quiet-penguins-wonder.md5
-rw-r--r--.changeset/six-phones-boil.md16
-rw-r--r--docs/src/content/docs/manual-setup.mdx4
-rw-r--r--docs/src/content/docs/reference/configuration.mdx12
-rw-r--r--packages/starlight/.gitignore4
-rw-r--r--packages/starlight/__e2e__/collection-config.test.ts5
-rw-r--r--packages/starlight/__e2e__/fixtures/git/.gitignore10
-rw-r--r--packages/starlight/__e2e__/fixtures/git/astro.config.mjs11
-rw-r--r--packages/starlight/__e2e__/fixtures/git/package.json9
-rw-r--r--packages/starlight/__e2e__/fixtures/git/src/content/config.ts6
-rw-r--r--packages/starlight/__e2e__/fixtures/git/src/content/docs/index.md6
-rw-r--r--packages/starlight/__e2e__/fixtures/ssr/astro.config.mjs25
-rw-r--r--packages/starlight/__e2e__/fixtures/ssr/package.json10
-rw-r--r--packages/starlight/__e2e__/fixtures/ssr/src/component/ServerCheck.astro15
-rw-r--r--packages/starlight/__e2e__/fixtures/ssr/src/content/config.ts6
-rw-r--r--packages/starlight/__e2e__/fixtures/ssr/src/content/docs/404.mdx9
-rw-r--r--packages/starlight/__e2e__/fixtures/ssr/src/content/docs/content.mdx6
-rw-r--r--packages/starlight/__e2e__/fixtures/ssr/src/content/docs/demo.mdx8
-rw-r--r--packages/starlight/__e2e__/fixtures/ssr/src/content/docs/index.md18
-rw-r--r--packages/starlight/__e2e__/git.test.ts36
-rw-r--r--packages/starlight/__e2e__/ssr.test.ts66
-rw-r--r--packages/starlight/__e2e__/tabs.test.ts47
-rw-r--r--packages/starlight/__e2e__/test-utils.ts80
-rw-r--r--packages/starlight/__tests__/basics/config-errors.test.ts1
-rw-r--r--packages/starlight/__tests__/basics/git.test.ts129
-rw-r--r--packages/starlight/__tests__/basics/routing.test.ts32
-rw-r--r--packages/starlight/__tests__/git-utils.ts76
-rw-r--r--packages/starlight/__tests__/i18n-root-locale/routing.test.ts8
-rw-r--r--packages/starlight/__tests__/test-config.ts9
-rw-r--r--packages/starlight/index.astro19
-rw-r--r--packages/starlight/index.ts18
-rw-r--r--packages/starlight/integrations/virtual-user-config.ts13
-rw-r--r--packages/starlight/package.json6
-rw-r--r--packages/starlight/routes/common.astro16
-rw-r--r--packages/starlight/routes/ssr/404.astro7
-rw-r--r--packages/starlight/routes/ssr/index.astro14
-rw-r--r--packages/starlight/routes/static/404.astro (renamed from packages/starlight/404.astro)19
-rw-r--r--packages/starlight/routes/static/index.astro15
-rw-r--r--packages/starlight/utils/git.ts92
-rw-r--r--packages/starlight/utils/gitInlined.ts20
-rw-r--r--packages/starlight/utils/plugins.ts9
-rw-r--r--packages/starlight/utils/route-data.ts6
-rw-r--r--packages/starlight/utils/routing.ts13
-rw-r--r--packages/starlight/utils/user-config.ts24
-rw-r--r--packages/starlight/virtual.d.ts4
-rw-r--r--packages/starlight/vitest.config.ts8
-rw-r--r--pnpm-lock.yaml171
48 files changed, 1004 insertions, 146 deletions
diff --git a/.changeset/ninety-singers-film.md b/.changeset/ninety-singers-film.md
new file mode 100644
index 00000000..3b8fa285
--- /dev/null
+++ b/.changeset/ninety-singers-film.md
@@ -0,0 +1,7 @@
+---
+'@astrojs/starlight': patch
+---
+
+Improves performance of computing the last updated times from Git history.
+
+Instead of executing `git` for each docs page, it is now executed twice regardless of the number of pages.
diff --git a/.changeset/quiet-penguins-wonder.md b/.changeset/quiet-penguins-wonder.md
new file mode 100644
index 00000000..5fda6c0f
--- /dev/null
+++ b/.changeset/quiet-penguins-wonder.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/starlight': patch
+---
+
+Fixes last updated times on projects with custom `srcDir`
diff --git a/.changeset/six-phones-boil.md b/.changeset/six-phones-boil.md
new file mode 100644
index 00000000..fa22cbed
--- /dev/null
+++ b/.changeset/six-phones-boil.md
@@ -0,0 +1,16 @@
+---
+'@astrojs/starlight': minor
+---
+
+Adds support for server-rendered Starlight pages.
+
+When building a project with `hybrid` or `server` output mode, a new `prerender` option on Starlight config can be set to `false` to make all Starlight pages be rendered on-demand:
+
+```ts
+export default defineConfig({
+ output: 'server',
+ integrations: [starlight({
+ prerender: false
+ })],
+})
+```
diff --git a/docs/src/content/docs/manual-setup.mdx b/docs/src/content/docs/manual-setup.mdx
index 0d7d5092..07465d9f 100644
--- a/docs/src/content/docs/manual-setup.mdx
+++ b/docs/src/content/docs/manual-setup.mdx
@@ -125,6 +125,6 @@ In the future, we plan to support this use case better to avoid the need for the
### Use Starlight with SSR
-You can use Starlight alongside custom on-demand rendered pages in your project by following the [“On-demand Rendering Adapters”](https://docs.astro.build/en/guides/server-side-rendering/) guide in Astro’s docs.
+To enable SSR, follow the [“On-demand Rendering Adapters”](https://docs.astro.build/en/guides/server-side-rendering/) guide in Astro’s docs to add a server adapter to your Starlight project.
-Currently, documentation pages generated by Starlight are always prerendered regardless of your project's output mode. We hope to be able to support on-demand rendering for Starlight pages soon.
+Documentation pages generated by Starlight are pre-rendered by default regardless of your project's output mode. To opt out of pre-rendering your Starlight pages, set the [`prerender` config option](/reference/configuration/#prerender) to `false`.
diff --git a/docs/src/content/docs/reference/configuration.mdx b/docs/src/content/docs/reference/configuration.mdx
index 8b97b0d3..c878141b 100644
--- a/docs/src/content/docs/reference/configuration.mdx
+++ b/docs/src/content/docs/reference/configuration.mdx
@@ -455,6 +455,18 @@ Define whether Starlight’s default site search provider [Pagefind](https://pag
Set to `false` to disable indexing your site with Pagefind.
This will also hide the default search UI if in use.
+Pagefind cannot be enabled when the [`prerender`](#prerender) option is set to `false`.
+
+### `prerender`
+
+**type:** `boolean`
+**default:** `true`
+
+Define whether Starlight pages should be pre-rendered to static HTML or on-demand rendered by an [SSR adapter](https://docs.astro.build/en/guides/server-side-rendering/).
+
+Starlight pages are pre-rendered by default.
+If you are using an SSR adapter and want to render Starlight pages on demand, set `prerender: false`.
+
### `head`
**type:** [`HeadConfig[]`](#headconfig)
diff --git a/packages/starlight/.gitignore b/packages/starlight/.gitignore
index 071d33cb..004798ee 100644
--- a/packages/starlight/.gitignore
+++ b/packages/starlight/.gitignore
@@ -1,4 +1,4 @@
# Astro generates this during tests, but we want to ignore it.
-src/env.d.ts
-__tests__/**/env.d.ts
+env.d.ts
__tests__/**/types.d.ts
+.astro
diff --git a/packages/starlight/__e2e__/collection-config.test.ts b/packages/starlight/__e2e__/collection-config.test.ts
index 7e9d6d06..feeadfe3 100644
--- a/packages/starlight/__e2e__/collection-config.test.ts
+++ b/packages/starlight/__e2e__/collection-config.test.ts
@@ -2,12 +2,13 @@ import { expect, testFactory } from './test-utils';
// This fixture contains a space in the directory so that we have a smoke test for building
// Starlight projects with pathnames like this, which are a common source of bugs.
-const test = await testFactory('./fixtures/custom src-dir/');
+const test = testFactory('./fixtures/custom src-dir/');
test('builds a custom page using the `<StarlightPage>` component and a custom `srcDir`', async ({
page,
- starlight,
+ getProdServer,
}) => {
+ const starlight = await getProdServer();
await starlight.goto('/custom');
await expect(page.getByText('Hello')).toBeVisible();
diff --git a/packages/starlight/__e2e__/fixtures/git/.gitignore b/packages/starlight/__e2e__/fixtures/git/.gitignore
new file mode 100644
index 00000000..1fb22bd5
--- /dev/null
+++ b/packages/starlight/__e2e__/fixtures/git/.gitignore
@@ -0,0 +1,10 @@
+# This fixture is used for the E2E tests related to Git where
+# a new repo is created. Nested repos do not inherit the .gitignore
+# options from the parent repo, so even though these files are
+# already ignored on the package they have to be re-declared here.
+# Do not delete this file or the Git tests will try to index and
+# and commit all the node_modules and generated files from the test.
+env.d.ts
+.astro
+/node_modules/
+/dist/
diff --git a/packages/starlight/__e2e__/fixtures/git/astro.config.mjs b/packages/starlight/__e2e__/fixtures/git/astro.config.mjs
new file mode 100644
index 00000000..0631c4f0
--- /dev/null
+++ b/packages/starlight/__e2e__/fixtures/git/astro.config.mjs
@@ -0,0 +1,11 @@
+import starlight from '@astrojs/starlight';
+import { defineConfig } from 'astro/config';
+
+export default defineConfig({
+ integrations: [
+ starlight({
+ title: 'Git',
+ pagefind: false,
+ }),
+ ],
+});
diff --git a/packages/starlight/__e2e__/fixtures/git/package.json b/packages/starlight/__e2e__/fixtures/git/package.json
new file mode 100644
index 00000000..b120c72d
--- /dev/null
+++ b/packages/starlight/__e2e__/fixtures/git/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@e2e/git",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/starlight": "workspace:*",
+ "astro": "^4.15.3"
+ }
+}
diff --git a/packages/starlight/__e2e__/fixtures/git/src/content/config.ts b/packages/starlight/__e2e__/fixtures/git/src/content/config.ts
new file mode 100644
index 00000000..45f60b01
--- /dev/null
+++ b/packages/starlight/__e2e__/fixtures/git/src/content/config.ts
@@ -0,0 +1,6 @@
+import { defineCollection } from 'astro:content';
+import { docsSchema } from '@astrojs/starlight/schema';
+
+export const collections = {
+ docs: defineCollection({ schema: docsSchema() }),
+};
diff --git a/packages/starlight/__e2e__/fixtures/git/src/content/docs/index.md b/packages/starlight/__e2e__/fixtures/git/src/content/docs/index.md
new file mode 100644
index 00000000..9a02f87c
--- /dev/null
+++ b/packages/starlight/__e2e__/fixtures/git/src/content/docs/index.md
@@ -0,0 +1,6 @@
+---
+title: Home Page
+lastUpdated: true
+---
+
+Home page content
diff --git a/packages/starlight/__e2e__/fixtures/ssr/astro.config.mjs b/packages/starlight/__e2e__/fixtures/ssr/astro.config.mjs
new file mode 100644
index 00000000..3faa2cd8
--- /dev/null
+++ b/packages/starlight/__e2e__/fixtures/ssr/astro.config.mjs
@@ -0,0 +1,25 @@
+import { defineConfig } from 'astro/config';
+import starlight from '@astrojs/starlight';
+import node from '@astrojs/node';
+
+const prerendering = process.env.STARLIGHT_PRERENDER === 'yes';
+
+export default defineConfig({
+ output: 'server',
+ adapter: node({ mode: 'standalone' }),
+ compressHTML: false, // for easier debugging
+ // Output to different folders and expose on different ports
+ // on each case so the servers don't conflict with
+ // each other during tests.
+ outDir: prerendering ? 'dist' : 'build',
+ server: {
+ port: prerendering ? 4322 : 4321,
+ },
+ integrations: [
+ starlight({
+ title: 'SSR',
+ prerender: prerendering,
+ pagefind: false,
+ }),
+ ],
+});
diff --git a/packages/starlight/__e2e__/fixtures/ssr/package.json b/packages/starlight/__e2e__/fixtures/ssr/package.json
new file mode 100644
index 00000000..da2b31f8
--- /dev/null
+++ b/packages/starlight/__e2e__/fixtures/ssr/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@e2e/ssr",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/node": "^8.3.2",
+ "@astrojs/starlight": "workspace:*",
+ "astro": "^4.15.3"
+ }
+}
diff --git a/packages/starlight/__e2e__/fixtures/ssr/src/component/ServerCheck.astro b/packages/starlight/__e2e__/fixtures/ssr/src/component/ServerCheck.astro
new file mode 100644
index 00000000..f6450a75
--- /dev/null
+++ b/packages/starlight/__e2e__/fixtures/ssr/src/component/ServerCheck.astro
@@ -0,0 +1,15 @@
+---
+function checkServer() {
+ try {
+ Astro.clientAddress;
+
+ return true;
+ } catch {
+ // Accessing `clientAddress` during build time
+ // fails, so it is not SSR.
+ return false;
+ }
+}
+---
+
+<div id="server-check">{checkServer() ? 'On server' : 'Not server'}</div>
diff --git a/packages/starlight/__e2e__/fixtures/ssr/src/content/config.ts b/packages/starlight/__e2e__/fixtures/ssr/src/content/config.ts
new file mode 100644
index 00000000..45f60b01
--- /dev/null
+++ b/packages/starlight/__e2e__/fixtures/ssr/src/content/config.ts
@@ -0,0 +1,6 @@
+import { defineCollection } from 'astro:content';
+import { docsSchema } from '@astrojs/starlight/schema';
+
+export const collections = {
+ docs: defineCollection({ schema: docsSchema() }),
+};
diff --git a/packages/starlight/__e2e__/fixtures/ssr/src/content/docs/404.mdx b/packages/starlight/__e2e__/fixtures/ssr/src/content/docs/404.mdx
new file mode 100644
index 00000000..522e0072
--- /dev/null
+++ b/packages/starlight/__e2e__/fixtures/ssr/src/content/docs/404.mdx
@@ -0,0 +1,9 @@
+---
+title: Not Found
+template: splash
+lastUpdate: true
+---
+
+import ServerCheck from '../../component/ServerCheck.astro';
+
+<ServerCheck />
diff --git a/packages/starlight/__e2e__/fixtures/ssr/src/content/docs/content.mdx b/packages/starlight/__e2e__/fixtures/ssr/src/content/docs/content.mdx
new file mode 100644
index 00000000..be10b182
--- /dev/null
+++ b/packages/starlight/__e2e__/fixtures/ssr/src/content/docs/content.mdx
@@ -0,0 +1,6 @@
+---
+title: Content
+lastUpdate: true
+---
+
+Example page
diff --git a/packages/starlight/__e2e__/fixtures/ssr/src/content/docs/demo.mdx b/packages/starlight/__e2e__/fixtures/ssr/src/content/docs/demo.mdx
new file mode 100644
index 00000000..e475b6fd
--- /dev/null
+++ b/packages/starlight/__e2e__/fixtures/ssr/src/content/docs/demo.mdx
@@ -0,0 +1,8 @@
+---
+title: Server Check
+lastUpdate: true
+---
+
+import ServerCheck from '../../component/ServerCheck.astro';
+
+<ServerCheck />
diff --git a/packages/starlight/__e2e__/fixtures/ssr/src/content/docs/index.md b/packages/starlight/__e2e__/fixtures/ssr/src/content/docs/index.md
new file mode 100644
index 00000000..f61f5cd3
--- /dev/null
+++ b/packages/starlight/__e2e__/fixtures/ssr/src/content/docs/index.md
@@ -0,0 +1,18 @@
+---
+title: Home Page
+template: splash
+lastUpdate: true
+hero:
+ title: Make your docs shine with Starlight
+ tagline: Everything you need to build a stellar documentation website. Fast, accessible, and easy-to-use.
+ actions:
+ - text: Get started
+ icon: right-arrow
+ variant: primary
+ link: /getting-started/
+ - text: View on GitHub
+ icon: external
+ link: https://github.com/withastro/starlight
+---
+
+Home page content
diff --git a/packages/starlight/__e2e__/git.test.ts b/packages/starlight/__e2e__/git.test.ts
new file mode 100644
index 00000000..f73075fe
--- /dev/null
+++ b/packages/starlight/__e2e__/git.test.ts
@@ -0,0 +1,36 @@
+import { makeTestRepo } from '../__tests__/git-utils';
+import { expect, testFactory } from './test-utils';
+import { rm } from 'node:fs/promises';
+import { fileURLToPath } from 'node:url';
+import { join } from 'node:path';
+
+const repoPath = fileURLToPath(new URL('./fixtures/git/', import.meta.url));
+
+const test = testFactory(repoPath);
+
+test.beforeAll(async () => {
+ // Clears existing nested repo to account for previously interrupted tests.
+ await rm(join(repoPath, '.git'), { recursive: true, force: true });
+ const testRepo = makeTestRepo(repoPath);
+
+ testRepo.commitAllChanges('Add home page', '2024-02-03');
+});
+
+test.afterAll(async () => {
+ // Remove nested repo after test runs
+ await rm(join(repoPath, '.git'), { recursive: true, force: true });
+});
+
+test('include last updated date from git in the footer', async ({ page, getProdServer }) => {
+ const starlight = await getProdServer();
+ await starlight.goto('/');
+
+ await expect(page.locator('footer')).toContainText('Last updated: Feb 3, 2024');
+});
+
+test('include git information while developing', async ({ page, makeServer }) => {
+ const starlight = await makeServer('dev', { mode: 'dev' });
+ await starlight.goto('/');
+
+ await expect(page.locator('footer')).toContainText('Last updated: Feb 3, 2024');
+});
diff --git a/packages/starlight/__e2e__/ssr.test.ts b/packages/starlight/__e2e__/ssr.test.ts
new file mode 100644
index 00000000..1bfd6d43
--- /dev/null
+++ b/packages/starlight/__e2e__/ssr.test.ts
@@ -0,0 +1,66 @@
+import { expect, testFactory } from './test-utils';
+import assert from 'node:assert';
+import { parseHTML } from 'linkedom';
+
+const test = testFactory('./fixtures/ssr/');
+
+test.beforeEach(() => {
+ delete process.env.STARLIGHT_PRERENDER;
+});
+
+test('Render page on the server', async ({ page, getProdServer }) => {
+ const starlight = await getProdServer();
+ await starlight.goto('/demo');
+
+ await expect(page.locator('#server-check')).toHaveText('On server');
+});
+
+test('Render 404 page on the server', async ({ page, getProdServer }) => {
+ const starlight = await getProdServer();
+ await starlight.goto('/not-found');
+
+ await expect(page.locator('#server-check')).toHaveText('On server');
+});
+
+test('SSR mode renders the same content page as prerendering', async ({
+ getProdServer,
+ makeServer,
+}) => {
+ const starlight = await getProdServer();
+ const ssrContent = await starlight.goto('/content').then((res) => res?.text());
+ assert(ssrContent);
+
+ process.env.STARLIGHT_PRERENDER = 'yes';
+ const prerenderStarlight = await makeServer('prerender');
+ const prerenderContent = await prerenderStarlight.goto('/content').then((res) => res?.text());
+ assert(prerenderContent);
+
+ expectEquivalentHTML(prerenderContent, ssrContent);
+});
+
+test('SSR mode renders the same splash page as prerendering', async ({
+ getProdServer,
+ makeServer,
+}) => {
+ const starlight = await getProdServer();
+ const ssrContent = await starlight.goto('/').then((res) => res?.text());
+ assert(ssrContent);
+
+ process.env.STARLIGHT_PRERENDER = 'yes';
+ const prerenderStarlight = await makeServer('prerender');
+ const prerenderContent = await prerenderStarlight.goto('/').then((res) => res?.text());
+ assert(prerenderContent);
+
+ expectEquivalentHTML(prerenderContent, ssrContent);
+});
+
+function expectEquivalentHTML(a: string, b: string) {
+ expect(getNormalizedHTML(a)).toEqual(getNormalizedHTML(b));
+}
+
+function getNormalizedHTML(html: string) {
+ const window = parseHTML(html);
+ window.document.querySelectorAll('script[src]').forEach((el) => el.setAttribute('src', ''));
+ window.document.querySelectorAll('link[href]').forEach((el) => el.setAttribute('href', ''));
+ return window.toString();
+}
diff --git a/packages/starlight/__e2e__/tabs.test.ts b/packages/starlight/__e2e__/tabs.test.ts
index 6903efb3..45e96c84 100644
--- a/packages/starlight/__e2e__/tabs.test.ts
+++ b/packages/starlight/__e2e__/tabs.test.ts
@@ -1,8 +1,9 @@
import { expect, testFactory, type Locator } from './test-utils';
-const test = await testFactory('./fixtures/basics/');
+const test = testFactory('./fixtures/basics/');
-test('syncs tabs with a click event', async ({ page, starlight }) => {
+test('syncs tabs with a click event', async ({ page, getProdServer }) => {
+ const starlight = await getProdServer();
await starlight.goto('/tabs');
const tabs = page.locator('starlight-tabs');
@@ -22,7 +23,8 @@ test('syncs tabs with a click event', async ({ page, starlight }) => {
await expectSelectedTab(pkgTabsA, 'yarn', 'yarn command');
});
-test('syncs tabs with a keyboard event', async ({ page, starlight }) => {
+test('syncs tabs with a keyboard event', async ({ page, getProdServer }) => {
+ const starlight = await getProdServer();
await starlight.goto('/tabs');
const tabs = page.locator('starlight-tabs');
@@ -45,7 +47,8 @@ test('syncs tabs with a keyboard event', async ({ page, starlight }) => {
await expectSelectedTab(pkgTabsB, 'npm', 'another npm command');
});
-test('syncs only tabs using the same sync key', async ({ page, starlight }) => {
+test('syncs only tabs using the same sync key', async ({ page, getProdServer }) => {
+ const starlight = await getProdServer();
await starlight.goto('/tabs');
const tabs = page.locator('starlight-tabs');
@@ -64,7 +67,8 @@ test('syncs only tabs using the same sync key', async ({ page, starlight }) => {
await expectSelectedTab(osTabsB, 'macos', 'ls');
});
-test('supports synced tabs with different tab items', async ({ page, starlight }) => {
+test('supports synced tabs with different tab items', async ({ page, getProdServer }) => {
+ const starlight = await getProdServer();
await starlight.goto('/tabs');
const tabs = page.locator('starlight-tabs');
@@ -78,7 +82,8 @@ test('supports synced tabs with different tab items', async ({ page, starlight }
await expectSelectedTab(pkgTabsB, 'bun', 'another bun command');
});
-test('persists the focus when syncing tabs', async ({ page, starlight }) => {
+test('persists the focus when syncing tabs', async ({ page, getProdServer }) => {
+ const starlight = await getProdServer();
await starlight.goto('/tabs');
const pkgTabsA = page.locator('starlight-tabs').nth(0);
@@ -97,8 +102,9 @@ test('persists the focus when syncing tabs', async ({ page, starlight }) => {
test('preserves tabs position when alternating between tabs with different content heights', async ({
page,
- starlight,
+ getProdServer,
}) => {
+ const starlight = await getProdServer();
await starlight.goto('/tabs-variable-height');
const tabs = page.locator('starlight-tabs').nth(1);
@@ -122,8 +128,9 @@ test('preserves tabs position when alternating between tabs with different conte
test('syncs tabs with the same sync key if they do not consistenly use icons', async ({
page,
- starlight,
+ getProdServer,
}) => {
+ const starlight = await getProdServer();
await starlight.goto('/tabs');
const tabs = page.locator('starlight-tabs');
@@ -143,7 +150,11 @@ test('syncs tabs with the same sync key if they do not consistenly use icons', a
await expectSelectedTab(pkgTabsA, 'yarn', 'yarn command');
});
-test('restores tabs only for synced tabs with a persisted state', async ({ page, starlight }) => {
+test('restores tabs only for synced tabs with a persisted state', async ({
+ page,
+ getProdServer,
+}) => {
+ const starlight = await getProdServer();
await starlight.goto('/tabs');
const tabs = page.locator('starlight-tabs');
@@ -178,8 +189,9 @@ test('restores tabs only for synced tabs with a persisted state', async ({ page,
test('restores tabs for a single set of synced tabs with a persisted state', async ({
page,
- starlight,
+ getProdServer,
}) => {
+ const starlight = await getProdServer();
await starlight.goto('/tabs');
const tabs = page.locator('starlight-tabs');
@@ -198,8 +210,9 @@ test('restores tabs for a single set of synced tabs with a persisted state', asy
test('restores tabs for multiple synced tabs with different sync keys', async ({
page,
- starlight,
+ getProdServer,
}) => {
+ const starlight = await getProdServer();
await starlight.goto('/tabs');
const tabs = page.locator('starlight-tabs');
@@ -233,8 +246,9 @@ test('restores tabs for multiple synced tabs with different sync keys', async ({
test('includes the `<starlight-tabs-restore>` element only for synced tabs', async ({
page,
- starlight,
+ getProdServer,
}) => {
+ const starlight = await getProdServer();
await starlight.goto('/tabs');
// The page includes 7 sets of tabs.
@@ -245,8 +259,9 @@ test('includes the `<starlight-tabs-restore>` element only for synced tabs', asy
test('includes the synced tabs restore script only when needed and at most once', async ({
page,
- starlight,
+ getProdServer,
}) => {
+ const starlight = await getProdServer();
const syncedTabsRestoreScriptRegex = /customElements\.define\('starlight-tabs-restore',/g;
await starlight.goto('/tabs');
@@ -260,7 +275,11 @@ test('includes the synced tabs restore script only when needed and at most once'
expect((await page.content()).match(syncedTabsRestoreScriptRegex)).toBeNull();
});
-test('gracefully handles invalid persisted state for synced tabs', async ({ page, starlight }) => {
+test('gracefully handles invalid persisted state for synced tabs', async ({
+ page,
+ getProdServer,
+}) => {
+ const starlight = await getProdServer();
await starlight.goto('/tabs');
const tabs = page.locator('starlight-tabs');
diff --git a/packages/starlight/__e2e__/test-utils.ts b/packages/starlight/__e2e__/test-utils.ts
index c4030cd0..6c98bbd8 100644
--- a/packages/starlight/__e2e__/test-utils.ts
+++ b/packages/starlight/__e2e__/test-utils.ts
@@ -1,6 +1,6 @@
import { fileURLToPath } from 'node:url';
import { test as baseTest, type Page } from '@playwright/test';
-import { build, preview } from 'astro';
+import { build, dev, preview } from 'astro';
export { expect, type Locator } from '@playwright/test';
@@ -9,27 +9,67 @@ process.env.ASTRO_DISABLE_UPDATE_CHECK = 'true';
// Setup a test environment that will build and start a preview server for a given fixture path and
// provide a Starlight Playwright fixture accessible from within all tests.
-export async function testFactory(fixturePath: string) {
- let previewServer: PreviewServer | undefined;
+export function testFactory(fixturePath: string) {
+ const fixturePathUrl = new URL(fixturePath, import.meta.url);
+ // Combining absolute paths with a `file:` base URL on Windows results
+ // in a URL with the drive letter as the protocol.
+ // In that case, use the URL as is instead of interpreting the `file:` protocol.
+ const root =
+ fixturePathUrl.protocol === 'file:'
+ ? fileURLToPath(new URL(fixturePath, import.meta.url))
+ : fixturePathUrl.toString();
- const test = baseTest.extend<{ starlight: StarlightPage }>({
- starlight: async ({ page }, use) => {
- if (!previewServer) {
- throw new Error('Could not find a preview server to run tests against.');
- }
+ async function makeServer(
+ options: {
+ mode?: 'build' | 'dev';
+ } = {}
+ ): Promise<Server> {
+ const { mode } = options;
+ if (mode === 'dev') {
+ return await dev({
+ logLevel: 'error',
+ root,
+ // Vite's dev server hangs on the optimization discovery phase when
+ // trying to stop the server programmatically at the end of the test.
+ // Disabling this optimization here allows the test to run properly
+ // on CI and fresh clones of the project.
+ vite: { optimizeDeps: { noDiscovery: true } },
+ });
+ } else {
+ await build({ logLevel: 'error', root });
+ return await preview({ logLevel: 'error', root });
+ }
+ }
- await use(new StarlightPage(previewServer, page));
- },
- });
+ // Optimization for tests that don't customize any server options
+ // to not rebuild the fixture for each test.
+ let prodServer: Server | null = null;
+ const servers = new Map<string, Server>();
- test.beforeAll(async () => {
- const root = fileURLToPath(new URL(fixturePath, import.meta.url));
- await build({ logLevel: 'error', root });
- previewServer = await preview({ logLevel: 'error', root });
+ const test = baseTest.extend<{
+ getProdServer: () => Promise<StarlightPage>;
+ makeServer: (name: string, ...params: Parameters<typeof makeServer>) => Promise<StarlightPage>;
+ }>({
+ getProdServer: ({ page }, use) =>
+ use(async () => {
+ const server = (prodServer ??= await makeServer({
+ mode: 'build',
+ }));
+ return new StarlightPage(server, page);
+ }),
+ makeServer: ({ page }, use) =>
+ use(async (name, ...params) => {
+ const server = servers.get(name) ?? (await makeServer(...params));
+ servers.set(name, server);
+ return new StarlightPage(server, page);
+ }),
});
test.afterAll(async () => {
- await previewServer?.stop();
+ await prodServer?.stop();
+ for (const server of servers.values()) {
+ await server.stop();
+ }
});
return test;
@@ -38,7 +78,7 @@ export async function testFactory(fixturePath: string) {
// A Playwright test fixture accessible from within all tests.
class StarlightPage {
constructor(
- private readonly previewServer: PreviewServer,
+ private readonly server: Server,
private readonly page: Page
) {}
@@ -49,8 +89,12 @@ class StarlightPage {
// Resolve a URL relative to the server used during a test run.
resolveUrl(url: string) {
- return `http://localhost:${this.previewServer.port}${url.replace(/^\/?/, '/')}`;
+ const port = 'address' in this.server ? this.server.address.port : this.server.port;
+
+ return `http://localhost:${port}${url.replace(/^\/?/, '/')}`;
}
}
type PreviewServer = Awaited<ReturnType<typeof preview>>;
+type DevServer = Awaited<ReturnType<typeof dev>>;
+type Server = PreviewServer | DevServer;
diff --git a/packages/starlight/__tests__/basics/config-errors.test.ts b/packages/starlight/__tests__/basics/config-errors.test.ts
index 3b326360..07424eb9 100644
--- a/packages/starlight/__tests__/basics/config-errors.test.ts
+++ b/packages/starlight/__tests__/basics/config-errors.test.ts
@@ -65,6 +65,7 @@ test('parses valid config successfully', () => {
"locales": undefined,
"pagefind": true,
"pagination": true,
+ "prerender": true,
"tableOfContents": {
"maxHeadingLevel": 3,
"minHeadingLevel": 2,
diff --git a/packages/starlight/__tests__/basics/git.test.ts b/packages/starlight/__tests__/basics/git.test.ts
index fd1b2549..28f6a97b 100644
--- a/packages/starlight/__tests__/basics/git.test.ts
+++ b/packages/starlight/__tests__/basics/git.test.ts
@@ -1,9 +1,11 @@
-import { mkdtempSync, writeFileSync } from 'node:fs';
-import { join } from 'node:path';
-import { tmpdir } from 'node:os';
-import { spawnSync } from 'node:child_process';
-import { describe, expect, test } from 'vitest';
-import { getNewestCommitDate } from '../../utils/git';
+import { assert, describe, expect, test } from 'vitest';
+import {
+ getAllNewestCommitDate,
+ getNewestCommitDate,
+ makeAPI as makeLiveGitAPI,
+} from '../../utils/git';
+import { makeAPI as makeInlineGitAPI } from '../../utils/gitInlined';
+import { makeTestRepo, type ISODate } from '../git-utils';
describe('getNewestCommitDate', () => {
const { commitAllChanges, getFilePath, writeFile } = makeTestRepo();
@@ -20,6 +22,20 @@ describe('getNewestCommitDate', () => {
expectCommitDateToEqual(getNewestCommitDate(getFilePath(file)), lastCommitDate);
});
+ test('returns the newest commit date from the wrapped API', () => {
+ const api = makeLiveGitAPI(getFilePath(''));
+
+ const file = 'updated.md';
+ const lastCommitDate = '2023-06-25';
+
+ writeFile(file, 'content 0');
+ commitAllChanges('add updated.md', '2023-06-21');
+ writeFile(file, 'content 1');
+ commitAllChanges('update updated.md', lastCommitDate);
+
+ expectCommitDateToEqual(api.getNewestCommitDate(file), lastCommitDate);
+ });
+
test('returns the initial commit date for a file never updated', () => {
const file = 'added.md';
const commitDate = '2022-09-18';
@@ -70,48 +86,73 @@ describe('getNewestCommitDate', () => {
});
});
-function expectCommitDateToEqual(commitDate: CommitDate, expectedDateStr: ISODate) {
- const expectedDate = new Date(expectedDateStr);
- expect(commitDate).toStrictEqual(expectedDate);
-}
+describe('getAllNewestCommitDate', () => {
+ const { commitAllChanges, getFilePath, writeFile } = makeTestRepo();
-function makeTestRepo() {
- const repoPath = mkdtempSync(join(tmpdir(), 'starlight-test-git-'));
+ test('returns the newest commit date', () => {
+ writeFile('added.md', 'content');
+ commitAllChanges('add added.md', '2022-09-18');
- function runInRepo(command: string, args: string[], env: NodeJS.ProcessEnv = {}) {
- const result = spawnSync(command, args, { cwd: repoPath, env });
+ writeFile('updated.md', 'content 0');
+ commitAllChanges('add updated.md', '2023-06-21');
+ writeFile('updated.md', 'content 1');
+ commitAllChanges('update updated.md', '2023-06-25');
- if (result.status !== 0) {
- throw new Error(`Failed to execute test repository command: '${command} ${args.join(' ')}'`);
+ writeFile('updated with space.md', 'content 0');
+ commitAllChanges('add updated.md', '2021-01-01');
+ writeFile('updated with space.md', 'content 1');
+ commitAllChanges('update updated.md', '2021-01-02');
+
+ writeFile('updated-same-day.md', 'content 0');
+ commitAllChanges('add updated.md', '2023-06-25T12:34:56Z');
+ writeFile('updated-same-day.md', 'content 1');
+ commitAllChanges('update updated.md', '2023-06-25T14:22:35Z');
+
+ const latestDates = new Map(getAllNewestCommitDate(getFilePath('')));
+
+ const expectedDates = new Map<string, ISODate>([
+ ['added.md', '2022-09-18'],
+ ['updated.md', '2023-06-25'],
+ ['updated with space.md', '2021-01-02'],
+ ['updated-same-day.md', '2023-06-25T14:22:35Z'],
+ ]);
+
+ for (const [file, date] of latestDates.entries()) {
+ const expectedDate = expectedDates.get(file);
+ assert.ok(expectedDate, `Unexpected tracked file: ${file}`);
+ expectCommitDateToEqual(new Date(date), expectedDate!);
}
- }
-
- // Configure git specifically for this test repository.
- runInRepo('git', ['init']);
- runInRepo('git', ['config', 'user.name', 'starlight-test']);
- runInRepo('git', ['config', 'user.email', 'starlight-test@example.com']);
- runInRepo('git', ['config', 'commit.gpgsign', 'false']);
-
- return {
- // The `dateStr` argument should be in the `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SSZ` format.
- commitAllChanges(message: string, dateStr: ISODate) {
- const date = dateStr.endsWith('Z') ? dateStr : `${dateStr}T00:00:00Z`;
-
- runInRepo('git', ['add', '-A']);
- // This sets both the author and committer dates to the provided date.
- runInRepo('git', ['commit', '-m', message, '--date', date], { GIT_COMMITTER_DATE: date });
- },
- getFilePath(name: string) {
- return join(repoPath, name);
- },
- writeFile(name: string, content: string) {
- writeFileSync(join(repoPath, name), content);
- },
- };
-}
-type ISODate =
- | `${number}-${number}-${number}`
- | `${number}-${number}-${number}T${number}:${number}:${number}Z`;
+ for (const file of expectedDates.keys()) {
+ const latestDate = latestDates.get(file);
+ assert.ok(latestDate, `Missing tracked file: ${file}`);
+ }
+ });
+
+ test('returns the newest commit date from inlined API', () => {
+ const api = makeInlineGitAPI(getAllNewestCommitDate(getFilePath('')));
+
+ const expectedDates = new Map<string, ISODate>([
+ ['added.md', '2022-09-18'],
+ ['updated.md', '2023-06-25'],
+ ['updated with space.md', '2021-01-02'],
+ ['updated-same-day.md', '2023-06-25T14:22:35Z'],
+ ]);
+
+ for (const [file, expectedDate] of expectedDates.entries()) {
+ const latestDate = api.getNewestCommitDate(file);
+ expectCommitDateToEqual(latestDate, expectedDate);
+ }
+ });
+
+ test('returns an empty list when the git history for the directory cannot be retrieved', () => {
+ expect(getAllNewestCommitDate(getFilePath('../not-a-starlight-test-repo'))).toStrictEqual([]);
+ });
+});
+
+function expectCommitDateToEqual(commitDate: CommitDate, expectedDateStr: ISODate) {
+ const expectedDate = new Date(expectedDateStr);
+ expect(commitDate).toStrictEqual(expectedDate);
+}
type CommitDate = ReturnType<typeof getNewestCommitDate>;
diff --git a/packages/starlight/__tests__/basics/routing.test.ts b/packages/starlight/__tests__/basics/routing.test.ts
index 37c3f9b0..c94e4895 100644
--- a/packages/starlight/__tests__/basics/routing.test.ts
+++ b/packages/starlight/__tests__/basics/routing.test.ts
@@ -1,7 +1,9 @@
+import { type GetStaticPathsResult } from 'astro';
import { getCollection } from 'astro:content';
import config from 'virtual:starlight/user-config';
import { expect, test, vi } from 'vitest';
-import { routes } from '../../utils/routing';
+import { routes, paths, getRouteBySlugParam } from '../../utils/routing';
+import { slugToParam } from '../../utils/slugs';
vi.mock('astro:content', async () =>
(await import('../test-utils')).mockedAstroContent({
@@ -42,6 +44,34 @@ test('routes have locale data added', () => {
}
});
+test('paths contain normalized slugs for path parameters', () => {
+ const expectedPaths: GetStaticPathsResult = [
+ {
+ params: { slug: '404' },
+ props: routes[0]!,
+ },
+ {
+ params: { slug: undefined },
+ props: routes[1]!,
+ },
+ {
+ params: { slug: 'guides/authoring-content' },
+ props: routes[2]!,
+ },
+ ];
+
+ expect(paths).toEqual(expectedPaths);
+});
+
+test('routes can be retrieved from their path parameters', () => {
+ for (const route of routes) {
+ const params = slugToParam(route.slug);
+ const routeFromParams = getRouteBySlugParam(params);
+
+ expect(routeFromParams).toBe(route);
+ }
+});
+
test('routes includes drafts except in production', async () => {
expect(routes.find((route) => route.id === 'guides/authoring-content.mdx')).toBeTruthy();
diff --git a/packages/starlight/__tests__/git-utils.ts b/packages/starlight/__tests__/git-utils.ts
new file mode 100644
index 00000000..81999931
--- /dev/null
+++ b/packages/starlight/__tests__/git-utils.ts
@@ -0,0 +1,76 @@
+import { join } from 'node:path';
+import { mkdtempSync, mkdirSync, writeFileSync, realpathSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { spawnSync } from 'node:child_process';
+
+export function makeTestRepo(onPath?: string) {
+ const repoPath = realpathSync(onPath ?? mkdtempSync(join(tmpdir(), 'starlight-test-git-')));
+
+ function runInRepo(command: string, args: string[], env: NodeJS.ProcessEnv = process.env) {
+ // Format arguments to be shell-friendly for Windows.
+ const formattedArgs = process.platform === 'win32' ? args.map((arg) => `"${arg}"`) : args;
+ const result = spawnSync(command, formattedArgs, {
+ cwd: repoPath,
+ env,
+ encoding: 'utf8',
+ // Run commands using shell on Windows to ensure proper path resolution.
+ shell: process.platform === 'win32',
+ windowsVerbatimArguments: true,
+ });
+
+ if (result.status !== 0) {
+ console.log(result.stdout);
+ console.error(result.stderr);
+ console.error(result.error);
+ throw new Error(`Failed to execute test repository command: '${command} ${args.join(' ')}'`);
+ }
+ }
+
+ // Configure git specifically for this test repository.
+ runInRepo('git', ['init']);
+ runInRepo('git', ['config', 'user.name', 'starlight-test']);
+ runInRepo('git', ['config', 'user.email', 'starlight-test@example.com']);
+ runInRepo('git', ['config', 'commit.gpgsign', 'false']);
+
+ return {
+ runInRepo,
+ // The `dateStr` argument should be in the `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SSZ` format.
+ commitAllChanges(message: string, dateStr: ISODate) {
+ const date = dateStr.endsWith('Z') ? dateStr : `${dateStr}T00:00:00Z`;
+
+ runInRepo('git', ['add', '-A']);
+ // This sets both the author and committer dates to the provided date.
+ runInRepo('git', ['commit', '-m', message, '--date', date], { GIT_COMMITTER_DATE: date });
+ },
+ getFilePath(name: string) {
+ return join(repoPath, name);
+ },
+ writeFile(name: string, content: string) {
+ writeFileSync(join(repoPath, name), content);
+ },
+ writeFileTree(fileTree: FileTree) {
+ writeFileTree(repoPath, fileTree);
+ },
+ };
+}
+
+export type ISODate =
+ | `${number}-${number}-${number}`
+ | `${number}-${number}-${number}T${number}:${number}:${number}Z`;
+
+// Receive a file tree instead of a flattened list of files so it better works on Windows.
+export type FileTree = { [name in string]: TreeEntry };
+type TreeEntry = string | FileTree;
+
+function writeFileTree(root: string, fileTree: FileTree) {
+ for (const [name, entry] of Object.entries(fileTree)) {
+ const path = join(root, name);
+
+ if (typeof entry === 'string') {
+ writeFileSync(path, entry);
+ } else {
+ mkdirSync(path, { recursive: true });
+ writeFileTree(path, entry);
+ }
+ }
+}
diff --git a/packages/starlight/__tests__/i18n-root-locale/routing.test.ts b/packages/starlight/__tests__/i18n-root-locale/routing.test.ts
index 824eebe3..32654f3a 100644
--- a/packages/starlight/__tests__/i18n-root-locale/routing.test.ts
+++ b/packages/starlight/__tests__/i18n-root-locale/routing.test.ts
@@ -2,7 +2,7 @@ import config from 'virtual:starlight/user-config';
import { assert, expect, test, vi } from 'vitest';
import { routes } from '../../utils/routing';
import { generateRouteData } from '../../utils/route-data';
-import * as git from '../../utils/git';
+import * as git from 'virtual:starlight/git-info';
vi.mock('astro:content', async () =>
(await import('../test-utils')).mockedAstroContent({
@@ -82,9 +82,9 @@ test('fallback routes use fallback entry last updated dates', () => {
});
expect(getNewestCommitDate).toHaveBeenCalledOnce();
- expect(getNewestCommitDate.mock.lastCall?.[0]).toMatch(
- /src[/\\]content[/\\]docs[/\\]guides[/\\]authoring-content.mdx$/
- // ^ no `en/` prefix
+ expect(getNewestCommitDate).toHaveBeenCalledWith(
+ 'guides/authoring-content.mdx'
+ //^ no `en/` prefix
);
getNewestCommitDate.mockRestore();
diff --git a/packages/starlight/__tests__/test-config.ts b/packages/starlight/__tests__/test-config.ts
index 1f3de592..b1a34440 100644
--- a/packages/starlight/__tests__/test-config.ts
+++ b/packages/starlight/__tests__/test-config.ts
@@ -11,17 +11,24 @@ export async function defineVitestConfig(
opts?: {
build?: Pick<AstroConfig['build'], 'format'>;
trailingSlash?: AstroConfig['trailingSlash'];
+ command?: 'dev' | 'build' | 'preview';
}
) {
const root = new URL('./', import.meta.url);
const srcDir = new URL('./src/', root);
const build = opts?.build ?? { format: 'directory' };
const trailingSlash = opts?.trailingSlash ?? 'ignore';
+ const command = opts?.command ?? 'dev';
const { starlightConfig } = await runPlugins(config, plugins, createTestPluginContext());
return getViteConfig({
plugins: [
- vitePluginStarlightUserConfig(starlightConfig, { root, srcDir, build, trailingSlash }),
+ vitePluginStarlightUserConfig(command, starlightConfig, {
+ root,
+ srcDir,
+ build,
+ trailingSlash,
+ }),
],
test: {
snapshotSerializers: ['./snapshot-serializer-astro-error.ts'],
diff --git a/packages/starlight/index.astro b/packages/starlight/index.astro
deleted file mode 100644
index 74ba7b1c..00000000
--- a/packages/starlight/index.astro
+++ /dev/null
@@ -1,19 +0,0 @@
----
-import type { InferGetStaticPropsType } from 'astro';
-import { generateRouteData } from './utils/route-data';
-import { paths } from './utils/routing';
-
-import Page from './components/Page.astro';
-
-export const prerender = true;
-
-export async function getStaticPaths() {
- return paths;
-}
-
-type Props = InferGetStaticPropsType<typeof getStaticPaths>;
-const { Content, headings } = await Astro.props.entry.render();
-const route = generateRouteData({ props: { ...Astro.props, headings }, url: Astro.url });
----
-
-<Page {...route}><Content frontmatter={Astro.props.entry.data} /></Page>
diff --git a/packages/starlight/index.ts b/packages/starlight/index.ts
index c13c03bc..d2a5e572 100644
--- a/packages/starlight/index.ts
+++ b/packages/starlight/index.ts
@@ -50,17 +50,20 @@ export default function StarlightIntegration({
if (!starlightConfig.disable404Route) {
injectRoute({
pattern: '404',
- entrypoint: '@astrojs/starlight/404.astro',
- // Ensure page is pre-rendered even when project is on server output mode
- prerender: true,
+ entrypoint: starlightConfig.prerender
+ ? '@astrojs/starlight/routes/static/404.astro'
+ : '@astrojs/starlight/routes/ssr/404.astro',
+ prerender: starlightConfig.prerender,
});
}
injectRoute({
pattern: '[...slug]',
- entrypoint: '@astrojs/starlight/index.astro',
- // Ensure page is pre-rendered even when project is on server output mode
- prerender: true,
+ 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];
@@ -73,6 +76,7 @@ export default function StarlightIntegration({
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.
@@ -87,7 +91,7 @@ export default function StarlightIntegration({
updateConfig({
vite: {
- plugins: [vitePluginStarlightUserConfig(starlightConfig, config)],
+ plugins: [vitePluginStarlightUserConfig(command, starlightConfig, config)],
},
markdown: {
remarkPlugins: [
diff --git a/packages/starlight/integrations/virtual-user-config.ts b/packages/starlight/integrations/virtual-user-config.ts
index 75bdd0d0..c0bd6089 100644
--- a/packages/starlight/integrations/virtual-user-config.ts
+++ b/packages/starlight/integrations/virtual-user-config.ts
@@ -1,7 +1,8 @@
-import type { AstroConfig, ViteUserConfig } from 'astro';
+import type { AstroConfig, HookParameters, ViteUserConfig } from 'astro';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { StarlightConfig } from '../utils/user-config';
+import { getAllNewestCommitDate } from '../utils/git';
function resolveVirtualModuleId<T extends string>(id: T): `\0${T}` {
return `\0${id}`;
@@ -9,6 +10,7 @@ function resolveVirtualModuleId<T extends string>(id: T): `\0${T}` {
/** Vite plugin that exposes Starlight user config and project context via virtual modules. */
export function vitePluginStarlightUserConfig(
+ command: HookParameters<'astro:config:setup'>['command'],
opts: StarlightConfig,
{
build,
@@ -29,6 +31,8 @@ export function vitePluginStarlightUserConfig(
const resolveId = (id: string, base = root) =>
JSON.stringify(id.startsWith('.') ? resolve(fileURLToPath(base), id) : id);
+ const docsPath = resolve(fileURLToPath(srcDir), 'content/docs');
+
const virtualComponentModules = Object.fromEntries(
Object.entries(opts.components).map(([name, path]) => [
`virtual:starlight/components/${name}`,
@@ -45,6 +49,13 @@ export function vitePluginStarlightUserConfig(
srcDir,
trailingSlash,
})}`,
+ 'virtual:starlight/git-info':
+ (command !== 'build'
+ ? `import { makeAPI } from '${new URL('../utils/git.ts', import.meta.url)}';` +
+ `const api = makeAPI(${JSON.stringify(docsPath)});`
+ : `import { makeAPI } from '${new URL('../utils/gitInlined.ts', import.meta.url)}';` +
+ `const api = makeAPI(${JSON.stringify(getAllNewestCommitDate(docsPath))});`) +
+ 'export const getNewestCommitDate = api.getNewestCommitDate;',
'virtual:starlight/user-css': opts.customCss.map((id) => `import ${resolveId(id)};`).join(''),
'virtual:starlight/user-images': opts.logo
? 'src' in opts.logo
diff --git a/packages/starlight/package.json b/packages/starlight/package.json
index 9cb31275..13786616 100644
--- a/packages/starlight/package.json
+++ b/packages/starlight/package.json
@@ -167,8 +167,7 @@
"types": "./integrations/expressive-code/hast.d.ts",
"default": "./integrations/expressive-code/hast.mjs"
},
- "./index.astro": "./index.astro",
- "./404.astro": "./404.astro",
+ "./routes/*": "./routes/*",
"./style/markdown.css": "./style/markdown.css"
},
"peerDependencies": {
@@ -180,7 +179,8 @@
"@types/node": "^18.16.19",
"@vitest/coverage-v8": "^1.6.0",
"astro": "^4.15.3",
- "vitest": "^1.6.0"
+ "vitest": "^1.6.0",
+ "linkedom": "^0.18.4"
},
"dependencies": {
"@astrojs/mdx": "^3.1.3",
diff --git a/packages/starlight/routes/common.astro b/packages/starlight/routes/common.astro
new file mode 100644
index 00000000..eab058a9
--- /dev/null
+++ b/packages/starlight/routes/common.astro
@@ -0,0 +1,16 @@
+---
+import { generateRouteData } from '../utils/route-data';
+import type { Route } from '../utils/routing';
+import Page from '../components/Page.astro';
+
+export type Props = {
+ route: Route;
+};
+
+const { route } = Astro.props;
+
+const { Content, headings } = await route.entry.render();
+const routeData = generateRouteData({ props: { ...route, headings }, url: Astro.url });
+---
+
+<Page {...routeData}><Content frontmatter={route.entry.data} /></Page>
diff --git a/packages/starlight/routes/ssr/404.astro b/packages/starlight/routes/ssr/404.astro
new file mode 100644
index 00000000..9376b310
--- /dev/null
+++ b/packages/starlight/routes/ssr/404.astro
@@ -0,0 +1,7 @@
+---
+import FourOhFour from '../static/404.astro';
+
+export const prerender = false;
+---
+
+<FourOhFour />
diff --git a/packages/starlight/routes/ssr/index.astro b/packages/starlight/routes/ssr/index.astro
new file mode 100644
index 00000000..9625bd57
--- /dev/null
+++ b/packages/starlight/routes/ssr/index.astro
@@ -0,0 +1,14 @@
+---
+import { getRouteBySlugParam } from '../../utils/routing';
+import CommonPage from '../common.astro';
+
+export const prerender = false;
+
+const route = getRouteBySlugParam(Astro.params.slug);
+
+if (route === undefined) {
+ return new Response(null, { status: 404 });
+}
+---
+
+<CommonPage route={route} />
diff --git a/packages/starlight/404.astro b/packages/starlight/routes/static/404.astro
index 15cabd6a..aa6e4afa 100644
--- a/packages/starlight/404.astro
+++ b/packages/starlight/routes/static/404.astro
@@ -1,12 +1,11 @@
---
import { getEntry } from 'astro:content';
import config from 'virtual:starlight/user-config';
-import EmptyContent from './components/EmptyMarkdown.md';
-import Page from './components/Page.astro';
-import { generateRouteData } from './utils/route-data';
-import type { StarlightDocsEntry } from './utils/routing';
-import { useTranslations } from './utils/translations';
-import { BuiltInDefaultLocale } from './utils/i18n';
+import EmptyContent from '../../components/EmptyMarkdown.md';
+import type { Route, StarlightDocsEntry } from '../../utils/routing';
+import { useTranslations } from '../../utils/translations';
+import { BuiltInDefaultLocale } from '../../utils/i18n';
+import CommonPage from '../common.astro';
export const prerender = true;
@@ -42,11 +41,7 @@ const fallbackEntry: StarlightDocsEntry = {
const userEntry = await getEntry('docs', '404');
const entry = userEntry || fallbackEntry;
-const { Content, headings } = await entry.render();
-const route = generateRouteData({
- props: { ...entryMeta, entryMeta, headings, entry, id: entry.id, slug: entry.slug },
- url: Astro.url,
-});
+const route: Route = { ...entryMeta, entryMeta, entry, id: entry.id, slug: entry.slug };
---
-<Page {...route}><Content /></Page>
+<CommonPage {route} />
diff --git a/packages/starlight/routes/static/index.astro b/packages/starlight/routes/static/index.astro
new file mode 100644
index 00000000..dfa6f259
--- /dev/null
+++ b/packages/starlight/routes/static/index.astro
@@ -0,0 +1,15 @@
+---
+import type { InferGetStaticPropsType } from 'astro';
+import { paths } from '../../utils/routing';
+import CommonPage from '../common.astro';
+
+export const prerender = true;
+
+export async function getStaticPaths() {
+ return paths;
+}
+
+type Props = InferGetStaticPropsType<typeof getStaticPaths>;
+---
+
+<CommonPage route={Astro.props} />
diff --git a/packages/starlight/utils/git.ts b/packages/starlight/utils/git.ts
index d69bd259..e5937883 100644
--- a/packages/starlight/utils/git.ts
+++ b/packages/starlight/utils/git.ts
@@ -1,7 +1,22 @@
-import { basename, dirname } from 'node:path';
+/**
+ * Git module to be used from the dev server and from the integration.
+ */
+
+import { basename, dirname, relative, resolve } from 'node:path';
+import { realpathSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
-export function getNewestCommitDate(file: string) {
+export type GitAPI = {
+ getNewestCommitDate: (file: string) => Date;
+};
+
+export const makeAPI = (directory: string): GitAPI => {
+ return {
+ getNewestCommitDate: (file) => getNewestCommitDate(resolve(directory, file)),
+ };
+};
+
+export function getNewestCommitDate(file: string): Date {
const result = spawnSync('git', ['log', '--format=%ct', '--max-count=1', basename(file)], {
cwd: dirname(file),
encoding: 'utf-8',
@@ -22,3 +37,76 @@ export function getNewestCommitDate(file: string) {
const date = new Date(timestamp * 1000);
return date;
}
+
+function getRepoRoot(directory: string): string {
+ const result = spawnSync('git', ['rev-parse', '--show-toplevel'], {
+ cwd: directory,
+ encoding: 'utf-8',
+ });
+
+ if (result.error) {
+ return directory;
+ }
+
+ try {
+ return realpathSync(result.stdout.trim());
+ } catch {
+ return directory;
+ }
+}
+
+export function getAllNewestCommitDate(directory: string): [string, number][] {
+ const repoRoot = getRepoRoot(directory);
+
+ const gitLog = spawnSync(
+ 'git',
+ [
+ 'log',
+ // Format each history entry as t:<seconds since epoch>
+ '--format=t:%ct',
+ // In each entry include the name and status for each modified file
+ '--name-status',
+ '--',
+ directory,
+ ],
+ {
+ cwd: repoRoot,
+ encoding: 'utf-8',
+ }
+ );
+
+ if (gitLog.error) {
+ return [];
+ }
+
+ let runningDate = Date.now();
+ const latestDates = new Map<string, number>();
+
+ for (const logLine of gitLog.stdout.split('\n')) {
+ if (logLine.startsWith('t:')) {
+ // t:<seconds since epoch>
+ runningDate = Number.parseInt(logLine.slice(2)) * 1000;
+ }
+
+ // - Added files take the format `A\t<file>`
+ // - Modified files take the format `M\t<file>`
+ // - Deleted files take the format `D\t<file>`
+ // - Renamed files take the format `R<count>\t<old>\t<new>`
+ // - Copied files take the format `C<count>\t<old>\t<new>`
+ // The name of the file as of the commit being processed is always
+ // the last part of the log line.
+ const tabSplit = logLine.lastIndexOf('\t');
+ if (tabSplit === -1) continue;
+ const fileName = logLine.slice(tabSplit + 1);
+
+ const currentLatest = latestDates.get(fileName) || 0;
+ latestDates.set(fileName, Math.max(currentLatest, runningDate));
+ }
+
+ return Array.from(latestDates.entries()).map(([file, date]) => {
+ const fileFullPath = resolve(repoRoot, file);
+ const fileInDirectory = relative(directory, fileFullPath);
+
+ return [fileInDirectory, date];
+ });
+}
diff --git a/packages/starlight/utils/gitInlined.ts b/packages/starlight/utils/gitInlined.ts
new file mode 100644
index 00000000..387e67ad
--- /dev/null
+++ b/packages/starlight/utils/gitInlined.ts
@@ -0,0 +1,20 @@
+/**
+ * Git module to be used on production build results.
+ * The API is based on inlined git information.
+ */
+
+import type { GitAPI, getAllNewestCommitDate } from './git';
+
+type InlinedData = ReturnType<typeof getAllNewestCommitDate>;
+
+export const makeAPI = (data: InlinedData): GitAPI => {
+ const trackedDocsFiles = new Map(data);
+
+ return {
+ getNewestCommitDate: (file) => {
+ const timestamp = trackedDocsFiles.get(file);
+ if (!timestamp) throw new Error(`Failed to retrieve the git history for file "${file}"`);
+ return new Date(timestamp);
+ },
+ };
+};
diff --git a/packages/starlight/utils/plugins.ts b/packages/starlight/utils/plugins.ts
index 94f07db2..e0df8ceb 100644
--- a/packages/starlight/utils/plugins.ts
+++ b/packages/starlight/utils/plugins.ts
@@ -2,6 +2,7 @@ import type { AstroIntegration } from 'astro';
import { z } from 'astro/zod';
import { StarlightConfigSchema, type StarlightUserConfig } from '../utils/user-config';
import { parseWithFriendlyErrors } from '../utils/error-map';
+import { AstroError } from 'astro/errors';
/**
* Runs Starlight plugins in the order that they are configured after validating the user-provided
@@ -72,6 +73,14 @@ export async function runPlugins(
});
}
+ if (context.config.output === 'static' && !starlightConfig.prerender) {
+ throw new AstroError(
+ 'Starlight’s `prerender: false` option requires `output: "hybrid"` or `"server"` in your Astro config.',
+ 'Either set `output` in your Astro config or set `prerender: true` in the Starlight options.\n\n' +
+ 'Learn more about rendering modes in the Astro docs: https://docs.astro.build/en/basics/rendering-modes/'
+ );
+ }
+
return { integrations, starlightConfig };
}
diff --git a/packages/starlight/utils/route-data.ts b/packages/starlight/utils/route-data.ts
index 0ce36799..6a3e3a2a 100644
--- a/packages/starlight/utils/route-data.ts
+++ b/packages/starlight/utils/route-data.ts
@@ -1,9 +1,8 @@
import type { MarkdownHeading } from 'astro';
-import { fileURLToPath } from 'node:url';
import project from 'virtual:starlight/project-context';
import config from 'virtual:starlight/user-config';
import { generateToC, type TocItem } from './generateToC';
-import { getNewestCommitDate } from './git';
+import { getNewestCommitDate } from 'virtual:starlight/git-info';
import { getPrevNextLinks, getSidebar, type SidebarEntry } from './navigation';
import { ensureTrailingSlash } from './path';
import type { Route } from './routing';
@@ -82,11 +81,10 @@ function getLastUpdated({ entry }: PageProps): Date | undefined {
const { lastUpdated: configLastUpdated } = config;
if (frontmatterLastUpdated ?? configLastUpdated) {
- const currentFilePath = fileURLToPath(new URL('src/content/docs/' + entry.id, project.root));
try {
return frontmatterLastUpdated instanceof Date
? frontmatterLastUpdated
- : getNewestCommitDate(currentFilePath);
+ : getNewestCommitDate(entry.id);
} catch {
// If the git command fails, ignore the error.
return undefined;
diff --git a/packages/starlight/utils/routing.ts b/packages/starlight/utils/routing.ts
index a0b15fd3..b365fb5f 100644
--- a/packages/starlight/utils/routing.ts
+++ b/packages/starlight/utils/routing.ts
@@ -100,6 +100,19 @@ function getRoutes(): Route[] {
}
export const routes = getRoutes();
+function getParamRouteMapping(): ReadonlyMap<string | undefined, Route> {
+ const map = new Map<string | undefined, Route>();
+ for (const route of routes) {
+ map.set(slugToParam(route.slug), route);
+ }
+ return map;
+}
+const routesBySlugParam = getParamRouteMapping();
+
+export function getRouteBySlugParam(slugParam: string | undefined): Route | undefined {
+ return routesBySlugParam.get(slugParam?.replace(/\/$/, '') || undefined);
+}
+
function getPaths(): Path[] {
return routes.map((route) => ({
params: { slug: slugToParam(route.slug) },
diff --git a/packages/starlight/utils/user-config.ts b/packages/starlight/utils/user-config.ts
index 296b8c84..9a47db4c 100644
--- a/packages/starlight/utils/user-config.ts
+++ b/packages/starlight/utils/user-config.ts
@@ -195,7 +195,7 @@ const UserConfigSchema = z.object({
* Set to `false` to disable indexing your site with Pagefind.
* This will also hide the default search UI if in use.
*/
- pagefind: z.boolean().default(true),
+ pagefind: z.boolean().optional(),
/** Specify paths to components that should override Starlight’s default components */
components: ComponentConfigSchema(),
@@ -209,6 +209,13 @@ const UserConfigSchema = z.object({
/** Disable Starlight's default 404 page. */
disable404Route: z.boolean().default(false).describe("Disable Starlight's default 404 page."),
+ /**
+ * Define whether Starlight pages should be prerendered or not.
+ * Defaults to always prerender Starlight pages, even when the project is
+ * set to "server" output mode.
+ */
+ prerender: z.boolean().default(true),
+
/** Enable displaying a “Built with Starlight” link in your site’s footer. */
credits: z
.boolean()
@@ -216,8 +223,16 @@ const UserConfigSchema = z.object({
.describe('Enable displaying a “Built with Starlight” link in your site’s footer.'),
});
-export const StarlightConfigSchema = UserConfigSchema.strict().transform(
- ({ title, locales, defaultLocale, ...config }, ctx) => {
+export const StarlightConfigSchema = UserConfigSchema.strict()
+ .transform((config) => ({
+ ...config,
+ // Pagefind only defaults to true if prerender is also true.
+ pagefind: config.pagefind ?? config.prerender,
+ }))
+ .refine((config) => !(!config.prerender && config.pagefind), {
+ message: 'Pagefind search is not support with prerendering disabled.',
+ })
+ .transform(({ title, locales, defaultLocale, ...config }, ctx) => {
const configuredLocales = Object.keys(locales ?? {});
// This is a multilingual site (more than one locale configured) or a monolingual site with
@@ -286,8 +301,7 @@ export const StarlightConfigSchema = UserConfigSchema.strict().transform(
defaultLocale: defaultLocaleConfig,
locales: undefined,
} as const;
- }
-);
+ });
export type StarlightConfig = z.infer<typeof StarlightConfigSchema>;
export type StarlightUserConfig = z.input<typeof StarlightConfigSchema>;
diff --git a/packages/starlight/virtual.d.ts b/packages/starlight/virtual.d.ts
index 1e733866..de3ba265 100644
--- a/packages/starlight/virtual.d.ts
+++ b/packages/starlight/virtual.d.ts
@@ -14,6 +14,10 @@ declare module 'virtual:starlight/project-context' {
export default ProjectContext;
}
+declare module 'virtual:starlight/git-info' {
+ export function getNewestCommitDate(file: string): Date;
+}
+
declare module 'virtual:starlight/user-css' {}
declare module 'virtual:starlight/user-images' {
diff --git a/packages/starlight/vitest.config.ts b/packages/starlight/vitest.config.ts
index 70ec5159..e15ce187 100644
--- a/packages/starlight/vitest.config.ts
+++ b/packages/starlight/vitest.config.ts
@@ -21,10 +21,10 @@ export default defineConfig({
],
thresholds: {
autoUpdate: true,
- lines: 80.11,
- functions: 93.61,
- branches: 91.23,
- statements: 80.11,
+ lines: 89.53,
+ functions: 94.08,
+ branches: 93.07,
+ statements: 89.53,
},
},
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c2911151..e2c7deef 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -240,6 +240,9 @@ importers:
astro:
specifier: ^4.15.3
version: 4.15.3(@types/node@18.16.19)(typescript@5.4.5)
+ linkedom:
+ specifier: ^0.18.4
+ version: 0.18.4
vitest:
specifier: ^1.6.0
version: 1.6.0(@types/node@18.16.19)
@@ -262,6 +265,27 @@ importers:
specifier: ^4.15.3
version: 4.15.3(@types/node@18.16.19)(typescript@5.4.5)
+ packages/starlight/__e2e__/fixtures/git:
+ dependencies:
+ '@astrojs/starlight':
+ specifier: workspace:*
+ version: link:../../..
+ astro:
+ specifier: ^4.15.3
+ version: 4.15.3(@types/node@18.16.19)(typescript@5.4.5)
+
+ packages/starlight/__e2e__/fixtures/ssr:
+ dependencies:
+ '@astrojs/node':
+ specifier: ^8.3.2
+ version: 8.3.3(astro@4.15.3)
+ '@astrojs/starlight':
+ specifier: workspace:*
+ version: link:../../..
+ astro:
+ specifier: ^4.15.3
+ version: 4.15.3(@types/node@18.16.19)(typescript@5.4.5)
+
packages/tailwind:
dependencies:
'@astrojs/starlight':
@@ -573,6 +597,18 @@ packages:
- supports-color
dev: false
+ /@astrojs/node@8.3.3(astro@4.15.3):
+ resolution: {integrity: sha512-idrKhnnPSi0ABV+PCQsRQqVNwpOvVDF/+fkwcIiE8sr9J8EMvW9g/oyAt8T4X2OBJ8FUzYPL8klfCdG7r0eB5g==}
+ peerDependencies:
+ astro: ^4.2.0
+ dependencies:
+ astro: 4.15.3(@types/node@18.16.19)(typescript@5.4.5)
+ send: 0.18.0
+ server-destroy: 1.0.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
/@astrojs/prism@3.1.0:
resolution: {integrity: sha512-Z9IYjuXSArkAUx3N6xj6+Bnvx8OdUSHA8YoOgyepp3+zJmtVYJIl/I18GozdJVW1p5u/CNpl3Km7/gwTJK85cw==}
engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0}
@@ -2931,6 +2967,10 @@ packages:
engines: {node: '>=4'}
hasBin: true
+ /cssom@0.5.0:
+ resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==}
+ dev: true
+
/csv-generate@3.4.3:
resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==}
dev: true
@@ -2962,6 +3002,17 @@ packages:
resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==}
dev: true
+ /debug@2.6.9:
+ resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ dependencies:
+ ms: 2.0.0
+ dev: false
+
/debug@4.3.5:
resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==}
engines: {node: '>=6.0'}
@@ -3041,10 +3092,20 @@ packages:
engines: {node: '>=0.4.0'}
dev: true
+ /depd@2.0.0:
+ resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
+ engines: {node: '>= 0.8'}
+ dev: false
+
/dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
+ /destroy@1.2.0:
+ resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
+ engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
+ dev: false
+
/detect-indent@6.1.0:
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
engines: {node: '>=8'}
@@ -3140,6 +3201,10 @@ packages:
/eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+ /ee-first@1.1.1:
+ resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
+ dev: false
+
/electron-to-chromium@1.5.16:
resolution: {integrity: sha512-2gQpi2WYobXmz2q23FrOBYTLcI1O/P4heW3eqX+ldmPVDQELRqhiebV380EhlGG12NtnX1qbK/FHpN0ba+7bLA==}
@@ -3159,6 +3224,11 @@ packages:
/emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+ /encodeurl@1.0.2:
+ resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
+ engines: {node: '>= 0.8'}
+ dev: false
+
/end-of-stream@1.4.4:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
dependencies:
@@ -3288,6 +3358,10 @@ packages:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
+ /escape-html@1.0.3:
+ resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
+ dev: false
+
/escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
@@ -3343,6 +3417,11 @@ packages:
dependencies:
'@types/estree': 1.0.5
+ /etag@1.8.1:
+ resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
/event-stream@3.3.4:
resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==}
dependencies:
@@ -3534,6 +3613,11 @@ packages:
resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
dev: false
+ /fresh@0.5.2:
+ resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
/from@0.1.7:
resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==}
dev: true
@@ -4081,11 +4165,21 @@ packages:
domhandler: 5.0.3
domutils: 3.1.0
entities: 4.5.0
- dev: false
/http-cache-semantics@4.1.1:
resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
+ /http-errors@2.0.0:
+ resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
+ engines: {node: '>= 0.8'}
+ dependencies:
+ depd: 2.0.0
+ inherits: 2.0.4
+ setprototypeof: 1.2.0
+ statuses: 2.0.1
+ toidentifier: 1.0.1
+ dev: false
+
/https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
@@ -4518,6 +4612,16 @@ packages:
/lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+ /linkedom@0.18.4:
+ resolution: {integrity: sha512-JhLErxMIEOKByMi3fURXgI1fYOzR87L1Cn0+MI9GlMckFrqFZpV1SUGox1jcKtsKN3y6JgclcQf0FzZT//BuGw==}
+ dependencies:
+ css-select: 5.1.0
+ cssom: 0.5.0
+ html-escaper: 3.0.3
+ htmlparser2: 9.1.0
+ uhyphen: 0.2.0
+ dev: true
+
/lite-youtube-embed@0.3.2:
resolution: {integrity: sha512-b1dgKyF4PHhinonmr3PB172Nj0qQgA/7DE9EmeIXHR1ksnFEC2olWjNJyJGdsN2cleKHRjjsmrziKlwXtPlmLQ==}
dev: false
@@ -5202,6 +5306,12 @@ packages:
mime-db: 1.52.0
dev: true
+ /mime@1.6.0:
+ resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
+ engines: {node: '>=4'}
+ hasBin: true
+ dev: false
+
/mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
@@ -5269,6 +5379,10 @@ packages:
resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==}
engines: {node: '>=10'}
+ /ms@2.0.0:
+ resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
+ dev: false
+
/ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
@@ -5422,6 +5536,13 @@ packages:
object-keys: 1.1.1
dev: true
+ /on-finished@2.4.1:
+ resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
+ engines: {node: '>= 0.8'}
+ dependencies:
+ ee-first: 1.1.1
+ dev: false
+
/once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies:
@@ -6014,6 +6135,11 @@ packages:
engines: {node: '>=8'}
dev: true
+ /range-parser@1.2.1:
+ resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
/rc@1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
@@ -6399,10 +6525,39 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ /send@0.18.0:
+ resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
+ engines: {node: '>= 0.8.0'}
+ dependencies:
+ debug: 2.6.9
+ depd: 2.0.0
+ destroy: 1.2.0
+ encodeurl: 1.0.2
+ escape-html: 1.0.3
+ etag: 1.8.1
+ fresh: 0.5.2
+ http-errors: 2.0.0
+ mime: 1.6.0
+ ms: 2.1.3
+ on-finished: 2.4.1
+ range-parser: 1.2.1
+ statuses: 2.0.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /server-destroy@1.0.1:
+ resolution: {integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==}
+ dev: false
+
/set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
dev: true
+ /setprototypeof@1.2.0:
+ resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
+ dev: false
+
/sharp@0.32.6:
resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==}
engines: {node: '>=14.15.0'}
@@ -6667,6 +6822,11 @@ packages:
- supports-color
dev: true
+ /statuses@2.0.1:
+ resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
+ engines: {node: '>= 0.8'}
+ dev: false
+
/std-env@3.7.0:
resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==}
dev: true
@@ -6993,6 +7153,11 @@ packages:
dependencies:
is-number: 7.0.0
+ /toidentifier@1.0.1:
+ resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
+ engines: {node: '>=0.6'}
+ dev: false
+
/tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
dev: true
@@ -7113,6 +7278,10 @@ packages:
resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==}
dev: true
+ /uhyphen@0.2.0:
+ resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==}
+ dev: true
+
/ultramatter@0.0.4:
resolution: {integrity: sha512-1f/hO3mR+/Hgue4eInOF/Qm/wzDqwhYha4DxM0hre9YIUyso3fE2XtrAU6B4njLqTC8CM49EZaYgsVSa+dXHGw==}
dev: false