From 0ebc47e52dc420240c8cb724c01f98dc22bdfc60 Mon Sep 17 00:00:00 2001 From: Chris Swithinbank Date: Mon, 24 Jul 2023 18:19:07 +0200 Subject: Add unit testing (#383) --- .changeset/dry-insects-shave.md | 5 + .github/workflows/ci.yml | 22 +- CONTRIBUTING.md | 65 ++- packages/starlight/.gitignore | 5 + packages/starlight/.npmignore | 6 + packages/starlight/__tests__/basics/base.test.ts | 37 ++ packages/starlight/__tests__/basics/config.test.ts | 22 + packages/starlight/__tests__/basics/head.test.ts | 66 +++ packages/starlight/__tests__/basics/i18n.test.ts | 15 + .../__tests__/basics/localizedUrl.test.ts | 7 + .../starlight/__tests__/basics/navigation.test.ts | 229 +++++++++++ .../starlight/__tests__/basics/routing.test.ts | 47 +++ packages/starlight/__tests__/basics/slugs.test.ts | 78 ++++ .../basics/translations-with-user-config.test.ts | 17 + .../__tests__/basics/translations.test.ts | 32 ++ .../starlight/__tests__/basics/vitest.config.ts | 3 + .../__tests__/i18n-root-locale/config.test.ts | 17 + .../i18n-root-locale/localizedUrl.test.ts | 22 + .../__tests__/i18n-root-locale/routing.test.ts | 63 +++ .../__tests__/i18n-root-locale/slugs.test.ts | 78 ++++ .../__tests__/i18n-root-locale/vitest.config.ts | 10 + packages/starlight/__tests__/i18n/config.test.ts | 17 + .../starlight/__tests__/i18n/localizedUrl.test.ts | 12 + packages/starlight/__tests__/i18n/routing.test.ts | 66 +++ packages/starlight/__tests__/i18n/vitest.config.ts | 11 + .../starlight/__tests__/sidebar/navigation.test.ts | 67 ++++ .../starlight/__tests__/sidebar/vitest.config.ts | 22 + packages/starlight/__tests__/test-config.ts | 16 + packages/starlight/__tests__/test-utils.ts | 57 +++ packages/starlight/index.ts | 59 +-- .../starlight/integrations/virtual-user-config.ts | 52 +++ packages/starlight/package.json | 9 +- packages/starlight/types.ts | 2 +- packages/starlight/utils/slugs.ts | 2 +- packages/starlight/vitest.config.ts | 31 ++ packages/starlight/vitest.workspace.ts | 1 + pnpm-lock.yaml | 442 ++++++++++++++++++++- 37 files changed, 1637 insertions(+), 75 deletions(-) create mode 100644 .changeset/dry-insects-shave.md create mode 100644 packages/starlight/.gitignore create mode 100644 packages/starlight/.npmignore create mode 100644 packages/starlight/__tests__/basics/base.test.ts create mode 100644 packages/starlight/__tests__/basics/config.test.ts create mode 100644 packages/starlight/__tests__/basics/head.test.ts create mode 100644 packages/starlight/__tests__/basics/i18n.test.ts create mode 100644 packages/starlight/__tests__/basics/localizedUrl.test.ts create mode 100644 packages/starlight/__tests__/basics/navigation.test.ts create mode 100644 packages/starlight/__tests__/basics/routing.test.ts create mode 100644 packages/starlight/__tests__/basics/slugs.test.ts create mode 100644 packages/starlight/__tests__/basics/translations-with-user-config.test.ts create mode 100644 packages/starlight/__tests__/basics/translations.test.ts create mode 100644 packages/starlight/__tests__/basics/vitest.config.ts create mode 100644 packages/starlight/__tests__/i18n-root-locale/config.test.ts create mode 100644 packages/starlight/__tests__/i18n-root-locale/localizedUrl.test.ts create mode 100644 packages/starlight/__tests__/i18n-root-locale/routing.test.ts create mode 100644 packages/starlight/__tests__/i18n-root-locale/slugs.test.ts create mode 100644 packages/starlight/__tests__/i18n-root-locale/vitest.config.ts create mode 100644 packages/starlight/__tests__/i18n/config.test.ts create mode 100644 packages/starlight/__tests__/i18n/localizedUrl.test.ts create mode 100644 packages/starlight/__tests__/i18n/routing.test.ts create mode 100644 packages/starlight/__tests__/i18n/vitest.config.ts create mode 100644 packages/starlight/__tests__/sidebar/navigation.test.ts create mode 100644 packages/starlight/__tests__/sidebar/vitest.config.ts create mode 100644 packages/starlight/__tests__/test-config.ts create mode 100644 packages/starlight/__tests__/test-utils.ts create mode 100644 packages/starlight/integrations/virtual-user-config.ts create mode 100644 packages/starlight/vitest.config.ts create mode 100644 packages/starlight/vitest.workspace.ts diff --git a/.changeset/dry-insects-shave.md b/.changeset/dry-insects-shave.md new file mode 100644 index 00000000..61e6b3bf --- /dev/null +++ b/.changeset/dry-insects-shave.md @@ -0,0 +1,5 @@ +--- +"@astrojs/starlight": patch +--- + +Fix edge case where index files in an index directory would end up with the wrong slug diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f3e206b..41900459 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,25 @@ concurrency: group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.ref }} cancel-in-progress: true +env: + NODE_VERSION: 16 + jobs: + unit-test: + name: Run unit tests + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2 + - uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'pnpm' + - run: pnpm i + - name: Test packages/starlight + working-directory: ./packages/starlight + run: pnpm test:coverage + pa11y: name: Check for accessibility issues runs-on: ubuntu-20.04 @@ -26,7 +44,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: 16 + node-version: ${{ env.NODE_VERSION }} cache: 'pnpm' - name: Install Dependencies @@ -50,7 +68,7 @@ jobs: - uses: pnpm/action-setup@v2 - uses: actions/setup-node@v3 with: - node-version: 16 + node-version: ${{ env.NODE_VERSION }} cache: 'pnpm' - run: pnpm i - name: Build docs site diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e3852b52..0cc41f03 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,7 +76,9 @@ Instead of working locally on your machine, you can also contribute using an onl pnpm i ``` -### Testing changes while you work +## Testing + +### Testing visual changes while you work Run the Astro dev server on the docs site to see how changes you make impact a project using Starlight. @@ -89,6 +91,66 @@ pnpm dev You should then be able to open and see your changes. +> **Note** +> Changes to the Starlight integration will require you to quit and restart the dev server to take effect. + +### Unit tests + +The Starlight package includes unit tests in [`packages/starlight/__tests__/`](./packages/starlight/__tests__/), which are run using [Vitest][vitest]. + +To run tests, move into the Starlight package and then run `pnpm test`: + +```sh +cd packages/starlight +pnpm test +``` + +This will run tests and then listen for changes, re-running tests when files change. + +#### Test environments + +A lot of Starlight code relies on Vite virtual modules provided either by Astro or by Starlight itself. Each subdirectory of `packages/starlight/__tests__/` should contain a `vitest.config.ts` file that uses the `defineVitestConfig()` helper to define a valid test environment for tests in that directory. This helper takes a single argument, which provides a Starlight user config object: + +```ts +// packages/starlight/__tests/basics/vitest.config.ts +import { defineVitestConfig } from '../test-config'; + +export default defineVitestConfig({ + title: 'Basics', +}); +``` + +This allows you to run tests of Starlight code against different combinations of Starlight configuration options. + +#### Mocking content collections + +Starlight relies on a user’s `docs` and (optional) `i18n` content collections, which aren’t available during testing. You can use a top-level `vi.mock()` call and the `mockedAstroContent` helper to set up fake collection entries for the current test file: + +```js +import { describe, expect, test, vi } from 'vitest'; + +vi.mock('astro:content', async () => + (await import('../test-utils')).mockedAstroContent({ + docs: [ + ['index.mdx', { title: 'Home Page' }], + ['environmental-impact.md', { title: 'Eco-friendly docs' }], + ], + i18n: [['en', { 'page.editLink': 'Modify this doc!' }]], + }) +); +``` + +#### Test coverage + +To see how much of Starlight’s code is currently being tested, run `pnpm test:coverage` from the Starlight package: + +```sh +cd packages/starlight +pnpm test:coverage +``` + +This will print a table to your terminal and also generate an HTML report you can load in a web browser by opening [`packages/starlight/__coverage__/index.html`](./packages/starlight/__coverage__/index.html). + ## Translations Translations help make Starlight accessible to more people. @@ -139,3 +201,4 @@ Visit **** to track translation progress for [pr-docs]: https://docs.github.com/en/get-started/quickstart/contributing-to-projects#making-a-pull-request [gfi]: https://github.com/withastro/starlight/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+ [api-docs]: https://docs.astro.build/en/reference/integrations-reference/ +[vitest]: https://vitest.dev/ diff --git a/packages/starlight/.gitignore b/packages/starlight/.gitignore new file mode 100644 index 00000000..01736279 --- /dev/null +++ b/packages/starlight/.gitignore @@ -0,0 +1,5 @@ +# Vitest +__coverage__/ + +# Astro generates this during tests, but we want to ignore it. +src/env.d.ts diff --git a/packages/starlight/.npmignore b/packages/starlight/.npmignore new file mode 100644 index 00000000..26babf74 --- /dev/null +++ b/packages/starlight/.npmignore @@ -0,0 +1,6 @@ +# Vitest +__coverage__/ +__tests__/ +vitest.* +# Astro generates this during tests, but we want to ignore it. +src/env.d.ts diff --git a/packages/starlight/__tests__/basics/base.test.ts b/packages/starlight/__tests__/basics/base.test.ts new file mode 100644 index 00000000..feed73e6 --- /dev/null +++ b/packages/starlight/__tests__/basics/base.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, test, vi } from 'vitest'; +import { fileWithBase, pathWithBase } from '../../utils/base'; + +describe('fileWithBase()', () => { + describe('with no base', () => { + test('does not prepend anything', () => { + expect(fileWithBase('/img.svg')).toBe('/img.svg'); + }); + test('adds leading slash if needed', () => { + expect(fileWithBase('img.svg')).toBe('/img.svg'); + }); + }); + + // TODO: Stubbing BASE_URL is not currently possible. + // Astro controls BASE_URL via its `vite-plugin-env`, which prevents Vitest’s stubbing from + // working and there’s also no way to pass in Astro config in Astro’s `getViteConfig` helper. + describe.todo('with base', () => { + test('prepends base', () => { + vi.stubEnv('BASE_URL', '/base/'); + expect(fileWithBase('/img.svg')).toBe('/base/img.svg'); + vi.unstubAllEnvs(); + }); + }); +}); + +describe('pathWithBase()', () => { + describe('with no base', () => { + test('does not prepend anything', () => { + expect(pathWithBase('/path/')).toBe('/path/'); + }); + test('adds leading and trailing slashes if needed', () => { + expect(pathWithBase('path')).toBe('/path/'); + }); + }); + + describe.todo('with base'); +}); diff --git a/packages/starlight/__tests__/basics/config.test.ts b/packages/starlight/__tests__/basics/config.test.ts new file mode 100644 index 00000000..b31d21ed --- /dev/null +++ b/packages/starlight/__tests__/basics/config.test.ts @@ -0,0 +1,22 @@ +import config from 'virtual:starlight/user-config'; +import { expect, test } from 'vitest'; + +test('test suite is using correct env', () => { + expect(config.title).toBe('Basics'); +}); + +test('isMultilingual is false when no locales configured ', () => { + expect(config.locales).toBeUndefined(); + expect(config.isMultilingual).toBe(false); +}); + +test('default locale is set when no locales configured', () => { + expect(config.defaultLocale).not.toBeUndefined(); + expect(config.defaultLocale.lang).toBe('en'); + expect(config.defaultLocale.label).toBe('English'); + expect(config.defaultLocale.dir).toBe('ltr'); +}); + +test('lastUpdated defaults to false', () => { + expect(config.lastUpdated).toBe(false); +}); diff --git a/packages/starlight/__tests__/basics/head.test.ts b/packages/starlight/__tests__/basics/head.test.ts new file mode 100644 index 00000000..2a189566 --- /dev/null +++ b/packages/starlight/__tests__/basics/head.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, test } from 'vitest'; +import { createHead } from '../../utils/head'; + +describe('createHead', () => { + test('merges two tags', () => { + expect( + createHead( + [{ tag: 'title', content: 'Default' }], + [{ tag: 'title', content: 'Override', attrs: {} }] + ) + ).toEqual([{ tag: 'title', content: 'Override', attrs: {} }]); + }); + + for (const prop of ['name', 'property', 'http-equiv']) { + test(`merges two <meta> tags with same ${prop} value`, () => { + expect( + createHead( + [{ tag: 'meta', attrs: { [prop]: 'x', content: 'Default' } }], + [{ tag: 'meta', attrs: { [prop]: 'x', content: 'Test' }, content: '' }] + ) + ).toEqual([{ tag: 'meta', content: '', attrs: { [prop]: 'x', content: 'Test' } }]); + }); + } + + for (const prop of ['name', 'property', 'http-equiv']) { + test(`does not merge <meta> tags with different ${prop} values`, () => { + expect( + createHead( + [{ tag: 'meta', attrs: { [prop]: 'x', content: 'X' } }], + [{ tag: 'meta', attrs: { [prop]: 'y', content: 'Y' }, content: '' }] + ) + ).toEqual([ + { tag: 'meta', content: '', attrs: { [prop]: 'x', content: 'X' } }, + { tag: 'meta', content: '', attrs: { [prop]: 'y', content: 'Y' } }, + ]); + }); + } + + test('sorts head by tag importance', () => { + expect( + createHead([ + // SEO meta tags + { tag: 'meta', attrs: { name: 'x' } }, + // Others + { tag: 'link', attrs: { rel: 'stylesheet' } }, + // Important meta tags + { tag: 'meta', attrs: { charset: 'utf-8' } }, + { tag: 'meta', attrs: { name: 'viewport' } }, + { tag: 'meta', attrs: { 'http-equiv': 'x' } }, + // <title> + { tag: 'title', content: 'Title' }, + ]) + ).toEqual([ + // Important meta tags + { tag: 'meta', attrs: { charset: 'utf-8' }, content: '' }, + { tag: 'meta', attrs: { name: 'viewport' }, content: '' }, + { tag: 'meta', attrs: { 'http-equiv': 'x' }, content: '' }, + // <title> + { tag: 'title', attrs: {}, content: 'Title' }, + // Others + { tag: 'link', attrs: { rel: 'stylesheet' }, content: '' }, + // SEO meta tags + { tag: 'meta', attrs: { name: 'x' }, content: '' }, + ]); + }); +}); diff --git a/packages/starlight/__tests__/basics/i18n.test.ts b/packages/starlight/__tests__/basics/i18n.test.ts new file mode 100644 index 00000000..1bed4644 --- /dev/null +++ b/packages/starlight/__tests__/basics/i18n.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from 'vitest'; +import { pickLang } from '../../utils/i18n'; + +describe('pickLang', () => { + const dictionary = { en: 'Hello', fr: 'Bonjour' }; + + test('returns the requested language string', () => { + expect(pickLang(dictionary, 'en')).toBe('Hello'); + expect(pickLang(dictionary, 'fr')).toBe('Bonjour'); + }); + + test('returns undefined for unknown languages', () => { + expect(pickLang(dictionary, 'ar' as any)).toBeUndefined(); + }); +}); diff --git a/packages/starlight/__tests__/basics/localizedUrl.test.ts b/packages/starlight/__tests__/basics/localizedUrl.test.ts new file mode 100644 index 00000000..fa8586d5 --- /dev/null +++ b/packages/starlight/__tests__/basics/localizedUrl.test.ts @@ -0,0 +1,7 @@ +import { expect, test } from 'vitest'; +import { localizedUrl } from '../../utils/localizedUrl'; + +test('it has no effect in a monolingual project', () => { + const url = new URL('https://example.com/en/guide/'); + expect(localizedUrl(url, undefined).href).toBe(url.href); +}); diff --git a/packages/starlight/__tests__/basics/navigation.test.ts b/packages/starlight/__tests__/basics/navigation.test.ts new file mode 100644 index 00000000..56ca5a02 --- /dev/null +++ b/packages/starlight/__tests__/basics/navigation.test.ts @@ -0,0 +1,229 @@ +import { describe, expect, test, vi } from 'vitest'; +import { flattenSidebar, getPrevNextLinks, getSidebar } from '../../utils/navigation'; + +vi.mock('astro:content', async () => + (await import('../test-utils')).mockedAstroContent({ + docs: [ + ['index.mdx', { title: 'Home Page' }], + ['environmental-impact.md', { title: 'Eco-friendly docs' }], + ['guides/authoring-content.md', { title: 'Authoring Markdown' }], + ['guides/components.mdx', { title: 'Components' }], + ], + }) +); + +describe('getSidebar', () => { + test('returns an array of sidebar entries', () => { + expect(getSidebar('/', undefined)).toMatchInlineSnapshot(` + [ + { + "href": "/", + "isCurrent": true, + "label": "Home Page", + "type": "link", + }, + { + "href": "/environmental-impact/", + "isCurrent": false, + "label": "Eco-friendly docs", + "type": "link", + }, + { + "collapsed": false, + "entries": [ + { + "href": "/guides/authoring-content/", + "isCurrent": false, + "label": "Authoring Markdown", + "type": "link", + }, + { + "href": "/guides/components/", + "isCurrent": false, + "label": "Components", + "type": "link", + }, + ], + "label": "guides", + "type": "group", + }, + ] + `); + }); + + test('marks current path with isCurrent', () => { + const paths = ['/', '/environmental-impact/', '/guides/authoring-content/']; + for (const currentPath of paths) { + const items = flattenSidebar(getSidebar(currentPath, undefined)); + const currentItems = items.filter((item) => item.type === 'link' && item.isCurrent); + expect(currentItems).toHaveLength(1); + const currentItem = currentItems[0]; + if (currentItem?.type !== 'link') throw new Error('Expected current item to be link'); + expect(currentItem.href).toBe(currentPath); + } + }); + + test('nests files in subdirectory in group when autogenerating', () => { + const sidebar = getSidebar('/', undefined); + expect(sidebar.every((item) => item.type === 'group' || !item.href.startsWith('/guides/'))); + const guides = sidebar.find((item) => item.type === 'group' && item.label === 'guides'); + expect(guides?.type).toBe('group'); + // @ts-expect-error — TypeScript doesn’t know we know we’re in a group. + expect(guides.entries).toHaveLength(2); + }); + + test('uses page title as label when autogenerating', () => { + const sidebar = getSidebar('/', undefined); + const homeLink = sidebar.find((item) => item.type === 'link' && item.href === '/'); + expect(homeLink?.label).toBe('Home Page'); + }); +}); + +describe('flattenSidebar', () => { + test('flattens nested sidebar array', () => { + const sidebar = getSidebar('/', undefined); + const flattened = flattenSidebar(sidebar); + // Sidebar should include some nested group items. + expect(sidebar.some((item) => item.type === 'group')).toBe(true); + // Flattened sidebar should only include link items. + expect(flattened.every((item) => item.type === 'link')).toBe(true); + + expect(flattened).toMatchInlineSnapshot(` + [ + { + "href": "/", + "isCurrent": true, + "label": "Home Page", + "type": "link", + }, + { + "href": "/environmental-impact/", + "isCurrent": false, + "label": "Eco-friendly docs", + "type": "link", + }, + { + "href": "/guides/authoring-content/", + "isCurrent": false, + "label": "Authoring Markdown", + "type": "link", + }, + { + "href": "/guides/components/", + "isCurrent": false, + "label": "Components", + "type": "link", + }, + ] + `); + }); +}); + +describe('getPrevNextLinks', () => { + test('returns stable previous/next values', () => { + const sidebar = getSidebar('/environmental-impact/', undefined); + const links = getPrevNextLinks(sidebar, true, {}); + expect(links).toMatchInlineSnapshot(` + { + "next": { + "href": "/guides/authoring-content/", + "isCurrent": false, + "label": "Authoring Markdown", + "type": "link", + }, + "prev": { + "href": "/", + "isCurrent": false, + "label": "Home Page", + "type": "link", + }, + } + `); + }); + + test('returns no links when pagination is disabled', () => { + const sidebar = getSidebar('/environmental-impact/', undefined); + const links = getPrevNextLinks(sidebar, false, {}); + expect(links).toEqual({ prev: undefined, next: undefined }); + }); + + test('returns no previous link for first item', () => { + const sidebar = getSidebar('/', undefined); + const links = getPrevNextLinks(sidebar, true, {}); + expect(links.prev).toBeUndefined(); + }); + + test('returns no next link for last item', () => { + const sidebar = getSidebar('/guides/components/', undefined); + const links = getPrevNextLinks(sidebar, true, {}); + expect(links.next).toBeUndefined(); + }); + + test('final parameter can disable prev/next', () => { + const sidebar = getSidebar('/environmental-impact/', undefined); + expect(getPrevNextLinks(sidebar, true, { prev: true }).prev).toBeDefined(); + expect(getPrevNextLinks(sidebar, true, { prev: false }).prev).toBeUndefined(); + expect(getPrevNextLinks(sidebar, true, { next: true }).next).toBeDefined(); + expect(getPrevNextLinks(sidebar, true, { next: false }).next).toBeUndefined(); + }); + + test('final parameter can set custom link label with string', () => { + const sidebar = getSidebar('/environmental-impact/', undefined); + const withDefaultLabels = getPrevNextLinks(sidebar, true, {}); + const withCustomLabels = getPrevNextLinks(sidebar, true, { prev: 'x', next: 'y' }); + expect(withCustomLabels.prev?.label).toBe('x'); + expect(withCustomLabels.prev?.label).not.toBe(withDefaultLabels.prev?.label); + expect(withCustomLabels.next?.label).toBe('y'); + expect(withCustomLabels.next?.label).not.toBe(withDefaultLabels.next?.label); + }); + + test('final parameter can set custom link label with object', () => { + const sidebar = getSidebar('/environmental-impact/', undefined); + const withDefaultLabels = getPrevNextLinks(sidebar, true, {}); + const withCustomLabels = getPrevNextLinks(sidebar, true, { + prev: { label: 'x' }, + next: { label: 'y' }, + }); + expect(withCustomLabels.prev?.label).toBe('x'); + expect(withCustomLabels.prev?.label).not.toBe(withDefaultLabels.prev?.label); + expect(withCustomLabels.next?.label).toBe('y'); + expect(withCustomLabels.next?.label).not.toBe(withDefaultLabels.next?.label); + }); + + test('final parameter can set custom link destination', () => { + const sidebar = getSidebar('/environmental-impact/', undefined); + const withDefaults = getPrevNextLinks(sidebar, true, {}); + const withCustomLinks = getPrevNextLinks(sidebar, true, { + prev: { link: '/x' }, + next: { link: '/y' }, + }); + expect(withCustomLinks.prev?.href).toBe('/x'); + expect(withCustomLinks.prev?.href).not.toBe(withDefaults.prev?.href); + expect(withCustomLinks.prev?.label).toBe(withDefaults.prev?.label); + expect(withCustomLinks.next?.href).toBe('/y'); + expect(withCustomLinks.next?.href).not.toBe(withDefaults.next?.href); + expect(withCustomLinks.next?.label).toBe(withDefaults.next?.label); + }); + + test('final parameter can set custom link even if no default link existed', () => { + const sidebar = getSidebar('/', undefined); + const withDefaults = getPrevNextLinks(sidebar, true, {}); + const withCustomLinks = getPrevNextLinks(sidebar, true, { + prev: { link: 'x', label: 'X' }, + }); + expect(withDefaults.prev).toBeUndefined(); + expect(withCustomLinks.prev).toEqual({ + type: 'link', + href: '/x/', + label: 'X', + isCurrent: false, + }); + }); + + test('final parameter can override global pagination toggle', () => { + const sidebar = getSidebar('/environmental-impact/', undefined); + const withDefaults = getPrevNextLinks(sidebar, true, {}); + const withOverrides = getPrevNextLinks(sidebar, false, { prev: true, next: true }); + expect(withOverrides).toEqual(withDefaults); + }); +}); diff --git a/packages/starlight/__tests__/basics/routing.test.ts b/packages/starlight/__tests__/basics/routing.test.ts new file mode 100644 index 00000000..18fc0605 --- /dev/null +++ b/packages/starlight/__tests__/basics/routing.test.ts @@ -0,0 +1,47 @@ +import { getCollection } from 'astro:content'; +import config from 'virtual:starlight/user-config'; +import { expect, test, vi } from 'vitest'; +import { routes } from '../../utils/routing'; + +vi.mock('astro:content', async () => + (await import('../test-utils')).mockedAstroContent({ + docs: [ + ['404.md', { title: 'Not found' }], + ['index.mdx', { title: 'Home page' }], + ['guides/authoring-content.md', { title: 'Authoring content' }], + ], + }) +); + +test('test suite is using correct env', () => { + expect(config.title).toBe('Basics'); +}); + +test('route slugs are normalized', () => { + const indexRoute = routes.find((route) => route.id.startsWith('index.md')); + expect(indexRoute?.slug).toBe(''); +}); + +test('routes are sorted by slug', () => { + expect(routes[0]?.slug).toBe(''); +}); + +test('routes contain copy of original doc as entry', async () => { + const docs = await getCollection('docs'); + for (const route of routes) { + const doc = docs.find((doc) => doc.id === route.id); + if (!doc) throw new Error('Expected to find doc for route ' + route.id); + // Compare without slug as slugs can be normalized. + const { slug: _, ...entry } = route.entry; + const { slug: __, ...input } = doc; + expect(entry).toEqual(input); + } +}); + +test('routes have locale data added', () => { + for (const route of routes) { + expect(route.lang).toBe('en'); + expect(route.dir).toBe('ltr'); + expect(route.locale).toBeUndefined(); + } +}); diff --git a/packages/starlight/__tests__/basics/slugs.test.ts b/packages/starlight/__tests__/basics/slugs.test.ts new file mode 100644 index 00000000..86ee432b --- /dev/null +++ b/packages/starlight/__tests__/basics/slugs.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, test } from 'vitest'; +import { + localeToLang, + localizedId, + localizedSlug, + slugToLocaleData, + slugToParam, + slugToPathname, +} from '../../utils/slugs'; + +describe('slugToLocaleData', () => { + test('returns an undefined locale for root locale slugs', () => { + expect(slugToLocaleData('test').locale).toBeUndefined(); + expect(slugToLocaleData('dir/test').locale).toBeUndefined(); + }); + test('returns default "en" lang when no locale config is set', () => { + expect(slugToLocaleData('test').lang).toBe('en'); + expect(slugToLocaleData('dir/test').lang).toBe('en'); + }); + test('returns default "ltr" dir when no locale config is set', () => { + expect(slugToLocaleData('test').dir).toBe('ltr'); + expect(slugToLocaleData('dir/test').dir).toBe('ltr'); + }); +}); + +describe('slugToParam', () => { + test('returns undefined for empty slug (index)', () => { + expect(slugToParam('')).toBeUndefined(); + }); + test('returns undefined for root index', () => { + expect(slugToParam('index')).toBeUndefined(); + }); + test('strips index from end of nested slug', () => { + expect(slugToParam('dir/index')).toBe('dir'); + expect(slugToParam('dir/index/sub-dir/index')).toBe('dir/index/sub-dir'); + }); + test('returns other slugs unchanged', () => { + expect(slugToParam('slug')).toBe('slug'); + expect(slugToParam('dir/page')).toBe('dir/page'); + expect(slugToParam('dir/sub-dir/page')).toBe('dir/sub-dir/page'); + }); +}); + +describe('slugToPathname', () => { + test('returns "/" for empty slug', () => { + expect(slugToPathname('')).toBe('/'); + }); + test('returns "/" for root index', () => { + expect(slugToPathname('index')).toBe('/'); + }); + test('strips index from end of nested slug', () => { + expect(slugToPathname('dir/index')).toBe('/dir/'); + expect(slugToPathname('dir/index/sub-dir/index')).toBe('/dir/index/sub-dir/'); + }); + test('returns slugs with leading and trailing slashes added', () => { + expect(slugToPathname('slug')).toBe('/slug/'); + expect(slugToPathname('dir/page')).toBe('/dir/page/'); + expect(slugToPathname('dir/sub-dir/page')).toBe('/dir/sub-dir/page/'); + }); +}); + +describe('localeToLang', () => { + test('returns lang for root locale', () => { + expect(localeToLang(undefined)).toBe('en'); + }); +}); + +describe('localizedId', () => { + test('returns unchanged when no locales are set', () => { + expect(localizedId('test.md', undefined)).toBe('test.md'); + }); +}); + +describe('localizedSlug', () => { + test('returns unchanged when no locales are set', () => { + expect(localizedSlug('test', undefined)).toBe('test'); + }); +}); diff --git a/packages/starlight/__tests__/basics/translations-with-user-config.test.ts b/packages/starlight/__tests__/basics/translations-with-user-config.test.ts new file mode 100644 index 00000000..4f76a67d --- /dev/null +++ b/packages/starlight/__tests__/basics/translations-with-user-config.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, test, vi } from 'vitest'; +import translations from '../../translations'; +import { useTranslations } from '../../utils/translations'; + +vi.mock('astro:content', async () => + (await import('../test-utils')).mockedAstroContent({ + i18n: [['en', { 'page.editLink': 'Modify this doc!' }]], + }) +); + +describe('useTranslations()', () => { + test('uses user-defined translations', () => { + const t = useTranslations(undefined); + expect(t('page.editLink')).toBe('Modify this doc!'); + expect(t('page.editLink')).not.toBe(translations.en?.['page.editLink']); + }); +}); diff --git a/packages/starlight/__tests__/basics/translations.test.ts b/packages/starlight/__tests__/basics/translations.test.ts new file mode 100644 index 00000000..7336cbf3 --- /dev/null +++ b/packages/starlight/__tests__/basics/translations.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test, vi } from 'vitest'; +import translations from '../../translations'; +import { useTranslations } from '../../utils/translations'; + +describe('built-in translations', () => { + test('includes English', () => { + expect(translations).toHaveProperty('en'); + }); +}); + +describe('useTranslations()', () => { + test('works when no i18n collection is available', () => { + const t = useTranslations(undefined); + expect(t).toBeTypeOf('function'); + expect(t('page.editLink')).toBe(translations.en?.['page.editLink']); + }); + + test('returns default locale for unknown language', () => { + const locale = 'xx'; + expect(translations).not.toHaveProperty(locale); + const t = useTranslations(locale); + expect(t('page.editLink')).toBe(translations.en?.['page.editLink']); + }); + + test('returns a pick method for filtering by key', () => { + const t = useTranslations('en'); + expect(t.pick('tableOfContents.')).toEqual({ + 'tableOfContents.onThisPage': 'On this page', + 'tableOfContents.overview': 'Overview', + }); + }); +}); diff --git a/packages/starlight/__tests__/basics/vitest.config.ts b/packages/starlight/__tests__/basics/vitest.config.ts new file mode 100644 index 00000000..ec3c32e5 --- /dev/null +++ b/packages/starlight/__tests__/basics/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineVitestConfig } from '../test-config'; + +export default defineVitestConfig({ title: 'Basics' }); diff --git a/packages/starlight/__tests__/i18n-root-locale/config.test.ts b/packages/starlight/__tests__/i18n-root-locale/config.test.ts new file mode 100644 index 00000000..012c0a4e --- /dev/null +++ b/packages/starlight/__tests__/i18n-root-locale/config.test.ts @@ -0,0 +1,17 @@ +import config from 'virtual:starlight/user-config'; +import { expect, test } from 'vitest'; + +test('test suite is using correct env', () => { + expect(config.title).toBe('i18n with root locale'); +}); + +test('config.isMultilingual is true with multiple locales', () => { + expect(config.isMultilingual).toBe(true); + expect(config.locales).keys('root', 'en', 'ar'); +}); + +test('config.defaultLocale is populated from root locale', () => { + expect(config.defaultLocale.lang).toBe('fr'); + expect(config.defaultLocale.dir).toBe('ltr'); + expect(config.defaultLocale.locale).toBeUndefined(); +}); diff --git a/packages/starlight/__tests__/i18n-root-locale/localizedUrl.test.ts b/packages/starlight/__tests__/i18n-root-locale/localizedUrl.test.ts new file mode 100644 index 00000000..0f062222 --- /dev/null +++ b/packages/starlight/__tests__/i18n-root-locale/localizedUrl.test.ts @@ -0,0 +1,22 @@ +import { expect, test } from 'vitest'; +import { localizedUrl } from '../../utils/localizedUrl'; + +test('it has no effect if locale matches', () => { + const url = new URL('https://example.com/en/guide/'); + expect(localizedUrl(url, 'en').href).toBe(url.href); +}); + +test('it changes locale to requested locale', () => { + const url = new URL('https://example.com/en/guide/'); + expect(localizedUrl(url, 'ar').href).toBe('https://example.com/ar/guide/'); +}); + +test('it can change to root locale', () => { + const url = new URL('https://example.com/en/guide/'); + expect(localizedUrl(url, undefined).href).toBe('https://example.com/guide/'); +}); + +test('it can change from root locale', () => { + const url = new URL('https://example.com/guide/'); + expect(localizedUrl(url, 'en').href).toBe('https://example.com/en/guide/'); +}); diff --git a/packages/starlight/__tests__/i18n-root-locale/routing.test.ts b/packages/starlight/__tests__/i18n-root-locale/routing.test.ts new file mode 100644 index 00000000..4d5a3c1c --- /dev/null +++ b/packages/starlight/__tests__/i18n-root-locale/routing.test.ts @@ -0,0 +1,63 @@ +import config from 'virtual:starlight/user-config'; +import { expect, test, vi } from 'vitest'; +import { routes } from '../../utils/routing'; + +vi.mock('astro:content', async () => + (await import('../test-utils')).mockedAstroContent({ + docs: [ + ['404.md', { title: 'Page introuvable' }], + ['index.mdx', { title: 'Accueil' }], + // @ts-expect-error — Using a slug not present in Starlight docs site + ['en/index.mdx', { title: 'Home page' }], + // @ts-expect-error — Using a slug not present in Starlight docs site + ['ar/index.mdx', { title: 'الصفحة الرئيسية' }], + ['guides/authoring-content.md', { title: 'Création de contenu en Markdown' }], + ], + }) +); + +test('test suite is using correct env', () => { + expect(config.title).toBe('i18n with root locale'); +}); + +test('routes includes fallback entries for untranslated pages', () => { + const numLocales = config.isMultilingual ? Object.keys(config.locales).length : 1; + const guides = routes.filter((route) => route.id.includes('guides/')); + expect(guides).toHaveLength(numLocales); +}); + +test('routes have locale data added', () => { + for (const { id, lang, dir, locale } of routes) { + if (id.startsWith('en')) { + expect(lang).toBe('en-US'); + expect(dir).toBe('ltr'); + expect(locale).toBe('en'); + } else if (id.startsWith('ar')) { + expect(lang).toBe('ar'); + expect(dir).toBe('rtl'); + expect(locale).toBe('ar'); + } else { + expect(lang).toBe('fr'); + expect(dir).toBe('ltr'); + expect(locale).toBeUndefined(); + } + } +}); + +test('fallback routes have fallback locale data in entryMeta', () => { + const fallbacks = routes.filter((route) => route.isFallback); + expect(fallbacks.length).toBeGreaterThan(0); + for (const route of fallbacks) { + expect(route.entryMeta.locale).toBeUndefined(); + expect(route.entryMeta.locale).not.toBe(route.locale); + expect(route.entryMeta.lang).toBe('fr'); + expect(route.entryMeta.lang).not.toBe(route.lang); + } +}); + +test('fallback routes use their own locale data', () => { + const enGuide = routes.find((route) => route.id === 'en/guides/authoring-content.md'); + if (!enGuide) throw new Error('Expected to find English fallback route for authoring-content.md'); + expect(enGuide.locale).toBe('en'); + expect(enGuide.lang).toBe('en-US'); +}); diff --git a/packages/starlight/__tests__/i18n-root-locale/slugs.test.ts b/packages/starlight/__tests__/i18n-root-locale/slugs.test.ts new file mode 100644 index 00000000..2614712a --- /dev/null +++ b/packages/starlight/__tests__/i18n-root-locale/slugs.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, test } from 'vitest'; +import { localeToLang, localizedId, localizedSlug, slugToLocaleData } from '../../utils/slugs'; + +describe('slugToLocaleData', () => { + test('returns an undefined locale for root locale slugs', () => { + expect(slugToLocaleData('test').locale).toBeUndefined(); + expect(slugToLocaleData('dir/test').locale).toBeUndefined(); + }); + test('returns a locale for localized slugs', () => { + expect(slugToLocaleData('en/test').locale).toBe('en'); + expect(slugToLocaleData('ar/test').locale).toBe('ar'); + }); + test('returns default locale lang for root locale slugs', () => { + expect(slugToLocaleData('test').lang).toBe('fr'); + expect(slugToLocaleData('dir/test').lang).toBe('fr'); + }); + test('returns langs for localized slugs', () => { + expect(slugToLocaleData('ar/test').lang).toBe('ar'); + expect(slugToLocaleData('en/dir/test').lang).toBe('en-US'); + }); + test('returns default locale dir for root locale slugs', () => { + expect(slugToLocaleData('test').dir).toBe('ltr'); + expect(slugToLocaleData('dir/test').dir).toBe('ltr'); + }); + test('returns configured dir for localized slugs', () => { + expect(slugToLocaleData('ar/test').dir).toBe('rtl'); + expect(slugToLocaleData('en/dir/test').dir).toBe('ltr'); + }); +}); + +describe('localeToLang', () => { + test('returns lang for root locale', () => { + expect(localeToLang(undefined)).toBe('fr'); + }); + test('returns lang for non-root locales', () => { + expect(localeToLang('en')).toBe('en-US'); + expect(localeToLang('ar')).toBe('ar'); + }); +}); + +describe('localizedId', () => { + test('returns unchanged when already in requested locale', () => { + expect(localizedId('test.md', undefined)).toBe('test.md'); + expect(localizedId('dir/test.md', undefined)).toBe('dir/test.md'); + expect(localizedId('en/test.md', 'en')).toBe('en/test.md'); + expect(localizedId('en/dir/test.md', 'en')).toBe('en/dir/test.md'); + expect(localizedId('ar/test.md', 'ar')).toBe('ar/test.md'); + expect(localizedId('ar/dir/test.md', 'ar')).toBe('ar/dir/test.md'); + }); + test('returns localized id for requested locale', () => { + expect(localizedId('test.md', 'en')).toBe('en/test.md'); + expect(localizedId('dir/test.md', 'en')).toBe('en/dir/test.md'); + expect(localizedId('en/test.md', 'ar')).toBe('ar/test.md'); + expect(localizedId('en/test.md', undefined)).toBe('test.md'); + }); +}); + +describe('localizedSlug', () => { + test('returns unchanged when already in requested locale', () => { + expect(localizedSlug('', undefined)).toBe(''); + expect(localizedSlug('test', undefined)).toBe('test'); + expect(localizedSlug('dir/test', undefined)).toBe('dir/test'); + expect(localizedSlug('en', 'en')).toBe('en'); + expect(localizedSlug('en/test', 'en')).toBe('en/test'); + expect(localizedSlug('en/dir/test', 'en')).toBe('en/dir/test'); + }); + test('returns localized slug for requested locale', () => { + expect(localizedSlug('', 'en')).toBe('en'); + expect(localizedSlug('test', 'en')).toBe('en/test'); + expect(localizedSlug('dir/test', 'en')).toBe('en/dir/test'); + expect(localizedSlug('en', undefined)).toBe(''); + expect(localizedSlug('en/test', undefined)).toBe('test'); + expect(localizedSlug('en/dir/test', undefined)).toBe('dir/test'); + expect(localizedSlug('en', 'ar')).toBe('ar'); + expect(localizedSlug('en/test', 'ar')).toBe('ar/test'); + expect(localizedSlug('en/dir/test', 'ar')).toBe('ar/dir/test'); + }); +}); diff --git a/packages/starlight/__tests__/i18n-root-locale/vitest.config.ts b/packages/starlight/__tests__/i18n-root-locale/vitest.config.ts new file mode 100644 index 00000000..c9f51421 --- /dev/null +++ b/packages/starlight/__tests__/i18n-root-locale/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineVitestConfig } from '../test-config'; + +export default defineVitestConfig({ + title: 'i18n with root locale', + locales: { + root: { label: 'French', lang: 'fr' }, + en: { label: 'English', lang: 'en-US' }, + ar: { label: 'Arabic', dir: 'rtl' }, + }, +}); diff --git a/packages/starlight/__tests__/i18n/config.test.ts b/packages/starlight/__tests__/i18n/config.test.ts new file mode 100644 index 00000000..642ce9fd --- /dev/null +++ b/packages/starlight/__tests__/i18n/config.test.ts @@ -0,0 +1,17 @@ +import config from 'virtual:starlight/user-config'; +import { expect, test } from 'vitest'; + +test('test suite is using correct env', () => { + expect(config.title).toBe('i18n with no root locale'); +}); + +test('config.isMultilingual is true with multiple locales', () => { + expect(config.isMultilingual).toBe(true); + expect(config.locales).keys('fr', 'en', 'ar'); +}); + +test('config.defaultLocale is populated from the user’s chosen default', () => { + expect(config.defaultLocale.locale).toBe('en'); + expect(config.defaultLocale.lang).toBe('en-US'); + expect(config.defaultLocale.dir).toBe('ltr'); +}); diff --git a/packages/starlight/__tests__/i18n/localizedUrl.test.ts b/packages/starlight/__tests__/i18n/localizedUrl.test.ts new file mode 100644 index 00000000..4f09f031 --- /dev/null +++ b/packages/starlight/__tests__/i18n/localizedUrl.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from 'vitest'; +import { localizedUrl } from '../../utils/localizedUrl'; + +test('it has no effect if locale matches', () => { + const url = new URL('https://example.com/en/guide/'); + expect(localizedUrl(url, 'en').href).toBe(url.href); +}); + +test('it changes locale to requested locale', () => { + const url = new URL('https://example.com/en/guide/'); + expect(localizedUrl(url, 'fr').href).toBe('https://example.com/fr/guide/'); +}); diff --git a/packages/starlight/__tests__/i18n/routing.test.ts b/packages/starlight/__tests__/i18n/routing.test.ts new file mode 100644 index 00000000..438d5494 --- /dev/null +++ b/packages/starlight/__tests__/i18n/routing.test.ts @@ -0,0 +1,66 @@ +import config from 'virtual:starlight/user-config'; +import { expect, test, vi } from 'vitest'; +import { routes } from '../../utils/routing'; + +vi.mock('astro:content', async () => + (await import('../test-utils')).mockedAstroContent({ + docs: [ + ['fr/index.mdx', { title: 'Accueil' }], + // @ts-expect-error — Using a slug not present in Starlight docs site + ['en/index.mdx', { title: 'Home page' }], + // @ts-expect-error — Using a slug not present in Starlight docs site + ['ar/index.mdx', { title: 'الصفحة الرئيسية' }], + // @ts-expect-error — Using a slug not present in Starlight docs site + ['en/guides/authoring-content.md', { title: 'Création de contenu en Markdown' }], + // @ts-expect-error — Using a slug not present in Starlight docs site + ['en/404.md', { title: 'Page introuvable' }], + ], + }) +); + +test('test suite is using correct env', () => { + expect(config.title).toBe('i18n with no root locale'); +}); + +test('routes includes fallback entries for untranslated pages', () => { + const numLocales = config.isMultilingual ? Object.keys(config.locales).length : 1; + const guides = routes.filter((route) => route.id.includes('/guides/')); + expect(guides).toHaveLength(numLocales); +}); + +test('routes have locale data added', () => { + for (const { id, lang, dir, locale } of routes) { + if (id.startsWith('en')) { + expect(lang).toBe('en-US'); + expect(dir).toBe('ltr'); + expect(locale).toBe('en'); + } else if (id.startsWith('ar')) { + expect(lang).toBe('ar'); + expect(dir).toBe('rtl'); + expect(locale).toBe('ar'); + } else { + expect(lang).toBe('fr'); + expect(dir).toBe('ltr'); + expect(locale).toBe('fr'); + } + } +}); + +test('fallback routes have fallback locale data in entryMeta', () => { + const fallbacks = routes.filter((route) => route.isFallback); + expect(fallbacks.length).toBeGreaterThan(0); + for (const route of fallbacks) { + expect(route.entryMeta.locale).toBe('en'); + expect(route.entryMeta.locale).not.toBe(route.locale); + expect(route.entryMeta.lang).toBe('en-US'); + expect(route.entryMeta.lang).not.toBe(route.lang); + } +}); + +test('fallback routes use their own locale data', () => { + const arGuide = routes.find((route) => route.id === 'ar/guides/authoring-content.md'); + if (!arGuide) throw new Error('Expected to find Arabic fallback route for authoring-content.md'); + expect(arGuide.locale).toBe('ar'); + expect(arGuide.lang).toBe('ar'); + expect(arGuide.dir).toBe('rtl'); +}); diff --git a/packages/starlight/__tests__/i18n/vitest.config.ts b/packages/starlight/__tests__/i18n/vitest.config.ts new file mode 100644 index 00000000..84c58a05 --- /dev/null +++ b/packages/starlight/__tests__/i18n/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineVitestConfig } from '../test-config'; + +export default defineVitestConfig({ + title: 'i18n with no root locale', + defaultLocale: 'en', + locales: { + fr: { label: 'French' }, + en: { label: 'English', lang: 'en-US' }, + ar: { label: 'Arabic', dir: 'rtl' }, + }, +}); diff --git a/packages/starlight/__tests__/sidebar/navigation.test.ts b/packages/starlight/__tests__/sidebar/navigation.test.ts new file mode 100644 index 00000000..5f9e362c --- /dev/null +++ b/packages/starlight/__tests__/sidebar/navigation.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, test, vi } from 'vitest'; +import { getSidebar } from '../../utils/navigation'; + +vi.mock('astro:content', async () => + (await import('../test-utils')).mockedAstroContent({ + docs: [ + ['index.mdx', { title: 'Home Page' }], + ['environmental-impact.md', { title: 'Eco-friendly docs' }], + ['reference/configuration.md', { title: 'Config Reference' }], + ['reference/frontmatter.md', { title: 'Frontmatter Reference' }], + ['guides/components.mdx', { title: 'Components' }], + ], + }) +); + +describe('getSidebar', () => { + test('returns an array of sidebar entries', () => { + expect(getSidebar('/', undefined)).toMatchInlineSnapshot(` + [ + { + "href": "/", + "isCurrent": true, + "label": "Home", + "type": "link", + }, + { + "collapsed": false, + "entries": [ + { + "href": "/intro/", + "isCurrent": false, + "label": "Introduction", + "type": "link", + }, + { + "href": "/next-steps/", + "isCurrent": false, + "label": "Next Steps", + "type": "link", + }, + ], + "label": "Start Here", + "type": "group", + }, + { + "collapsed": false, + "entries": [ + { + "href": "/reference/configuration/", + "isCurrent": false, + "label": "Config Reference", + "type": "link", + }, + { + "href": "/reference/frontmatter/", + "isCurrent": false, + "label": "Frontmatter Reference", + "type": "link", + }, + ], + "label": "Reference", + "type": "group", + }, + ] + `); + }); +}); diff --git a/packages/starlight/__tests__/sidebar/vitest.config.ts b/packages/starlight/__tests__/sidebar/vitest.config.ts new file mode 100644 index 00000000..0a17416b --- /dev/null +++ b/packages/starlight/__tests__/sidebar/vitest.config.ts @@ -0,0 +1,22 @@ +import { defineVitestConfig } from '../test-config'; + +export default defineVitestConfig({ + title: 'Sidebar Test', + sidebar: [ + // A single link item labelled “Home”. + { label: 'Home', link: '/' }, + // A group labelled “Start Here” containing two links. + { + label: 'Start Here', + items: [ + { label: 'Introduction', link: '/intro' }, + { label: 'Next Steps', link: '/next-steps' }, + ], + }, + // A group linking to all pages in the reference directory. + { + label: 'Reference', + autogenerate: { directory: 'reference' }, + }, + ], +}); diff --git a/packages/starlight/__tests__/test-config.ts b/packages/starlight/__tests__/test-config.ts new file mode 100644 index 00000000..304093a5 --- /dev/null +++ b/packages/starlight/__tests__/test-config.ts @@ -0,0 +1,16 @@ +/// <reference types="vitest" /> + +import { getViteConfig } from 'astro/config'; +import type { z } from 'astro/zod'; +import { vitePluginStarlightUserConfig } from '../integrations/virtual-user-config'; +import { StarlightConfigSchema } from '../utils/user-config'; + +export function defineVitestConfig(config: z.input<typeof StarlightConfigSchema>) { + return getViteConfig({ + plugins: [ + vitePluginStarlightUserConfig(StarlightConfigSchema.parse(config), { + root: new URL(import.meta.url), + }), + ], + }); +} diff --git a/packages/starlight/__tests__/test-utils.ts b/packages/starlight/__tests__/test-utils.ts new file mode 100644 index 00000000..e8d87ce3 --- /dev/null +++ b/packages/starlight/__tests__/test-utils.ts @@ -0,0 +1,57 @@ +import { z } from 'astro/zod'; +import { docsSchema, i18nSchema } from '../schema'; +import type { StarlightDocsEntry } from '../utils/routing'; +import { vi } from 'vitest'; + +const frontmatterSchema = docsSchema()({ + image: () => + z.object({ + src: z.string(), + width: z.number(), + height: z.number(), + format: z.union([ + z.literal('png'), + z.literal('jpg'), + z.literal('jpeg'), + z.literal('tiff'), + z.literal('webp'), + z.literal('gif'), + z.literal('svg'), + ]), + }), +}); + +function mockDoc( + id: StarlightDocsEntry['id'], + data: z.input<typeof frontmatterSchema>, + body = '' +): StarlightDocsEntry { + return { + id, + slug: id.replace(/\.[^\.]+$/, '').replace(/\/index$/, ''), + body, + collection: 'docs', + data: frontmatterSchema.parse(data), + render: (() => {}) as StarlightDocsEntry['render'], + }; +} + +function mockDict(id: string, data: z.input<ReturnType<typeof i18nSchema>>) { + return { id, data: i18nSchema().parse(data) }; +} + +export async function mockedAstroContent({ + docs = [], + i18n = [], +}: { + docs?: Parameters<typeof mockDoc>[]; + i18n?: Parameters<typeof mockDict>[]; +}) { + const mod = await vi.importActual<typeof import('astro:content')>('astro:content'); + const mockDocs = docs.map((doc) => mockDoc(...doc)); + const mockDicts = i18n.map((dict) => mockDict(...dict)); + return { + ...mod, + getCollection: (collection: 'docs' | 'i18n') => (collection === 'i18n' ? mockDicts : mockDocs), + }; +} diff --git a/packages/starlight/index.ts b/packages/starlight/index.ts index 80d3b630..63167eed 100644 --- a/packages/starlight/index.ts +++ b/packages/starlight/index.ts @@ -1,21 +1,16 @@ import mdx from '@astrojs/mdx'; -import type { - AstroConfig, - AstroIntegration, - AstroUserConfig, - ViteUserConfig, -} from 'astro'; +import type { AstroIntegration, AstroUserConfig } from 'astro'; import { spawn } from 'node:child_process'; -import { dirname, relative, resolve } from 'node:path'; +import { dirname, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; import { starlightAsides } from './integrations/asides'; import { starlightSitemap } from './integrations/sitemap'; +import { vitePluginStarlightUserConfig } from './integrations/virtual-user-config'; +import { errorMap } from './utils/error-map'; import { - StarlightUserConfig, - StarlightConfig, StarlightConfigSchema, + StarlightUserConfig, } from './utils/user-config'; -import { errorMap } from './utils/error-map'; export default function StarlightIntegration( opts: StarlightUserConfig @@ -78,47 +73,3 @@ export default function StarlightIntegration( return [starlightSitemap(userConfig), Starlight, mdx()]; } - -function resolveVirtualModuleId(id: string) { - return '\0' + id; -} - -/** Expose the Starlight user config object via a virtual module. */ -function vitePluginStarlightUserConfig( - opts: StarlightConfig, - { root }: AstroConfig -): NonNullable<ViteUserConfig['plugins']>[number] { - const resolveRelativeId = (id: string) => - JSON.stringify(id.startsWith('.') ? resolve(fileURLToPath(root), id) : id); - const modules = { - 'virtual:starlight/user-config': `export default ${JSON.stringify(opts)}`, - 'virtual:starlight/project-context': `export default ${JSON.stringify({ - root, - })}`, - 'virtual:starlight/user-css': opts.customCss - .map((id) => `import ${resolveRelativeId(id)};`) - .join(''), - 'virtual:starlight/user-images': opts.logo - ? 'src' in opts.logo - ? `import src from ${resolveRelativeId(opts.logo.src)}; export const logos = { dark: src, light: src };` - : `import dark from ${resolveRelativeId(opts.logo.dark)}; import light from ${resolveRelativeId(opts.logo.light)}; export const logos = { dark, light };` - : 'export const logos = {};', - }; - const resolutionMap = Object.fromEntries( - (Object.keys(modules) as (keyof typeof modules)[]).map((key) => [ - resolveVirtualModuleId(key), - key, - ]) - ); - - return { - name: 'vite-plugin-starlight-user-config', - resolveId(id): string | void { - if (id in modules) return resolveVirtualModuleId(id); - }, - load(id): string | void { - const resolution = resolutionMap[id]; - if (resolution) return modules[resolution]; - }, - }; -} diff --git a/packages/starlight/integrations/virtual-user-config.ts b/packages/starlight/integrations/virtual-user-config.ts new file mode 100644 index 00000000..1ddf4995 --- /dev/null +++ b/packages/starlight/integrations/virtual-user-config.ts @@ -0,0 +1,52 @@ +import type { AstroConfig, ViteUserConfig } from 'astro'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import type { StarlightConfig } from '../utils/user-config'; + +function resolveVirtualModuleId<T extends string>(id: T): `\0${T}` { + return `\0${id}`; +} + +/** Vite plugin that exposes Starlight user config and project context via virtual modules. */ +export function vitePluginStarlightUserConfig( + opts: StarlightConfig, + { root }: Pick<AstroConfig, 'root'> +): NonNullable<ViteUserConfig['plugins']>[number] { + const resolveId = (id: string) => + JSON.stringify(id.startsWith('.') ? resolve(fileURLToPath(root), id) : id); + + /** Map of virtual module names to their code contents as strings. */ + const modules = { + 'virtual:starlight/user-config': `export default ${JSON.stringify(opts)}`, + 'virtual:starlight/project-context': `export default ${JSON.stringify({ root })}`, + 'virtual:starlight/user-css': opts.customCss.map((id) => `import ${resolveId(id)};`).join(''), + 'virtual:starlight/user-images': opts.logo + ? 'src' in opts.logo + ? `import src from ${resolveId( + opts.logo.src + )}; export const logos = { dark: src, light: src };` + : `import dark from ${resolveId(opts.logo.dark)}; import light from ${resolveId( + opts.logo.light + )}; export const logos = { dark, light };` + : 'export const logos = {};', + } satisfies Record<string, string>; + + /** Mapping names prefixed with `\0` to their original form. */ + const resolutionMap = Object.fromEntries( + (Object.keys(modules) as (keyof typeof modules)[]).map((key) => [ + resolveVirtualModuleId(key), + key, + ]) + ); + + return { + name: 'vite-plugin-starlight-user-config', + resolveId(id): string | void { + if (id in modules) return resolveVirtualModuleId(id); + }, + load(id): string | void { + const resolution = resolutionMap[id]; + if (resolution) return modules[resolution]; + }, + }; +} diff --git a/packages/starlight/package.json b/packages/starlight/package.json index 8f765289..f30834ba 100644 --- a/packages/starlight/package.json +++ b/packages/starlight/package.json @@ -2,7 +2,10 @@ "name": "@astrojs/starlight", "version": "0.5.5", "description": "Build beautiful, high-performance documentation websites with Astro", - "scripts": {}, + "scripts": { + "test": "vitest", + "test:coverage": "vitest run --coverage" + }, "keywords": [ "docs", "documentation", @@ -33,7 +36,9 @@ }, "devDependencies": { "@types/node": "^18.16.19", - "astro": "^2.8.5" + "@vitest/coverage-v8": "^0.33.0", + "astro": "^2.8.5", + "vitest": "^0.33.0" }, "dependencies": { "@astrojs/mdx": "^0.19.7", diff --git a/packages/starlight/types.ts b/packages/starlight/types.ts index 70513dc2..8d99451e 100644 --- a/packages/starlight/types.ts +++ b/packages/starlight/types.ts @@ -1 +1 @@ -export { StarlightConfig } from './utils/user-config'; +export type { StarlightConfig } from './utils/user-config'; diff --git a/packages/starlight/utils/slugs.ts b/packages/starlight/utils/slugs.ts index 9706b653..c6956509 100644 --- a/packages/starlight/utils/slugs.ts +++ b/packages/starlight/utils/slugs.ts @@ -56,7 +56,7 @@ export function slugToParam(slug: string): string | undefined { return slug === 'index' || slug === '' ? undefined : slug.endsWith('/index') - ? slug.replace('/index', '') + ? slug.replace(/\/index$/, '') : slug; } diff --git a/packages/starlight/vitest.config.ts b/packages/starlight/vitest.config.ts new file mode 100644 index 00000000..8312dbdf --- /dev/null +++ b/packages/starlight/vitest.config.ts @@ -0,0 +1,31 @@ +import { defineConfig } from 'vitest/config'; + +// Copy of https://github.com/vitest-dev/vitest/blob/8693449b412743f20a63fd9bfa1a9054aa74613f/packages/vitest/src/defaults.ts#L13C1-L26C1 +const defaultCoverageExcludes = [ + 'coverage/**', + 'dist/**', + 'packages/*/test?(s)/**', + '**/*.d.ts', + 'cypress/**', + 'test?(s)/**', + 'test?(-*).?(c|m)[jt]s?(x)', + '**/*{.,-}{test,spec}.?(c|m)[jt]s?(x)', + '**/__tests__/**', + '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*', + '**/.{eslint,mocha,prettier}rc.{?(c|m)js,yml}', +]; + +export default defineConfig({ + test: { + coverage: { + all: true, + reportsDirectory: './__coverage__', + exclude: [...defaultCoverageExcludes, '**/vitest.*', 'components.ts', 'types.ts'], + thresholdAutoUpdate: true, + lines: 52.11, + functions: 82.35, + branches: 89.17, + statements: 52.11, + }, + }, +}); diff --git a/packages/starlight/vitest.workspace.ts b/packages/starlight/vitest.workspace.ts new file mode 100644 index 00000000..4cfafe82 --- /dev/null +++ b/packages/starlight/vitest.workspace.ts @@ -0,0 +1 @@ +export default ['__tests__/*']; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32db1365..24434380 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + importers: .: @@ -147,9 +151,15 @@ importers: '@types/node': specifier: ^18.16.19 version: 18.16.19 + '@vitest/coverage-v8': + specifier: ^0.33.0 + version: 0.33.0(vitest@0.33.0) astro: specifier: ^2.8.5 version: 2.8.5(@types/node@18.16.19) + vitest: + specifier: ^0.33.0 + version: 0.33.0 packages: @@ -160,6 +170,14 @@ packages: '@jridgewell/gen-mapping': 0.1.1 '@jridgewell/trace-mapping': 0.3.18 + /@ampproject/remapping@2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.2 + '@jridgewell/trace-mapping': 0.3.18 + dev: true + /@astrojs/compiler@1.5.3: resolution: {integrity: sha512-/HSFkJ+Yv+WUWSq0QVsIlhBKam5VUpGV+s8MvPguC/krHmw4Ww9TIgmfJSvV8/BN0sHJB7pCgf7yInae1zb+TQ==} @@ -190,7 +208,7 @@ packages: astro: ^2.5.0 dependencies: '@astrojs/prism': 2.1.2 - astro: 2.8.5(sharp@0.32.3) + astro: 2.8.5(@types/node@18.16.19) github-slugger: 1.5.0 import-meta-resolve: 2.2.2 rehype-raw: 6.1.1 @@ -500,6 +518,10 @@ packages: '@babel/helper-validator-identifier': 7.22.5 to-fast-properties: 2.0.0 + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: true + /@changesets/apply-release-plan@6.1.3: resolution: {integrity: sha512-ECDNeoc3nfeAe1jqJb5aFQX7CqzQhD2klXRez2JDb/aVpGUbX673HgKrnrgJRuQR/9f2TtLoYIzrGB9qwD77mg==} dependencies: @@ -920,19 +942,31 @@ packages: '@hapi/hoek': 9.3.0 dev: true + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + dev: true + + /@jest/schemas@29.6.0: + resolution: {integrity: sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + /@jridgewell/gen-mapping@0.1.1: resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} engines: {node: '>=6.0.0'} dependencies: '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/sourcemap-codec': 1.4.15 /@jridgewell/gen-mapping@0.3.2: resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} engines: {node: '>=6.0.0'} dependencies: '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/trace-mapping': 0.3.18 /@jridgewell/resolve-uri@3.1.0: @@ -946,6 +980,9 @@ packages: /@jridgewell/sourcemap-codec@1.4.14: resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + /@jridgewell/trace-mapping@0.3.18: resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} dependencies: @@ -1095,6 +1132,10 @@ packages: resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} dev: true + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + /@size-limit/file@8.2.4(size-limit@8.2.4): resolution: {integrity: sha512-xLuF97W7m7lxrRJvqXRlxO/4t7cpXtfxOnjml/t4aRVUCMXLdyvebRr9OM4jjoK8Fmiz8jomCbETUCI3jVhLzA==} engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0} @@ -1136,6 +1177,16 @@ packages: dependencies: '@babel/types': 7.22.5 + /@types/chai-subset@1.3.3: + resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} + dependencies: + '@types/chai': 4.3.5 + dev: true + + /@types/chai@4.3.5: + resolution: {integrity: sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==} + dev: true + /@types/debug@4.1.7: resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} dependencies: @@ -1166,6 +1217,10 @@ packages: ci-info: 3.8.0 dev: true + /@types/istanbul-lib-coverage@2.0.4: + resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} + dev: true + /@types/json5@0.0.30: resolution: {integrity: sha512-sqm9g7mHlPY/43fcSNrCYfOeX9zkTTK+euO5E6+CVijSMm5tTjkVdwdqRkY3ljjIAf8679vps5jKUoJBCLsMDA==} @@ -1201,11 +1256,6 @@ packages: /@types/node@18.16.19: resolution: {integrity: sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==} - /@types/node@20.4.2: - resolution: {integrity: sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==} - dev: true - optional: true - /@types/normalize-package-data@2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} dev: true @@ -1240,10 +1290,69 @@ packages: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true dependencies: - '@types/node': 20.4.2 + '@types/node': 18.16.19 dev: true optional: true + /@vitest/coverage-v8@0.33.0(vitest@0.33.0): + resolution: {integrity: sha512-Rj5IzoLF7FLj6yR7TmqsfRDSeaFki6NAJ/cQexqhbWkHEV2htlVGrmuOde3xzvFsCbLCagf4omhcIaVmfU8Okg==} + peerDependencies: + vitest: '>=0.32.0 <1' + dependencies: + '@ampproject/remapping': 2.2.1 + '@bcoe/v8-coverage': 0.2.3 + istanbul-lib-coverage: 3.2.0 + istanbul-lib-report: 3.0.0 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.5 + magic-string: 0.30.1 + picocolors: 1.0.0 + std-env: 3.3.3 + test-exclude: 6.0.0 + v8-to-istanbul: 9.1.0 + vitest: 0.33.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@vitest/expect@0.33.0: + resolution: {integrity: sha512-sVNf+Gla3mhTCxNJx+wJLDPp/WcstOe0Ksqz4Vec51MmgMth/ia0MGFEkIZmVGeTL5HtjYR4Wl/ZxBxBXZJTzQ==} + dependencies: + '@vitest/spy': 0.33.0 + '@vitest/utils': 0.33.0 + chai: 4.3.7 + dev: true + + /@vitest/runner@0.33.0: + resolution: {integrity: sha512-UPfACnmCB6HKRHTlcgCoBh6ppl6fDn+J/xR8dTufWiKt/74Y9bHci5CKB8tESSV82zKYtkBJo9whU3mNvfaisg==} + dependencies: + '@vitest/utils': 0.33.0 + p-limit: 4.0.0 + pathe: 1.1.1 + dev: true + + /@vitest/snapshot@0.33.0: + resolution: {integrity: sha512-tJjrl//qAHbyHajpFvr8Wsk8DIOODEebTu7pgBrP07iOepR5jYkLFiqLq2Ltxv+r0uptUb4izv1J8XBOwKkVYA==} + dependencies: + magic-string: 0.30.1 + pathe: 1.1.1 + pretty-format: 29.6.1 + dev: true + + /@vitest/spy@0.33.0: + resolution: {integrity: sha512-Kv+yZ4hnH1WdiAkPUQTpRxW8kGtH8VRTnus7ZTGovFYM1ZezJpvGtb9nPIjPnptHbsyIAxYZsEpVPYgtpjGnrg==} + dependencies: + tinyspy: 2.1.1 + dev: true + + /@vitest/utils@0.33.0: + resolution: {integrity: sha512-pF1w22ic965sv+EN6uoePkAOTkAPWM03Ri/jXNyMIKBb/XHLDPfhLvf/Fa9g0YECevAIz56oVYXhodLvLQ/awA==} + dependencies: + diff-sequences: 29.4.3 + loupe: 2.3.6 + pretty-format: 29.6.1 + dev: true + /@vscode/emmet-helper@2.8.8: resolution: {integrity: sha512-QuD4CmNeXSFxuP8VZwI6qL+8vmmd7JcSdwsEIdsrzb4YumWs/+4rXRX9MM+NsFfUO69g6ezngCD7XRd6jY9TQw==} dependencies: @@ -1268,6 +1377,11 @@ packages: acorn: 8.9.0 dev: false + /acorn-walk@8.2.0: + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + engines: {node: '>=0.4.0'} + dev: true + /acorn@8.9.0: resolution: {integrity: sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==} engines: {node: '>=0.4.0'} @@ -1315,6 +1429,11 @@ packages: dependencies: color-convert: 2.0.1 + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + /ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} @@ -1379,6 +1498,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true + /astring@1.8.4: resolution: {integrity: sha512-97a+l2LBU3Op3bBQEff79i/E4jMD2ZLFD8rHx9B6mXyB2uQwhJQYfiDqUwtfjF4QA1F2qs//N6Cw8LetMbQjcw==} hasBin: true @@ -1458,7 +1581,6 @@ packages: - sugarss - supports-color - terser - dev: true /astro@2.8.5(sharp@0.32.3): resolution: {integrity: sha512-qfPUKLpZ9lVi5Hc5MrzyekUUx54AyrEphW5eetNQj/+d0iodHEneZXFDzZxTEsk3rL8Y2Y9pYFXJPmQB3eahUA==} @@ -1701,6 +1823,11 @@ packages: engines: {node: '>= 0.8'} dev: true + /cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: true + /call-bind@1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: @@ -1732,6 +1859,19 @@ packages: /ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + /chai@4.3.7: + resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.2 + deep-eql: 4.1.3 + get-func-name: 2.0.0 + loupe: 2.3.6 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -1768,6 +1908,10 @@ packages: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true + /check-error@1.0.2: + resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + dev: true + /check-more-types@2.24.0: resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} engines: {node: '>= 0.8.0'} @@ -2026,6 +2170,13 @@ packages: resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} dev: true + /deep-eql@4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.0.8 + dev: true + /deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -2076,6 +2227,11 @@ packages: resolution: {integrity: sha512-VvlVYY+VDJe639yHs5PHISzdWTLL3Aw8rO4cvUtwvoxFd6FHbE4OpHHcde52M6096uYYazAmd4l0o5VuFRO2WA==} dev: true + /diff-sequences@29.4.3: + resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /diff@5.1.0: resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} engines: {node: '>=0.3.1'} @@ -2817,6 +2973,10 @@ packages: engines: {node: 6.* || 8.* || >= 10.*} dev: true + /get-func-name@2.0.0: + resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} + dev: true + /get-intrinsic@1.2.0: resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==} dependencies: @@ -3124,6 +3284,10 @@ packages: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: true + /html-escaper@3.0.3: resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} @@ -3449,6 +3613,39 @@ packages: /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + /istanbul-lib-coverage@3.2.0: + resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} + engines: {node: '>=8'} + dev: true + + /istanbul-lib-report@3.0.0: + resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==} + engines: {node: '>=8'} + dependencies: + istanbul-lib-coverage: 3.2.0 + make-dir: 3.1.0 + supports-color: 7.2.0 + dev: true + + /istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + dependencies: + debug: 4.3.4 + istanbul-lib-coverage: 3.2.0 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-reports@3.1.5: + resolution: {integrity: sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.0 + dev: true + /joi@17.9.2: resolution: {integrity: sha512-Itk/r+V4Dx0V3c7RLFdRh12IOjySm2/WGPMubBT92cQvRfYZhPM2W0hZlctjj72iES8jsRCwp7S/cRmWBnJ4nw==} dependencies: @@ -3536,6 +3733,11 @@ packages: pify: 4.0.1 strip-bom: 3.0.0 + /local-pkg@0.4.3: + resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} + engines: {node: '>=14'} + dev: true + /locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -3566,6 +3768,12 @@ packages: /longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + /loupe@2.3.6: + resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} + dependencies: + get-func-name: 2.0.0 + dev: true + /lru-cache@4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} dependencies: @@ -3590,6 +3798,20 @@ packages: dependencies: '@jridgewell/sourcemap-codec': 1.4.14 + /magic-string@0.30.1: + resolution: {integrity: sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + dependencies: + semver: 6.3.0 + dev: true + /map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} @@ -4228,6 +4450,15 @@ packages: deprecated: Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.) dev: true + /mlly@1.4.0: + resolution: {integrity: sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==} + dependencies: + acorn: 8.9.0 + pathe: 1.1.1 + pkg-types: 1.0.3 + ufo: 1.1.2 + dev: true + /mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -4604,6 +4835,14 @@ packages: engines: {node: '>=8'} dev: true + /pathe@1.1.1: + resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} + dev: true + + /pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true + /pause-stream@0.0.11: resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} dependencies: @@ -4656,6 +4895,14 @@ packages: dependencies: find-up: 4.1.0 + /pkg-types@1.0.3: + resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + dependencies: + jsonc-parser: 3.2.0 + mlly: 1.4.0 + pathe: 1.1.1 + dev: true + /postcss@8.4.23: resolution: {integrity: sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==} engines: {node: ^10 || ^12 || >=14} @@ -4716,6 +4963,15 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + /pretty-format@29.6.1: + resolution: {integrity: sha512-7jRj+yXO0W7e4/tSJKoR7HRIHLPPjtNaUGG2xxKQnGvPNRkgWcQ0AZX6P4KBRJN4FcTBWb3sa7DVUJmocYuoog==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.0 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: true + /prismjs@1.29.0: resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} engines: {node: '>=6'} @@ -4810,6 +5066,10 @@ packages: minimist: 1.2.8 strip-json-comments: 2.0.1 + /react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + dev: true + /read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} @@ -5192,6 +5452,10 @@ packages: object-inspect: 1.12.3 dev: true + /siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true + /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -5269,6 +5533,11 @@ packages: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + /source-map@0.7.4: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} @@ -5315,6 +5584,10 @@ packages: /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + /stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true + /start-server-and-test@2.0.0: resolution: {integrity: sha512-UqKLw0mJbfrsG1jcRLTUlvuRi9sjNuUiDOLI42r7R5fA9dsFoywAy9DoLXNYys9B886E4RCKb+qM1Gzu96h7DQ==} engines: {node: '>=6'} @@ -5332,6 +5605,10 @@ packages: - supports-color dev: true + /std-env@3.3.3: + resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==} + dev: true + /stdin-discarder@0.1.0: resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5456,6 +5733,12 @@ packages: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} + /strip-literal@1.0.1: + resolution: {integrity: sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==} + dependencies: + acorn: 8.9.0 + dev: true + /style-to-object@0.4.1: resolution: {integrity: sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==} dependencies: @@ -5527,6 +5810,15 @@ packages: engines: {node: '>=8'} dev: true + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + dev: true + /through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} dev: true @@ -5537,6 +5829,20 @@ packages: globalyzer: 0.1.0 globrex: 0.1.2 + /tinybench@2.5.0: + resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==} + dev: true + + /tinypool@0.6.0: + resolution: {integrity: sha512-FdswUUo5SxRizcBc6b1GSuLpLjisa8N8qMyYoP3rl+bym+QauhtJP5bvZY1ytt8krKGmMLYIRl36HBZfeAoqhQ==} + engines: {node: '>=14.0.0'} + dev: true + + /tinyspy@2.1.1: + resolution: {integrity: sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==} + engines: {node: '>=14.0.0'} + dev: true + /tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -5613,6 +5919,11 @@ packages: dependencies: safe-buffer: 5.2.1 + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + /type-fest@0.13.1: resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} engines: {node: '>=10'} @@ -5644,6 +5955,10 @@ packages: engines: {node: '>=4.2.0'} hasBin: true + /ufo@1.1.2: + resolution: {integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==} + dev: true + /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: @@ -5771,6 +6086,15 @@ packages: kleur: 4.1.5 sade: 1.8.1 + /v8-to-istanbul@9.1.0: + resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.18 + '@types/istanbul-lib-coverage': 2.0.4 + convert-source-map: 1.9.0 + dev: true + /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: @@ -5798,6 +6122,27 @@ packages: unist-util-stringify-position: 3.0.3 vfile-message: 3.1.4 + /vite-node@0.33.0(@types/node@18.16.19): + resolution: {integrity: sha512-19FpHYbwWWxDr73ruNahC+vtEdza52kA90Qb3La98yZ0xULqV8A5JLNPUff0f5zID4984tW7l3DH2przTJUZSw==} + engines: {node: '>=v14.18.0'} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + mlly: 1.4.0 + pathe: 1.1.1 + picocolors: 1.0.0 + vite: 4.3.9(@types/node@18.16.19) + transitivePeerDependencies: + - '@types/node' + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vite@4.3.9(@types/node@18.16.19): resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} engines: {node: ^14.18.0 || >=16.0.0} @@ -5840,6 +6185,70 @@ packages: dependencies: vite: 4.3.9(@types/node@18.16.19) + /vitest@0.33.0: + resolution: {integrity: sha512-1CxaugJ50xskkQ0e969R/hW47za4YXDUfWJDxip1hwbnhUjYolpfUn2AMOulqG/Dtd9WYAtkHmM/m3yKVrEejQ==} + engines: {node: '>=v14.18.0'} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@vitest/browser': '*' + '@vitest/ui': '*' + happy-dom: '*' + jsdom: '*' + playwright: '*' + safaridriver: '*' + webdriverio: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + playwright: + optional: true + safaridriver: + optional: true + webdriverio: + optional: true + dependencies: + '@types/chai': 4.3.5 + '@types/chai-subset': 1.3.3 + '@types/node': 18.16.19 + '@vitest/expect': 0.33.0 + '@vitest/runner': 0.33.0 + '@vitest/snapshot': 0.33.0 + '@vitest/spy': 0.33.0 + '@vitest/utils': 0.33.0 + acorn: 8.9.0 + acorn-walk: 8.2.0 + cac: 6.7.14 + chai: 4.3.7 + debug: 4.3.4 + local-pkg: 0.4.3 + magic-string: 0.30.1 + pathe: 1.1.1 + picocolors: 1.0.0 + std-env: 3.3.3 + strip-literal: 1.0.1 + tinybench: 2.5.0 + tinypool: 0.6.0 + vite: 4.3.9(@types/node@18.16.19) + vite-node: 0.33.0(@types/node@18.16.19) + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vscode-css-languageservice@6.2.5: resolution: {integrity: sha512-/1oyBZK3jfx6A0cA46FCUpy6OlqEsMT47LUIldCIP1YMKRYezJ9No+aNj9IM0AqhRZ92DxZ1DmU5lJ+biuiacA==} dependencies: @@ -5979,6 +6388,15 @@ packages: dependencies: isexe: 2.0.0 + /why-is-node-running@2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + /widest-line@4.0.1: resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} engines: {node: '>=12'} @@ -6112,7 +6530,3 @@ packages: /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false -- cgit