├── .changeset
├── README.md
└── config.json
├── .github
└── workflows
│ ├── autofix.yml
│ ├── integration.yml
│ └── release.yml
├── .gitignore
├── .node-version
├── .prettierignore
├── .prettierrc.cjs
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── docs
├── README.md
├── astro.config.ts
├── package.json
├── public
│ └── favicon.svg
├── src
│ ├── assets
│ │ └── showcase
│ │ │ ├── classchartsapi.github.io.png
│ │ │ ├── docs.garajonai.com.png
│ │ │ ├── docs.taiko.xyz.png
│ │ │ ├── fxhu.kripod.dev.png
│ │ │ ├── openpayments.dev.png
│ │ │ └── openpodcastapi.org.png
│ ├── content.config.ts
│ ├── content
│ │ └── docs
│ │ │ ├── configuration.mdx
│ │ │ ├── getting-started.mdx
│ │ │ ├── index.mdx
│ │ │ └── resources
│ │ │ ├── showcase.mdx
│ │ │ └── starlight.mdx
│ ├── env.d.ts
│ └── styles
│ │ └── custom.css
└── tsconfig.json
├── eslint.config.mjs
├── package.json
├── packages
├── starlight-openapi-docs-demo
│ ├── index.ts
│ ├── middleware.ts
│ └── package.json
└── starlight-openapi
│ ├── .npmignore
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── components
│ ├── Content.astro
│ ├── ContentPicker.astro
│ ├── Deprecated.astro
│ ├── ExternalDocs.astro
│ ├── Heading.astro
│ ├── Items.astro
│ ├── Key.astro
│ ├── Md.astro
│ ├── OperationTag.astro
│ ├── Overview.astro
│ ├── RequestBody.astro
│ ├── Route.astro
│ ├── Section.astro
│ ├── Select.astro
│ ├── Tag.astro
│ ├── Tags.astro
│ ├── Text.astro
│ ├── callback
│ │ ├── CallbackOperation.astro
│ │ └── Callbacks.astro
│ ├── example
│ │ ├── Example.astro
│ │ └── Examples.astro
│ ├── operation
│ │ ├── Operation.astro
│ │ ├── OperationDescription.astro
│ │ ├── OperationMethod.astro
│ │ └── OperationUrl.astro
│ ├── parameter
│ │ ├── Parameter.astro
│ │ └── Parameters.astro
│ ├── response
│ │ ├── Response.astro
│ │ ├── ResponseExamples.astro
│ │ ├── ResponseHeaders.astro
│ │ └── Responses.astro
│ ├── schema
│ │ ├── Schema.astro
│ │ ├── SchemaObject.astro
│ │ ├── SchemaObjectAllOf.astro
│ │ ├── SchemaObjectObject.astro
│ │ ├── SchemaObjectObjectProperties.astro
│ │ └── SchemaObjects.astro
│ └── security
│ │ ├── Security.astro
│ │ ├── SecurityDefinitions.astro
│ │ └── SecurityOAuth2Flow.astro
│ ├── index.ts
│ ├── libs
│ ├── callback.ts
│ ├── config.ts
│ ├── content.ts
│ ├── document.ts
│ ├── env.d.ts
│ ├── example.ts
│ ├── header.ts
│ ├── integration.ts
│ ├── items.ts
│ ├── markdown.ts
│ ├── operation.ts
│ ├── parameter.ts
│ ├── parser.ts
│ ├── path.ts
│ ├── pathItem.ts
│ ├── requestBody.ts
│ ├── response.ts
│ ├── route.ts
│ ├── schema.ts
│ ├── schemaObject.ts
│ ├── security.ts
│ ├── starlight.ts
│ ├── utils.ts
│ └── vite.ts
│ ├── middleware.ts
│ ├── package.json
│ ├── playwright.config.ts
│ ├── styles.css
│ ├── tests
│ ├── callback.test.ts
│ ├── fixtures
│ │ ├── DocPage.ts
│ │ └── SidebarPage.ts
│ ├── header.test.ts
│ ├── operation.test.ts
│ ├── operationTag.test.ts
│ ├── overview.test.ts
│ ├── parameter.test.ts
│ ├── recursion.test.ts
│ ├── requestBody.test.ts
│ ├── response.test.ts
│ ├── security.test.ts
│ ├── sidebar.test.ts
│ ├── test.ts
│ ├── toc.test.ts
│ └── webhooks.test.ts
│ ├── tsconfig.json
│ └── virtual.d.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── schemas
├── README.md
├── v2.0
│ ├── animals.yaml
│ └── petstore-simple.yaml
└── v3.0
│ ├── animals.yaml
│ ├── petstore-expanded.yaml
│ ├── petstore.json
│ ├── recursive-simple.yaml
│ └── recursive.yaml
└── tsconfig.json
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json",
3 | "changelog": [
4 | "@changesets/changelog-github",
5 | { "repo": "HiDeoo/starlight-openapi" }
6 | ],
7 | "commit": false,
8 | "access": "public",
9 | "baseBranch": "main",
10 | "updateInternalDependencies": "patch",
11 | "ignore": ["starlight-openapi-docs", "starlight-openapi-docs-demo"]
12 | }
13 |
--------------------------------------------------------------------------------
/.github/workflows/autofix.yml:
--------------------------------------------------------------------------------
1 | name: autofix.ci
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 | workflow_call:
11 |
12 | permissions:
13 | contents: read
14 |
15 | concurrency:
16 | cancel-in-progress: true
17 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.ref }}
18 |
19 | jobs:
20 | autofix:
21 | name: Format code
22 | runs-on: ubuntu-latest
23 | steps:
24 | - name: Checkout
25 | uses: actions/checkout@v4
26 |
27 | - name: Install pnpm
28 | uses: pnpm/action-setup@v4
29 | with:
30 | version: 8.6.12
31 |
32 | - name: Install Node.js
33 | uses: actions/setup-node@v4
34 | with:
35 | cache: pnpm
36 | node-version: 18
37 |
38 | - name: Install dependencies
39 | run: pnpm install
40 |
41 | - name: Format code
42 | run: pnpm format
43 |
44 | - name: Run autofix
45 | uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c
46 | with:
47 | fail-fast: false
48 |
--------------------------------------------------------------------------------
/.github/workflows/integration.yml:
--------------------------------------------------------------------------------
1 | name: integration
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 | workflow_call:
11 |
12 | concurrency:
13 | cancel-in-progress: true
14 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.ref }}
15 |
16 | jobs:
17 | lint_test:
18 | name: Lint & Test
19 | runs-on: ubuntu-latest
20 | steps:
21 | - name: Checkout
22 | uses: actions/checkout@v4
23 |
24 | - name: Install pnpm
25 | uses: pnpm/action-setup@v4
26 | with:
27 | version: 8.6.12
28 |
29 | - name: Install Node.js
30 | uses: actions/setup-node@v4
31 | with:
32 | cache: pnpm
33 | node-version: 18
34 |
35 | - name: Install dependencies
36 | run: pnpm install
37 |
38 | - name: Generates docs TypeScript types
39 | run: pnpm astro sync
40 | working-directory: docs
41 |
42 | - name: Lint
43 | run: pnpm lint
44 |
45 | - name: Test
46 | run: pnpm test
47 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | changeset:
10 | name: Changeset
11 | if: ${{ github.repository_owner == 'hideoo' }}
12 | runs-on: ubuntu-latest
13 | permissions:
14 | contents: write
15 | id-token: write
16 | pull-requests: write
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v4
20 | with:
21 | fetch-depth: 0
22 |
23 | - name: Install pnpm
24 | uses: pnpm/action-setup@v4
25 | with:
26 | version: 8.6.12
27 |
28 | - name: Install Node.js
29 | uses: actions/setup-node@v4
30 | with:
31 | cache: pnpm
32 | node-version: 18
33 |
34 | - name: Install dependencies
35 | run: pnpm install
36 |
37 | - name: Create Release Pull Request or Publish
38 | uses: changesets/action@v1
39 | with:
40 | version: pnpm run version
41 | publish: pnpm changeset publish
42 | commit: 'ci: release'
43 | title: 'ci: release'
44 | env:
45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .astro
2 | .DS_Store
3 | .eslintcache
4 | .idea
5 | .next
6 | .turbo
7 | .vercel
8 | .vscode/*
9 | !.vscode/extensions.json
10 | !.vscode/launch.json
11 | !.vscode/settings.json
12 | !.vscode/tasks.json
13 | .vscode-test
14 | .vscode-test-web
15 | *.local
16 | *.log
17 | *.pem
18 | *.tsbuildinfo
19 | build
20 | coverage
21 | dist
22 | dist-ssr
23 | lerna-debug.log*
24 | logs
25 | next-env.d.ts
26 | node_modules
27 | npm-debug.log*
28 | out
29 | pnpm-debug.log*
30 | releases
31 | test-results
32 | yarn-debug.log*
33 | yarn-error.log*
34 |
--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------
1 | v18.17.1
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .astro
2 | .changeset
3 | .github/blocks
4 | .next
5 | .vercel
6 | .vscode-test
7 | .vscode-test-web
8 | build
9 | coverage
10 | dist
11 | dist-ssr
12 | out
13 | pnpm-lock.yaml
14 |
--------------------------------------------------------------------------------
/.prettierrc.cjs:
--------------------------------------------------------------------------------
1 | const baseConfig = require('@hideoo/prettier-config')
2 |
3 | /**
4 | * @type {import('prettier').Config}
5 | */
6 | const prettierConfig = {
7 | ...baseConfig,
8 | overrides: [
9 | {
10 | files: '*.astro',
11 | options: {
12 | parser: 'astro',
13 | },
14 | },
15 | ],
16 | plugins: [require.resolve('prettier-plugin-astro')],
17 | }
18 |
19 | module.exports = prettierConfig
20 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.useFlatConfig": true,
3 | "eslint.validate": [
4 | "javascript",
5 | "javascriptreact",
6 | "typescript",
7 | "typescriptreact",
8 | "html",
9 | "vue",
10 | "markdown",
11 | "astro"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023-present, HiDeoo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | packages/starlight-openapi/README.md
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 |
2 |
starlight-openapi 🧭
3 |
Starlight plugin to generate documentation from OpenAPI/Swagger specifications.
4 |
5 |
6 |
15 |
16 | ## Docs
17 |
18 | Run the docs locally using [pnpm](https://pnpm.io):
19 |
20 | ```shell
21 | pnpm run dev
22 | ```
23 |
24 | ## License
25 |
26 | Licensed under the MIT License, Copyright © HiDeoo.
27 |
28 | See [LICENSE](https://github.com/HiDeoo/starlight-openapi/blob/main/LICENSE) for more information.
29 |
--------------------------------------------------------------------------------
/docs/astro.config.ts:
--------------------------------------------------------------------------------
1 | import starlight from '@astrojs/starlight'
2 | import { defineConfig } from 'astro/config'
3 | import starlightOpenAPI, { openAPISidebarGroups } from 'starlight-openapi'
4 | import starlightOpenAPIDocsDemo from 'starlight-openapi-docs-demo'
5 |
6 | export default defineConfig({
7 | integrations: [
8 | starlight({
9 | customCss: ['./src/styles/custom.css'],
10 | editLink: {
11 | baseUrl: 'https://github.com/HiDeoo/starlight-openapi/edit/main/docs/',
12 | },
13 | plugins: [
14 | starlightOpenAPI([
15 | {
16 | base: 'api/petstore',
17 | schema: '../schemas/v3.0/petstore-expanded.yaml',
18 | sidebar: { collapsed: false, label: 'Petstore' },
19 | },
20 | {
21 | base: 'api/1password',
22 | schema:
23 | 'https://raw.githubusercontent.com/APIs-guru/openapi-directory/gh-pages/v2/specs/1password.local/connect/1.5.7/openapi.yaml',
24 | sidebar: { label: '1Password Connect' },
25 | },
26 | {
27 | base: 'api/giphy',
28 | schema:
29 | 'https://raw.githubusercontent.com/APIs-guru/openapi-directory/gh-pages/v2/specs/giphy.com/1.0/openapi.yaml',
30 | sidebar: { label: 'Giphy' },
31 | },
32 | {
33 | base: 'api/v3/petstore-simple',
34 | schema: '../schemas/v3.0/petstore.json',
35 | sidebar: { label: 'Petstore v3.0 (simple)', operations: { labels: 'operationId' } },
36 | },
37 | {
38 | base: 'api/v2/petstore-simple',
39 | schema: '../schemas/v2.0/petstore-simple.yaml',
40 | sidebar: { label: 'Petstore v2.0 (simple)' },
41 | },
42 | {
43 | base: 'api/v3/animals',
44 | schema: '../schemas/v3.0/animals.yaml',
45 | sidebar: { label: 'Animals v3.0', operations: { sort: 'alphabetical' }, tags: { sort: 'alphabetical' } },
46 | },
47 | {
48 | base: 'api/v2/animals',
49 | schema: '../schemas/v2.0/animals.yaml',
50 | sidebar: { label: 'Animals v2.0' },
51 | },
52 | {
53 | base: 'api/v3/recursive',
54 | schema: '../schemas/v3.0/recursive.yaml',
55 | sidebar: { label: 'Recursion v3.0' },
56 | },
57 | {
58 | base: 'api/v3/recursive-simple',
59 | schema: '../schemas/v3.0/recursive-simple.yaml',
60 | sidebar: { label: 'Simple Recursion v3.0' },
61 | },
62 | ]),
63 | starlightOpenAPIDocsDemo(),
64 | ],
65 | sidebar: [
66 | {
67 | label: 'Start Here',
68 | items: [
69 | { label: 'Getting Started', link: '/getting-started/' },
70 | { label: 'Configuration', link: '/configuration/' },
71 | ],
72 | },
73 | {
74 | label: 'Resources',
75 | items: [
76 | { label: 'Showcase', link: '/resources/showcase/' },
77 | { label: 'Plugins and Tools', link: '/resources/starlight/' },
78 | ],
79 | },
80 | {
81 | label: 'Demo',
82 | items: openAPISidebarGroups,
83 | },
84 | ],
85 | social: [
86 | { href: 'https://bsky.app/profile/hideoo.dev', icon: 'blueSky', label: 'Bluesky' },
87 | { href: 'https://github.com/HiDeoo/starlight-openapi', icon: 'github', label: 'GitHub' },
88 | ],
89 | title: 'Starlight OpenAPI',
90 | }),
91 | ],
92 | image: { service: { entrypoint: 'astro/assets/services/sharp' } },
93 | })
94 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "starlight-openapi-docs",
3 | "version": "0.8.3",
4 | "license": "MIT",
5 | "description": "Starlight plugin to generate documentation from OpenAPI/Swagger specifications.",
6 | "author": "HiDeoo (https://hideoo.dev)",
7 | "type": "module",
8 | "scripts": {
9 | "dev": "astro dev",
10 | "start": "astro dev",
11 | "build": "astro build",
12 | "preview": "astro preview",
13 | "astro": "astro",
14 | "lint": "eslint . --cache --max-warnings=0"
15 | },
16 | "dependencies": {
17 | "@astrojs/starlight": "^0.34.0",
18 | "@hideoo/starlight-plugins-docs-components": "^0.4.0",
19 | "astro": "^5.7.4",
20 | "sharp": "^0.33.5",
21 | "starlight-openapi": "workspace:*",
22 | "starlight-openapi-docs-demo": "workspace:*"
23 | },
24 | "engines": {
25 | "node": ">=18.17.1"
26 | },
27 | "packageManager": "pnpm@8.6.12",
28 | "private": true,
29 | "sideEffects": false,
30 | "keywords": [
31 | "starlight",
32 | "plugin",
33 | "openapi",
34 | "swagger",
35 | "documentation",
36 | "astro"
37 | ],
38 | "homepage": "https://github.com/HiDeoo/starlight-openapi",
39 | "repository": {
40 | "type": "git",
41 | "url": "https://github.com/HiDeoo/starlight-openapi.git",
42 | "directory": "docs"
43 | },
44 | "bugs": "https://github.com/HiDeoo/starlight-openapi/issues"
45 | }
46 |
--------------------------------------------------------------------------------
/docs/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/src/assets/showcase/classchartsapi.github.io.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HiDeoo/starlight-openapi/ed6e878f3b873df4d63f616c436003907537f519/docs/src/assets/showcase/classchartsapi.github.io.png
--------------------------------------------------------------------------------
/docs/src/assets/showcase/docs.garajonai.com.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HiDeoo/starlight-openapi/ed6e878f3b873df4d63f616c436003907537f519/docs/src/assets/showcase/docs.garajonai.com.png
--------------------------------------------------------------------------------
/docs/src/assets/showcase/docs.taiko.xyz.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HiDeoo/starlight-openapi/ed6e878f3b873df4d63f616c436003907537f519/docs/src/assets/showcase/docs.taiko.xyz.png
--------------------------------------------------------------------------------
/docs/src/assets/showcase/fxhu.kripod.dev.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HiDeoo/starlight-openapi/ed6e878f3b873df4d63f616c436003907537f519/docs/src/assets/showcase/fxhu.kripod.dev.png
--------------------------------------------------------------------------------
/docs/src/assets/showcase/openpayments.dev.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HiDeoo/starlight-openapi/ed6e878f3b873df4d63f616c436003907537f519/docs/src/assets/showcase/openpayments.dev.png
--------------------------------------------------------------------------------
/docs/src/assets/showcase/openpodcastapi.org.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HiDeoo/starlight-openapi/ed6e878f3b873df4d63f616c436003907537f519/docs/src/assets/showcase/openpodcastapi.org.png
--------------------------------------------------------------------------------
/docs/src/content.config.ts:
--------------------------------------------------------------------------------
1 | import { docsLoader } from '@astrojs/starlight/loaders'
2 | import { docsSchema } from '@astrojs/starlight/schema'
3 | import { defineCollection } from 'astro:content'
4 |
5 | export const collections = {
6 | docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
7 | }
8 |
--------------------------------------------------------------------------------
/docs/src/content/docs/configuration.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Configuration
3 | description: An overview of all the configuration options supported by the Starlight OpenAPI plugin.
4 | tableOfContents:
5 | maxHeadingLevel: 5
6 | ---
7 |
8 | The Starlight OpenAPI plugin can be configured inside the `astro.config.mjs` configuration file of your project:
9 |
10 | ```js {11}
11 | // astro.config.mjs
12 | import starlight from '@astrojs/starlight'
13 | import { defineConfig } from 'astro/config'
14 | import starlightOpenAPI, { openAPISidebarGroups } from 'starlight-openapi'
15 |
16 | export default defineConfig({
17 | integrations: [
18 | starlight({
19 | plugins: [
20 | starlightOpenAPI([
21 | // Configuration options go here.
22 | ]),
23 | ],
24 | title: 'My Docs',
25 | }),
26 | ],
27 | })
28 | ```
29 |
30 | ## Plugin configuration
31 |
32 | The Starlight OpenAPI plugin accepts an array of objects where each object represents a configuration for a specific OpenAPI/Swagger schema.
33 |
34 | A configuration object can have the following properties:
35 |
36 | ### `base` (required)
37 |
38 | **Type:** `string`
39 |
40 | The base path containing the generated documentation, e.g. `'api/petstore'`.
41 |
42 | ### `schema` (required)
43 |
44 | **Type:** `string`
45 |
46 | The OpenAPI/Swagger schema path or URL.
47 |
48 | ### `sidebar`
49 |
50 | The generated sidebar group configuration which accepts the following properties:
51 |
52 | #### `collapsed`
53 |
54 | **Type:** `boolean`
55 | **Default:** `true`
56 |
57 | Wheter the generated documentation sidebar group should be collapsed by default or not.
58 |
59 | #### `label`
60 |
61 | **Type:** `string`
62 | **Default:** the OpenAPI document title
63 |
64 | The generated documentation sidebar group label.
65 |
66 | #### `operations`
67 |
68 | The generated documentation operations sidebar links configuration which accepts the following properties:
69 |
70 | ##### `badges`
71 |
72 | **Type:** `boolean`
73 | **Default:** `false`
74 |
75 | Defines if the sidebar should display badges next to operation links with the associated HTTP method.
76 |
77 | ##### `labels`
78 |
79 | **Type:** `'operationId' | 'summary'`
80 | **Default:** `'summary'`
81 |
82 | Whether the operation sidebar link labels should use the operation ID or summary.
83 |
84 | By default, the summary is used as the operation sidebar label and falls back to the operation ID if no summary is provided.
85 | Setting this option to `'operationId'` will always use the operation ID as the operation sidebar label.
86 |
87 | ##### `sort`
88 |
89 | **Type:** `'alphabetical' | 'document'`
90 | **Default:** `'document'`
91 |
92 | Defines the sorting method for the operation sidebar links.
93 |
94 | By default, the operation sidebar links are sorted in the order they appear in the OpenAPI document.
95 |
96 | #### `tags`
97 |
98 | The generated documentation tags sidebar groups configuration which accepts the following properties:
99 |
100 | ##### `sort`
101 |
102 | **Type:** `'alphabetical' | 'document'`
103 | **Default:** `'document'`
104 |
105 | Defines the sorting method for the tag sidebar groups.
106 |
107 | By default, the tag sidebar groups are sorted in the order they appear in the OpenAPI document.
108 |
109 | ## Multiple schemas
110 |
111 | You can generate documentation for multiple OpenAPI/Swagger schemas by passing multiple objects to the plugin configuration.
112 |
113 | ```js {11-21}
114 | // astro.config.mjs
115 | import starlight from '@astrojs/starlight'
116 | import { defineConfig } from 'astro/config'
117 | import starlightOpenAPI, { openAPISidebarGroups } from 'starlight-openapi'
118 |
119 | export default defineConfig({
120 | integrations: [
121 | starlight({
122 | plugins: [
123 | starlightOpenAPI([
124 | {
125 | base: 'api/petstore',
126 | schema: '../schemas/api-schema.yaml',
127 | sidebar: { label: 'My API' },
128 | },
129 | {
130 | base: 'api/1password',
131 | schema:
132 | 'https://raw.githubusercontent.com/APIs-guru/openapi-directory/gh-pages/v2/specs/1password.local/connect/1.5.7/openapi.yaml',
133 | sidebar: { label: '1Password Connect' },
134 | },
135 | ]),
136 | ],
137 | title: 'My Docs',
138 | }),
139 | ],
140 | })
141 | ```
142 |
143 | ## Sidebar groups
144 |
145 | The `openAPISidebarGroups` export can be used in your Starlight [sidebar configuration](https://starlight.astro.build/reference/configuration/#sidebar) to add the generated documentation sidebar groups to the sidebar.
146 |
147 | ```js {24-25} "{ openAPISidebarGroups }"
148 | // astro.config.mjs
149 | import starlight from '@astrojs/starlight'
150 | import { defineConfig } from 'astro/config'
151 | import starlightOpenAPI, { openAPISidebarGroups } from 'starlight-openapi'
152 |
153 | export default defineConfig({
154 | integrations: [
155 | starlight({
156 | plugins: [
157 | // Generate the OpenAPI documentation pages.
158 | starlightOpenAPI([
159 | {
160 | base: 'api',
161 | schema: '../schemas/api-schema.yaml',
162 | sidebar: { label: 'My API' },
163 | },
164 | ]),
165 | ],
166 | sidebar: [
167 | {
168 | label: 'Guides',
169 | items: [{ label: 'Example Guide', link: '/guides/example/' }],
170 | },
171 | // Add the generated sidebar groups to the sidebar.
172 | ...openAPISidebarGroups,
173 | ],
174 | title: 'My Docs',
175 | }),
176 | ],
177 | })
178 | ```
179 |
--------------------------------------------------------------------------------
/docs/src/content/docs/getting-started.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Getting Started
3 | description: Learn how to generate documentation from OpenAPI/Swagger specifications using the Starlight OpenAPI plugin.
4 | ---
5 |
6 | A [Starlight](https://starlight.astro.build) plugin to generate documentation from OpenAPI/Swagger specifications.
7 |
8 | - Support for [Swagger 2.0](https://swagger.io/specification/v2/), [OpenAPI 3.0](https://swagger.io/specification/v3/) and [OpenAPI 3.1](https://swagger.io/specification/) specifications.
9 | - Support for local and remote schemas.
10 | - Configurable sidebar label and sidebar group collapsing.
11 |
12 | ## Prerequisites
13 |
14 | You will need to have a Starlight website set up.
15 | If you don't have one yet, you can follow the ["Getting Started"](https://starlight.astro.build/getting-started) guide in the Starlight docs to create one.
16 |
17 | ## Installation
18 |
19 | import { Steps } from '@astrojs/starlight/components'
20 | import { PackageManagers } from '@hideoo/starlight-plugins-docs-components'
21 |
22 |
23 |
24 | 1. Starlight OpenAPI is a Starlight [plugin](https://starlight.astro.build/reference/plugins/). Install it using your favorite package manager:
25 |
26 |
27 |
28 | 2. Configure the plugin in your Starlight [configuration](https://starlight.astro.build/reference/configuration/#plugins) in the `astro.config.mjs` file.
29 |
30 | The following example shows how to specify a schema file and add the generated sidebar group to the sidebar:
31 |
32 | ```diff lang="js"
33 | // astro.config.mjs
34 | import starlight from '@astrojs/starlight'
35 | import { defineConfig } from 'astro/config'
36 | +import starlightOpenAPI, { openAPISidebarGroups } from 'starlight-openapi'
37 |
38 | export default defineConfig({
39 | integrations: [
40 | starlight({
41 | + plugins: [
42 | + // Generate the OpenAPI documentation pages.
43 | + starlightOpenAPI([
44 | + {
45 | + base: 'api',
46 | + schema: '../schemas/api-schema.yaml',
47 | + },
48 | + ]),
49 | + ],
50 | sidebar: [
51 | {
52 | label: 'Guides',
53 | items: [{ label: 'Example Guide', link: '/guides/example/' }],
54 | },
55 | + // Add the generated sidebar group to the sidebar.
56 | + ...openAPISidebarGroups,
57 | ],
58 | title: 'My Docs',
59 | }),
60 | ],
61 | })
62 | ```
63 |
64 | 3. [Start the development server](https://starlight.astro.build/getting-started/#start-the-development-server) to preview the generated documentation.
65 |
66 |
67 |
68 | The Starlight OpenAPI plugin behavior can be tweaked using various [configuration options](/configuration).
69 |
--------------------------------------------------------------------------------
/docs/src/content/docs/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Starlight OpenAPI
3 | description: Starlight plugin to generate documentation from OpenAPI/Swagger specifications.
4 | head:
5 | - tag: title
6 | content: Starlight OpenAPI
7 | template: splash
8 | editUrl: false
9 | lastUpdated: false
10 | hero:
11 | tagline: Starlight plugin to generate documentation from OpenAPI/Swagger specifications.
12 | image:
13 | html: '🧭'
14 | actions:
15 | - text: Getting Started
16 | link: /getting-started/
17 | icon: rocket
18 | - text: Demo
19 | link: /api/petstore/operations/addpet/
20 | icon: right-arrow
21 | variant: minimal
22 | ---
23 |
24 | import { Card, CardGrid } from '@astrojs/starlight/components'
25 |
26 | ## Next steps
27 |
28 |
29 |
30 | Check the [getting started guide](/getting-started/) for installation instructions.
31 |
32 |
33 | Edit your config in `astro.config.mjs`.
34 |
35 |
36 | Add your OpenAPI schema to your project.
37 |
38 |
39 | Learn more in the [Starlight OpenAPI Docs](/getting-started/).
40 |
41 |
42 |
--------------------------------------------------------------------------------
/docs/src/content/docs/resources/showcase.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Showcase
3 | description: Discover Starlight project using the Starlight OpenAPI plugin.
4 | ---
5 |
6 | import { ShowcaseIntro, Showcase } from '@hideoo/starlight-plugins-docs-components'
7 |
8 |
12 |
13 | ## Sites
14 |
15 | Starlight OpenAPI is already being used in production. These are some of the sites around the web:
16 |
17 |
51 |
52 | See all the [public project repos using Starlight OpenAPI on GitHub](https://github.com/hideoo/starlight-openapi/network/dependents).
53 |
--------------------------------------------------------------------------------
/docs/src/content/docs/resources/starlight.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Starlight Plugins and Tools
3 | description: Discover other Starlight plugins, components and tools developed by HiDeoo.
4 | ---
5 |
6 | import { ResourcesIntro, Resources } from '@hideoo/starlight-plugins-docs-components'
7 |
8 |
9 |
10 | ## Plugins
11 |
12 |
13 |
14 | ## Components
15 |
16 |
17 |
18 | ## Tools
19 |
20 |
21 |
--------------------------------------------------------------------------------
/docs/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/docs/src/styles/custom.css:
--------------------------------------------------------------------------------
1 | .hero-html {
2 | --size: 10rem;
3 |
4 | font-size: var(--size);
5 | justify-content: center;
6 | line-height: var(--size);
7 | }
8 |
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import hideoo from '@hideoo/eslint-config'
2 |
3 | export default hideoo([
4 | {
5 | rules: {
6 | '@typescript-eslint/no-duplicate-type-constituents': 'off',
7 | },
8 | },
9 | ])
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "starlight-openapi-monorepo",
3 | "version": "0.8.3",
4 | "license": "MIT",
5 | "description": "Starlight plugin to generate documentation from OpenAPI/Swagger specifications.",
6 | "author": "HiDeoo (https://hideoo.dev)",
7 | "type": "module",
8 | "scripts": {
9 | "test": "pnpm --stream -r test",
10 | "lint": "astro check --noSync --minimumSeverity warning && pnpm -r lint",
11 | "format": "prettier -w --cache --ignore-unknown .",
12 | "version": "pnpm changeset version && pnpm i --no-frozen-lockfile"
13 | },
14 | "devDependencies": {
15 | "@astrojs/check": "^0.9.4",
16 | "@changesets/changelog-github": "^0.5.0",
17 | "@changesets/cli": "^2.27.10",
18 | "@hideoo/eslint-config": "^4.0.0",
19 | "@hideoo/prettier-config": "^2.0.0",
20 | "@hideoo/tsconfig": "^2.0.1",
21 | "astro": "^5.7.4",
22 | "eslint": "^9.17.0",
23 | "prettier": "^3.4.2",
24 | "prettier-plugin-astro": "^0.14.1",
25 | "typescript": "^5.7.2"
26 | },
27 | "engines": {
28 | "node": ">=18.17.1"
29 | },
30 | "packageManager": "pnpm@8.6.12",
31 | "private": true,
32 | "sideEffects": false,
33 | "keywords": [
34 | "starlight",
35 | "plugin",
36 | "openapi",
37 | "swagger",
38 | "documentation",
39 | "astro"
40 | ],
41 | "homepage": "https://github.com/HiDeoo/starlight-openapi",
42 | "repository": {
43 | "type": "git",
44 | "url": "https://github.com/HiDeoo/starlight-openapi.git"
45 | },
46 | "bugs": "https://github.com/HiDeoo/starlight-openapi/issues"
47 | }
48 |
--------------------------------------------------------------------------------
/packages/starlight-openapi-docs-demo/index.ts:
--------------------------------------------------------------------------------
1 | import type { StarlightPlugin } from '@astrojs/starlight/types'
2 |
3 | export default function starlightOpenAPIDocsDemoPlugin(): StarlightPlugin {
4 | return {
5 | name: 'starlight-openapi-docs-demo-plugin',
6 | hooks: {
7 | 'config:setup': ({ addRouteMiddleware }) => {
8 | if (process.env['TEST']) return
9 |
10 | addRouteMiddleware({ entrypoint: 'starlight-openapi-docs-demo/middleware', order: 'post' })
11 | },
12 | },
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/packages/starlight-openapi-docs-demo/middleware.ts:
--------------------------------------------------------------------------------
1 | import { defineRouteMiddleware, type StarlightRouteData } from '@astrojs/starlight/route-data'
2 |
3 | export const onRequest = defineRouteMiddleware((context) => {
4 | const { starlightRoute } = context.locals
5 | const { sidebar } = starlightRoute
6 |
7 | starlightRoute.sidebar = sidebar.map((item) => {
8 | if (isSidebarGroup(item) && item.label === 'Demo') {
9 | return { ...item, entries: item.entries.slice(0, 3) }
10 | }
11 |
12 | return item
13 | })
14 | })
15 |
16 | function isSidebarGroup(item: SidebarItem): item is SidebarGroup {
17 | return item.type === 'group'
18 | }
19 |
20 | type SidebarItem = StarlightRouteData['sidebar'][number]
21 | export type SidebarGroup = Extract
22 |
--------------------------------------------------------------------------------
/packages/starlight-openapi-docs-demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "starlight-openapi-docs-demo",
3 | "version": "0.1.0",
4 | "license": "MIT",
5 | "author": "HiDeoo (https://hideoo.dev)",
6 | "type": "module",
7 | "exports": {
8 | ".": "./index.ts",
9 | "./middleware": "./middleware.ts",
10 | "./package.json": "./package.json"
11 | },
12 | "peerDependencies": {
13 | "@astrojs/starlight": ">=0.34.0"
14 | },
15 | "engines": {
16 | "node": ">=18.17.1"
17 | },
18 | "packageManager": "pnpm@8.6.12",
19 | "private": true,
20 | "sideEffects": false,
21 | "keywords": [
22 | "starlight",
23 | "plugin",
24 | "openapi",
25 | "swagger",
26 | "documentation",
27 | "astro"
28 | ],
29 | "homepage": "https://github.com/HiDeoo/starlight-openapi",
30 | "repository": {
31 | "type": "git",
32 | "url": "https://github.com/HiDeoo/starlight-openapi.git",
33 | "directory": "packages/starlight-openapi-docs-demo"
34 | },
35 | "bugs": "https://github.com/HiDeoo/starlight-openapi/issues"
36 | }
37 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/.npmignore:
--------------------------------------------------------------------------------
1 | .eslintcache
2 | .prettierignore
3 | playwright.config.ts
4 | tests
5 | test-results
6 | tsconfig.json
7 | tsconfig.tsbuildinfo
8 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/README.md:
--------------------------------------------------------------------------------
1 |
2 |
starlight-openapi 🧭
3 |
Starlight plugin to generate documentation from OpenAPI/Swagger specifications.
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
20 |
21 | ## Getting Started
22 |
23 | Want to get started immediately? Check out the [getting started guide](https://starlight-openapi.vercel.app/getting-started/) or check out the [demo](https://starlight-openapi.vercel.app/api/petstore/operations/addpet/) to see the plugin in action.
24 |
25 | ## Features
26 |
27 | A [Starlight](https://starlight.astro.build) plugin to generate documentation from OpenAPI/Swagger specifications.
28 |
29 | - Support for [Swagger 2.0](https://swagger.io/specification/v2/), [OpenAPI 3.0](https://swagger.io/specification/v3/) and [OpenAPI 3.1](https://swagger.io/specification/) specifications.
30 | - Support for local and remote schemas.
31 | - Configurable sidebar label and sidebar group collapsing.
32 |
33 | ## License
34 |
35 | Licensed under the MIT License, Copyright © HiDeoo.
36 |
37 | See [LICENSE](https://github.com/HiDeoo/starlight-openapi/blob/main/LICENSE) for more information.
38 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/Content.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Content } from '../libs/content'
3 | import { isExamples } from '../libs/example'
4 | import { isSchemaObject } from '../libs/schemaObject'
5 |
6 | import ContentPicker from './ContentPicker.astro'
7 | import Schema from './schema/Schema.astro'
8 |
9 | interface Props {
10 | content: Content
11 | }
12 |
13 | const { content } = Astro.props
14 | ---
15 |
16 |
17 | {
18 | Object.entries(content).map(([type, media], index) => {
19 | return (
20 | 0} role="tabpanel">
21 |
27 |
28 | )
29 | })
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/ContentPicker.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Select from './Select.astro'
3 |
4 | interface Props {
5 | label: string
6 | types: string[] | undefined
7 | }
8 |
9 | const { label, types } = Astro.props
10 | ---
11 |
12 |
13 |
14 |
15 |
16 |
17 |
47 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/Deprecated.astro:
--------------------------------------------------------------------------------
1 | ---
2 | interface Props {
3 | deprecated: boolean | undefined
4 | }
5 |
6 | const { deprecated } = Astro.props
7 | ---
8 |
9 | {deprecated && Deprecated
}
10 |
11 |
34 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/ExternalDocs.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
3 |
4 | interface Props {
5 | docs:
6 | | OpenAPIV2.ExternalDocumentationObject
7 | | OpenAPIV3.ExternalDocumentationObject
8 | | OpenAPIV3_1.ExternalDocumentationObject
9 | | undefined
10 | }
11 |
12 | const { docs } = Astro.props
13 | ---
14 |
15 | {
16 | docs && (
17 |
18 | {docs.description ?? 'More information'}
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/Heading.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import AnchorHeading from '@astrojs/starlight/components/AnchorHeading.astro'
3 | import starlightConfig from 'virtual:starlight/user-config'
4 |
5 | export interface Props {
6 | id: string
7 | level: number
8 | }
9 |
10 | const { id, level } = Astro.props
11 | const headingLevel = Math.min(Math.max(level, 1), 6) as 1 | 2 | 3 | 4 | 5 | 6
12 | const Element = `h${headingLevel}` as const
13 | ---
14 |
15 | {
16 | starlightConfig.markdown.headingLinks ? (
17 |
18 |
19 |
20 | ) : (
21 |
22 |
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/Items.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getBound, getType, type Items } from '../libs/items'
3 | import { isSchemaObjectObject, type SchemaObject as SchemaObjectType } from '../libs/schemaObject'
4 |
5 | import SchemaObject from './schema/SchemaObject.astro'
6 | import Tag from './Tag.astro'
7 | import Tags from './Tags.astro'
8 |
9 | interface Props {
10 | hideExample?: boolean
11 | items: Items
12 | negated?: boolean | undefined
13 | nullable?: boolean | undefined
14 | parents?: SchemaObjectType[]
15 | type?: string | undefined
16 | }
17 |
18 | const { hideExample, items, negated, nullable, parents = [], type } = Astro.props
19 |
20 | const enumItems = items.enum ?? items.items?.enum
21 | ---
22 |
23 | {
24 | items.type && (
25 |
26 |
27 | {negated && 'not '}
28 | {getType(items)}
29 |
30 | {items.format && format: {items.format}}
31 |
32 | )
33 | }
34 | = ${items.minLength} characters`,
41 | items.maxLength && `<= ${items.maxLength} characters`,
42 | items.minItems && `>= ${items.minItems} items`,
43 | items.maxItems && `<= ${items.maxItems} items`,
44 | items.pattern && `/${items.pattern}/`,
45 | items.multipleOf && `multiple of ${items.multipleOf}`,
46 | items.uniqueItems && 'unique items',
47 | ]}
48 | />
49 |
50 | {enumItems && }
51 | {
52 | items.items && isSchemaObjectObject(items.items) && (
53 |
54 | )
55 | }
56 |
57 |
64 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/Key.astro:
--------------------------------------------------------------------------------
1 | ---
2 | interface Props {
3 | additional?: boolean
4 | deprecated?: boolean
5 | name: string
6 | required?: boolean | undefined
7 | }
8 |
9 | const { additional, deprecated, name, required } = Astro.props
10 | ---
11 |
12 |
13 |
14 |
{additional ? {name} : deprecated ? {name} : name}
15 | {required &&
required
}
16 | {deprecated &&
deprecated
}
17 | {additional &&
additional properties
}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
60 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/Md.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { transformMarkdown } from '../libs/markdown'
3 | import { capitalize } from '../libs/utils'
4 |
5 | interface Props {
6 | text: string | undefined
7 | }
8 |
9 | const { text } = Astro.props
10 |
11 | const code = text && text.length > 0 ? await transformMarkdown(capitalize(text)) : ''
12 | ---
13 |
14 | {text && text.length > 0 && }
15 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/OperationTag.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { OperationTag } from '../libs/operation'
3 |
4 | import ExternalDocs from './ExternalDocs.astro'
5 | import Heading from './Heading.astro'
6 | import Md from './Md.astro'
7 |
8 | interface Props {
9 | tag: OperationTag
10 | }
11 |
12 | const { tag } = Astro.props
13 | ---
14 |
15 | {tag.name}
16 |
17 | {tag.description && }
18 | {tag.externalDocs && }
19 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/Overview.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getOpenAPIVersion, getSummary } from '../libs/document'
3 | import type { Schema } from '../libs/schema'
4 | import { getSecurityDefinitions } from '../libs/security'
5 | import { interspece } from '../libs/utils'
6 |
7 | import ExternalDocs from './ExternalDocs.astro'
8 | import Heading from './Heading.astro'
9 | import Md from './Md.astro'
10 | import SecurityDefinitions from './security/SecurityDefinitions.astro'
11 | import Text from './Text.astro'
12 |
13 | interface Props {
14 | schema: Schema
15 | }
16 |
17 | const {
18 | schema: { document },
19 | } = Astro.props
20 |
21 | const summary = getSummary(document)
22 | const securityDefinitions = getSecurityDefinitions(document)
23 | const contacts = [document.info.contact?.url, document.info.contact?.email].filter(
24 | (contact): contact is string => !!contact,
25 | )
26 | ---
27 |
28 | {summary}
29 |
30 | {document.info.title} ({document.info.version})
31 |
32 |
33 |
34 |
35 | {
36 | contacts.length > 0 && (
37 | -
38 | {document.info.contact?.name ?? 'Contact'}:{' '}
39 | {interspece(
40 | ' - ',
41 | contacts.map((contact, index) => {contact}),
42 | )}
43 |
44 | )
45 | }
46 | {
47 | document.info.license && (
48 | -
49 | License:{' '}
50 | {document.info.license.url ? (
51 | {document.info.license.name}
52 | ) : (
53 | document.info.license.name
54 | )}
55 |
56 | )
57 | }
58 | {
59 | document.info.termsOfService && (
60 | -
61 | Terms of Service
62 |
63 | )
64 | }
65 | - OpenAPI version: {getOpenAPIVersion(document)}
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/RequestBody.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Operation } from '../libs/operation'
3 | import {
4 | getOpenAPIV2OperationConsumes,
5 | getOpenAPIV2RequestBodyParameter,
6 | getOpenAPIV3RequestBody,
7 | } from '../libs/requestBody'
8 | import type { Schema } from '../libs/schema'
9 |
10 | import Content from './Content.astro'
11 | import Md from './Md.astro'
12 | import SchemaObject from './schema/SchemaObject.astro'
13 | import Section, { type SectionHeadingProps } from './Section.astro'
14 | import Select from './Select.astro'
15 |
16 | interface Props extends SectionHeadingProps {
17 | operation: Operation
18 | schema: Schema
19 | }
20 |
21 | const { level = 2, operation, prefix, schema } = Astro.props
22 |
23 | const openAPIV2RequestBodyParameter = getOpenAPIV2RequestBodyParameter(operation)
24 | const openAPIV3RequestBody = getOpenAPIV3RequestBody(operation)
25 |
26 | const hasRequestBody = openAPIV2RequestBodyParameter !== undefined || openAPIV3RequestBody !== undefined
27 | const description = openAPIV2RequestBodyParameter
28 | ? openAPIV2RequestBodyParameter.description
29 | : openAPIV3RequestBody?.description
30 |
31 | const consumes = getOpenAPIV2OperationConsumes(schema, operation)
32 | ---
33 |
34 | {
35 | hasRequestBody && (
36 |
37 | {openAPIV3RequestBody?.required && (
38 |
39 | required
40 |
41 | )}
42 |
43 | {openAPIV2RequestBodyParameter ? (
44 | <>
45 |
46 |
47 | >
48 | ) : openAPIV3RequestBody ? (
49 |
50 | ) : null}
51 |
52 | )
53 | }
54 |
55 |
63 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/Route.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'
3 | import OpenAPIParser from '@readme/openapi-parser'
4 | import type { InferGetStaticPropsType } from 'astro'
5 |
6 | import { getSchemaStaticPaths } from '../libs/route'
7 | import { getPageProps } from '../libs/starlight'
8 |
9 | import Operation from './operation/Operation.astro'
10 | import OperationTag from './OperationTag.astro'
11 | import Overview from './Overview.astro'
12 |
13 | export const prerender = true
14 |
15 | export function getStaticPaths() {
16 | return getSchemaStaticPaths()
17 | }
18 |
19 | type Props = InferGetStaticPropsType
20 |
21 | const { schema, type } = Astro.props
22 |
23 | schema.document = await OpenAPIParser.dereference(schema.document)
24 |
25 | const isOverview = type === 'overview'
26 | const isOperationTag = type === 'operation-tag'
27 |
28 | const title = isOverview || isOperationTag ? 'Overview' : Astro.props.operation.title
29 | ---
30 |
31 |
39 | {
40 | isOverview ? (
41 |
42 | ) : isOperationTag ? (
43 |
44 | ) : (
45 |
46 | )
47 | }
48 |
49 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/Section.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { slug } from '../libs/path'
3 |
4 | import Heading from './Heading.astro'
5 |
6 | export interface SectionHeadingProps {
7 | level?: number
8 | prefix?: string | undefined
9 | }
10 |
11 | interface Props extends SectionHeadingProps {
12 | empty?: boolean
13 | title: string
14 | }
15 |
16 | const { empty, level = 3, prefix = '', title } = Astro.props
17 | ---
18 |
19 |
20 |
21 | {title}
22 |
23 | {
24 | Astro.slots.has('pre-panel') && (
25 |
26 |
27 |
28 | )
29 | }
30 | {
31 | !empty && (
32 |
33 |
34 |
35 | )
36 | }
37 | {
38 | Astro.slots.has('post-panel') && (
39 |
40 |
41 |
42 | )
43 | }
44 |
45 |
46 |
74 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/Select.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Icon } from '@astrojs/starlight/components'
3 |
4 | interface Props {
5 | label: string
6 | options: string[] | undefined
7 | }
8 |
9 | const { label, options } = Astro.props
10 | ---
11 |
12 | {
13 | options && options.length > 0 && (
14 |
23 | )
24 | }
25 |
26 |
59 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/Tag.astro:
--------------------------------------------------------------------------------
1 | {' '}
2 |
3 |
4 |
5 |
6 |
15 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/Tags.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Tag from './Tag.astro'
3 |
4 | interface Props {
5 | label?: string
6 | tags: unknown[]
7 | }
8 |
9 | const { label, tags: allTags } = Astro.props
10 |
11 | const tags = allTags.filter((tag): tag is string | number => typeof tag === 'string' || typeof tag === 'number')
12 | ---
13 |
14 | {
15 | tags.length > 0 && (
16 |
17 | {label && <>{label}>}
18 | {tags.map((tag) => {
19 | if (typeof tag === 'string' && tag.length === 0) {
20 | return ""
21 | }
22 |
23 | return {tag}
24 | })}
25 |
26 | )
27 | }
28 |
29 |
38 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/Text.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Tag from './Tag.astro'
3 |
4 | interface Props {
5 | label?: string
6 | tag?: boolean
7 | }
8 |
9 | const { label, tag } = Astro.props
10 |
11 | const text: string | undefined = await Astro.slots.render('default')
12 | ---
13 |
14 | {
15 | text && text.length > 0 && (
16 |
17 | {label && {label}: }
18 | {tag ? (
19 |
20 |
21 |
22 | ) : (
23 |
24 | )}
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/callback/CallbackOperation.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Callback } from '../../libs/callback'
3 | import type { CallbackOperation } from '../../libs/operation'
4 | import { getParametersByLocation } from '../../libs/parameter'
5 | import type { Schema } from '../../libs/schema'
6 | import Deprecated from '../Deprecated.astro'
7 | import ExternalDocs from '../ExternalDocs.astro'
8 | import Md from '../Md.astro'
9 | import OperationDescription from '../operation/OperationDescription.astro'
10 | import Parameters from '../parameter/Parameters.astro'
11 | import RequestBody from '../RequestBody.astro'
12 | import Responses from '../response/Responses.astro'
13 | import type { SectionHeadingProps } from '../Section.astro'
14 | import Security from '../security/Security.astro'
15 |
16 | interface Props {
17 | callback: Callback
18 | id: string
19 | operation: CallbackOperation
20 | schema: Schema
21 | url: string
22 | }
23 |
24 | const { callback, id, operation: callbackOperation, schema, url } = Astro.props
25 | const { operation } = callbackOperation
26 |
27 | const sectionHeadingProps: SectionHeadingProps = { level: 4, prefix: id }
28 | const parameters = getParametersByLocation(operation.parameters, callback.parameters)
29 | ---
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/callback/Callbacks.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getCallbackObject, getCallbacks } from '../../libs/callback'
3 | import { getCallbackOperations, type Operation as CallbacksOperation } from '../../libs/operation'
4 | import { slug } from '../../libs/path'
5 | import type { Schema } from '../../libs/schema'
6 | import CallbackOperation from '../callback/CallbackOperation.astro'
7 | import Heading from '../Heading.astro'
8 | import Md from '../Md.astro'
9 |
10 | interface Props {
11 | operation: CallbacksOperation
12 | schema: Schema
13 | }
14 |
15 | const { operation, schema } = Astro.props
16 |
17 | const callbacks = getCallbacks(operation)
18 | const identifiers = Object.keys(callbacks ?? {})
19 | ---
20 |
21 | {
22 | callbacks && identifiers.length > 0 && (
23 | <>
24 |
25 | Callbacks
26 |
27 | {identifiers.map((identifier) => {
28 | const callbackObject = getCallbackObject(callbacks, identifier)
29 | const id = slug(identifier)
30 |
31 | return (
32 | <>
33 |
34 | {identifier}
35 |
36 | {Object.entries(callbackObject).map(([url, callback]) => {
37 | const operations = getCallbackOperations(callback)
38 |
39 | return (
40 | <>
41 | {callback.summary && {callback.summary}
}
42 |
43 | {operations.map((operation) => (
44 |
45 | ))}
46 | >
47 | )
48 | })}
49 | >
50 | )
51 | })}
52 | >
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/example/Example.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Code } from '@astrojs/starlight/components'
3 | import { parseTemplate, type PrimitiveValue } from 'url-template'
4 |
5 | import type { ExampleV3 } from '../../libs/example'
6 | import type { Parameter } from '../../libs/parameter'
7 | import Md from '../Md.astro'
8 | import Text from '../Text.astro'
9 |
10 | interface Props {
11 | example?: ExampleV3
12 | parameter?: Parameter | undefined
13 | raw?: ExampleV3['value']
14 | type?: string | undefined
15 | }
16 |
17 | const { example, parameter, raw, type } = Astro.props
18 |
19 | const exampleToRender = raw === undefined ? example : { value: raw }
20 |
21 | function getExampleValue(value: unknown): string {
22 | return parameter?.in === 'query' ? getQueryParameterValue(parameter, value) : getFallbackValue(value)
23 | }
24 |
25 | function getFallbackValue(value: unknown): string {
26 | switch (typeof value) {
27 | case 'string': {
28 | return value
29 | }
30 | case 'boolean':
31 | case 'number': {
32 | return value.toString()
33 | }
34 | default: {
35 | return JSON.stringify(value, null, 2)
36 | }
37 | }
38 | }
39 |
40 | function getQueryParameterValue(parameter: Parameter, value: unknown): string {
41 | switch (parameter.style) {
42 | case 'deepObject': {
43 | if (!value || typeof value !== 'object' || !parameter.explode) {
44 | return getFallbackValue(value)
45 | }
46 |
47 | return `?${Object.keys(value)
48 | .map((key) => `${parameter.name}[${key}]=${value[key as keyof typeof value]}`)
49 | .join('&')}`
50 | }
51 | case 'form': {
52 | return getFormValue(parameter, value)
53 | }
54 | case 'pipeDelimited':
55 | case 'spaceDelimited': {
56 | if (!Array.isArray(value)) {
57 | return getFallbackValue(value)
58 | }
59 |
60 | return parameter.explode
61 | ? getFormValue(parameter, value)
62 | : `?${parameter.name}=${value.join(parameter.style === 'pipeDelimited' ? '|' : '%20')}`
63 | }
64 | default: {
65 | return getFallbackValue(value)
66 | }
67 | }
68 | }
69 |
70 | function getFormValue(parameter: Parameter, value: unknown) {
71 | // https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.8
72 | return parseTemplate(`{?${parameter.name}${parameter.explode ? '*' : ''}}`).expand({
73 | [parameter.name]: value as PrimitiveValue,
74 | })
75 | }
76 |
77 | function getExampleLang(code: string, type: string | undefined) {
78 | switch (type) {
79 | case 'application/json': {
80 | return 'json'
81 | }
82 | default: {
83 | try {
84 | JSON.parse(code)
85 | return 'json'
86 | } catch {
87 | // Fallback to plain text when failing to parse the code as JSON.
88 | }
89 | return 'plaintext'
90 | }
91 | }
92 | }
93 |
94 | const code = exampleToRender ? getExampleValue(exampleToRender.value).trim() : ''
95 | ---
96 |
97 | {
98 | exampleToRender && (
99 | <>
100 | {exampleToRender.summary}
101 |
102 | {exampleToRender.externalValue && (
103 |
104 | {exampleToRender.externalValue}
105 |
106 | )}
107 | {code.length > 0 &&
}
108 | >
109 | )
110 | }
111 |
112 |
124 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/example/Examples.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { ExamplesV3 } from '../../libs/example'
3 | import type { Parameter } from '../../libs/parameter'
4 | import ContentPicker from '../ContentPicker.astro'
5 |
6 | import Example from './Example.astro'
7 |
8 | export interface Props {
9 | example: unknown | undefined
10 | examples: ExamplesV3 | undefined
11 | parameter?: Parameter | undefined
12 | type?: string | undefined
13 | }
14 |
15 | const { example, examples, parameter, type } = Astro.props
16 | ---
17 |
18 | {
19 | example || examples ? (
20 | examples ? (
21 | <>
22 | Examples
23 |
24 | {Object.entries(examples).map(([exampleType, data], index) => (
25 | 0} role="tabpanel">
26 |
27 |
28 | ))}
29 |
30 | >
31 | ) : (
32 | <>
33 | Example
34 |
35 | >
36 | )
37 | ) : null
38 | }
39 |
40 |
45 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/operation/Operation.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getOperationURLs, type PathItemOperation } from '../../libs/operation'
3 | import { getParametersByLocation } from '../../libs/parameter'
4 | import type { Schema } from '../../libs/schema'
5 | import Callbacks from '../callback/Callbacks.astro'
6 | import Deprecated from '../Deprecated.astro'
7 | import ExternalDocs from '../ExternalDocs.astro'
8 | import Md from '../Md.astro'
9 | import Parameters from '../parameter/Parameters.astro'
10 | import RequestBody from '../RequestBody.astro'
11 | import Responses from '../response/Responses.astro'
12 | import Security from '../security/Security.astro'
13 |
14 | import OperationDescription from './OperationDescription.astro'
15 |
16 | interface Props {
17 | operation: PathItemOperation
18 | schema: Schema
19 | }
20 |
21 | const { operation: pathItemOperation, schema } = Astro.props
22 | const { operation } = pathItemOperation
23 | const urls = getOperationURLs(schema.document, pathItemOperation)
24 | const parameters = getParametersByLocation(operation.parameters, pathItemOperation.pathItem.parameters)
25 | ---
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/operation/OperationDescription.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Icon } from '@astrojs/starlight/components'
3 |
4 | import type { OperationURL, PathItemOperation } from '../../libs/operation'
5 |
6 | import OperationMethod from './OperationMethod.astro'
7 | import OperationUrl from './OperationUrl.astro'
8 |
9 | interface Props {
10 | method: PathItemOperation['method']
11 | path: PathItemOperation['path']
12 | urls?: OperationURL[]
13 | }
14 |
15 | const { method, path, urls } = Astro.props
16 | ---
17 |
18 |
19 | {
20 | path ? (
21 | urls && urls.length > 0 ? (
22 |
23 |
24 |
25 |
26 |
27 |
28 | {urls.map(({ description, url }) => (
29 |
30 | ))}
31 |
32 |
33 | ) : (
34 |
35 |
36 |
37 | )
38 | ) : (
39 |
40 | )
41 | }
42 |
43 |
44 |
105 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/operation/OperationMethod.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { OperationHttpMethod } from '../../libs/operation'
3 |
4 | interface Props {
5 | method: OperationHttpMethod
6 | path?: string
7 | }
8 |
9 | const { method, path } = Astro.props
10 | ---
11 |
12 |
13 |
{method.toUpperCase()}
14 |
{path}
15 |
16 |
17 |
79 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/operation/OperationUrl.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { HTMLAttributes } from 'astro/types'
3 |
4 | interface Props {
5 | description: string | undefined
6 | url: string
7 | }
8 |
9 | const { description, url } = Astro.props
10 |
11 | const inputAttributes: HTMLAttributes<'input'> = {
12 | readonly: true,
13 | type: 'text',
14 | value: url,
15 | }
16 | ---
17 |
18 |
19 | {
20 | description && description.length > 0 ? (
21 |
25 | ) : (
26 |
27 | )
28 | }
29 |
30 |
31 |
52 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/parameter/Parameter.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { isOpenAPIV2Items } from '../../libs/items'
3 | import type { Parameter } from '../../libs/parameter'
4 | import { isParameterWithSchemaObject } from '../../libs/schemaObject'
5 | import Content from '../Content.astro'
6 | import Items from '../Items.astro'
7 | import Key from '../Key.astro'
8 | import Md from '../Md.astro'
9 | import Schema from '../schema/Schema.astro'
10 |
11 | interface Props {
12 | parameter: Parameter
13 | }
14 |
15 | const { parameter } = Astro.props
16 | ---
17 |
18 |
19 | {
20 | isOpenAPIV2Items(parameter) ? (
21 | <>
22 |
23 |
24 | >
25 | ) : parameter.content ? (
26 | <>
27 |
28 |
29 | >
30 | ) : isParameterWithSchemaObject(parameter) ? (
31 |
38 | ) : null
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/parameter/Parameters.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { getParametersByLocation } from '../../libs/parameter'
3 | import { capitalize } from '../../libs/utils'
4 | import Heading from '../Heading.astro'
5 | import Section, { type SectionHeadingProps } from '../Section.astro'
6 |
7 | import Parameter from './Parameter.astro'
8 |
9 | interface Props extends SectionHeadingProps {
10 | parameters: ReturnType
11 | }
12 |
13 | const { level = 2, parameters, prefix } = Astro.props
14 | ---
15 |
16 | {
17 | parameters.size > 0 && (
18 | <>
19 |
20 | Parameters
21 |
22 | {[...parameters.entries()].map(([location, parameters]) => (
23 |
24 | {[...parameters.values()].map((parameter) => (
25 |
26 | ))}
27 |
28 | ))}
29 | >
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/response/Response.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { isOpenAPIV2ResponseWithExamples } from '../../libs/example'
3 | import { isResponseWithHeaders } from '../../libs/header'
4 | import type { Operation } from '../../libs/operation'
5 | import { getOpenAPIV2OperationProduces } from '../../libs/requestBody'
6 | import {
7 | getOpenAPIV2ResponseSchema,
8 | getOpenAPIV3ResponseContent,
9 | type Response,
10 | type Responses,
11 | } from '../../libs/response'
12 | import type { Schema } from '../../libs/schema'
13 | import Content from '../Content.astro'
14 | import Md from '../Md.astro'
15 | import SchemaObject from '../schema/SchemaObject.astro'
16 | import Section, { type SectionHeadingProps } from '../Section.astro'
17 | import Select from '../Select.astro'
18 |
19 | import ResponseExamples from './ResponseExamples.astro'
20 | import ResponseHeaders from './ResponseHeaders.astro'
21 |
22 | interface Props extends SectionHeadingProps {
23 | name: keyof Responses
24 | operation: Operation
25 | response: Response
26 | schema: Schema
27 | }
28 |
29 | const { name, operation, response, schema, ...sectionHeadingProps } = Astro.props
30 |
31 | const openAPIV2ResponseSchema = getOpenAPIV2ResponseSchema(response)
32 | const openAPIV3ResponseContent = getOpenAPIV3ResponseContent(response)
33 |
34 | const produces = getOpenAPIV2OperationProduces(schema, operation)
35 |
36 | const isEmpty = !openAPIV2ResponseSchema && !openAPIV3ResponseContent
37 | ---
38 |
39 |
40 |
41 | {
42 | openAPIV2ResponseSchema ? (
43 | <>
44 |
45 |
46 | >
47 | ) : openAPIV3ResponseContent ? (
48 |
49 | ) : null
50 | }
51 | {isResponseWithHeaders(response) && }
52 | {isOpenAPIV2ResponseWithExamples(response) && }
53 |
54 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/response/ResponseExamples.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { ExamplesV2 } from '../../libs/example'
3 | import ContentPicker from '../ContentPicker.astro'
4 | import Example from '../example/Example.astro'
5 | import Section from '../Section.astro'
6 |
7 | interface Props {
8 | examples: ExamplesV2
9 | }
10 |
11 | const examples = Object.entries(Astro.props.examples)
12 | ---
13 |
14 | {
15 | examples.length > 0 && (
16 |
17 |
18 | {examples.map(([type, data], index) => (
19 | 0} role="tabpanel">
20 |
21 |
22 | ))}
23 |
24 |
25 | )
26 | }
27 |
28 |
37 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/response/ResponseHeaders.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Headers } from '../../libs/header'
3 | import { isOpenAPIV2Items } from '../../libs/items'
4 | import { isHeaderParameter } from '../../libs/parameter'
5 | import Items from '../Items.astro'
6 | import Key from '../Key.astro'
7 | import Md from '../Md.astro'
8 | import Parameter from '../parameter/Parameter.astro'
9 | import Section from '../Section.astro'
10 |
11 | interface Props {
12 | headers: Headers
13 | }
14 |
15 | const headers = Object.entries(Astro.props.headers).filter(([key]) => key.toLowerCase() !== 'content-type')
16 | ---
17 |
18 | {
19 | headers.length > 0 && (
20 |
21 | {headers.map(([name, header]) =>
22 | isOpenAPIV2Items(header) ? (
23 |
24 |
25 |
26 |
27 | ) : isHeaderParameter(header) ? (
28 |
29 | ) : null,
30 | )}
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/response/Responses.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { Operation } from '../../libs/operation'
3 | import { includesDefaultResponse, type Responses } from '../../libs/response'
4 | import type { Schema } from '../../libs/schema'
5 | import Heading from '../Heading.astro'
6 | import type { SectionHeadingProps } from '../Section.astro'
7 |
8 | import Response from './Response.astro'
9 |
10 | interface Props extends SectionHeadingProps {
11 | operation: Operation
12 | responses: Responses | undefined
13 | schema: Schema
14 | }
15 |
16 | const { level = 2, operation, prefix = '', responses, schema } = Astro.props
17 |
18 | const id = prefix ? `${prefix}-responses` : 'responses'
19 | const responseSectionHeadingProps: SectionHeadingProps = {
20 | level: level + 1,
21 | prefix: prefix ? id : undefined,
22 | }
23 | ---
24 |
25 | {
26 | responses && (
27 | <>
28 |
29 | Responses
30 |
31 | {Object.entries(responses).map(([name, response]) => {
32 | if (name === 'default') {
33 | return null
34 | }
35 |
36 | return
37 | })}
38 | {includesDefaultResponse(responses) && (
39 |
46 | )}
47 | >
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/schema/Schema.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { isSchemaObjectObject, type SchemaObject as SchemaObjectType } from '../../libs/schemaObject'
3 | import Examples, { type Props as ExamplesProps } from '../example/Examples.astro'
4 | import Md from '../Md.astro'
5 |
6 | import SchemaObject from './SchemaObject.astro'
7 |
8 | interface Props extends ExamplesProps {
9 | description?: string | undefined
10 | schema: SchemaObjectType | undefined
11 | type?: string | undefined
12 | }
13 |
14 | const { description, example, examples, parameter, schema, type } = Astro.props
15 |
16 | // For objects, we want to show the description before the object itself.
17 | const isObject = schema && isSchemaObjectObject(schema)
18 | ---
19 |
20 | {isObject && }
21 | {schema && }
22 | {!isObject && }
23 |
24 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/schema/SchemaObject.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import {
3 | getNullable,
4 | getSchemaObjects,
5 | isSchemaObject,
6 | isSchemaObjectAllOf,
7 | isSchemaObjectObject,
8 | type SchemaObject,
9 | } from '../../libs/schemaObject'
10 | import Example from '../example/Example.astro'
11 | import ExternalDocs from '../ExternalDocs.astro'
12 | import Items from '../Items.astro'
13 | import Md from '../Md.astro'
14 |
15 | import SchemaObjectAllOf from './SchemaObjectAllOf.astro'
16 | import SchemaObjectObject from './SchemaObjectObject.astro'
17 | import SchemaObjects from './SchemaObjects.astro'
18 |
19 | interface Props {
20 | hideExample?: boolean | undefined
21 | negated?: boolean
22 | nested?: boolean
23 | parents?: SchemaObject[]
24 | schemaObject: SchemaObject
25 | type?: string | undefined
26 | }
27 |
28 | const { hideExample = false, negated, nested = false, parents = [], schemaObject, type } = Astro.props
29 |
30 | const schemaObjects = getSchemaObjects(schemaObject)
31 |
32 | const hasMany = schemaObjects !== undefined
33 | ---
34 |
35 | {
36 | hasMany ? (
37 |
38 | ) : isSchemaObject(schemaObject.not) ? (
39 |
40 | ) : (
41 | <>
42 | {schemaObject.title && {schemaObject.title}}
43 |
44 |
45 | {isSchemaObjectObject(schemaObject) ? (
46 |
47 | ) : isSchemaObjectAllOf(schemaObject) ? (
48 |
49 | ) : (
50 |
51 | )}
52 | {!hideExample && schemaObject.example && }
53 | >
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/schema/SchemaObjectAllOf.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import {
3 | getNullable,
4 | getProperties,
5 | getSchemaObjects,
6 | isSchemaObject,
7 | isSchemaObjectObject,
8 | type SchemaObject,
9 | } from '../../libs/schemaObject'
10 | import Items from '../Items.astro'
11 |
12 | import SchemaObjectObjectProperties from './SchemaObjectObjectProperties.astro'
13 | import SchemaObjects from './SchemaObjects.astro'
14 |
15 | interface Props {
16 | nested: boolean
17 | parents?: SchemaObject[]
18 | schemaObject: SchemaObject
19 | }
20 |
21 | const { nested, schemaObject, parents = [] } = Astro.props
22 | ---
23 |
24 | {
25 | schemaObject.allOf &&
26 | schemaObject.allOf.map((allOfSchemaObject) => {
27 | if (!isSchemaObject(allOfSchemaObject)) return null
28 | const schemaObjects = getSchemaObjects(allOfSchemaObject)
29 | if (schemaObjects !== undefined) {
30 | return (
31 |
37 | )
38 | } else if (isSchemaObjectObject(schemaObject)) {
39 | return (
40 | <>
41 |
46 |
47 | >
48 | )
49 | }
50 | return (
51 |
57 | )
58 | })
59 | }
60 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/schema/SchemaObjectObject.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getProperties, isAdditionalPropertiesWithSchemaObject, type SchemaObject } from '../../libs/schemaObject'
3 | import Key from '../Key.astro'
4 | import Tags from '../Tags.astro'
5 |
6 | import Schema from './SchemaObject.astro'
7 | import SchemaObjectAllOf from './SchemaObjectAllOf.astro'
8 | import SchemaObjectObjectProperties from './SchemaObjectObjectProperties.astro'
9 |
10 | interface Props {
11 | nested: boolean
12 | parents?: SchemaObject[]
13 | schemaObject: SchemaObject
14 | }
15 |
16 | const { nested, parents = [], schemaObject } = Astro.props
17 |
18 | const properties = getProperties(schemaObject)
19 | ---
20 |
21 |
22 |
23 | object
24 |
25 | = ${schemaObject.minProperties} properties`,
28 | schemaObject.maxProperties && `<= ${schemaObject.maxProperties} properties`,
29 | ]}
30 | />
31 |
32 |
33 | {
34 | schemaObject.additionalProperties && (
35 |
36 | {schemaObject.additionalProperties === true ? (
37 | any
38 | ) : isAdditionalPropertiesWithSchemaObject(schemaObject.additionalProperties) ? (
39 |
40 | ) : null}
41 |
42 | )
43 | }
44 |
45 |
46 |
91 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/schema/SchemaObjectObjectProperties.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getType } from '../../libs/items'
3 | import type { Properties, SchemaObject } from '../../libs/schemaObject'
4 | import Key from '../Key.astro'
5 | import Tag from '../Tag.astro'
6 |
7 | import Schema from './SchemaObject.astro'
8 |
9 | interface Props {
10 | parents: SchemaObject[]
11 | properties: Properties
12 | required: string[] | undefined
13 | }
14 |
15 | const { parents, properties, required } = Astro.props
16 | ---
17 |
18 | {
19 | Object.entries(properties).map(([name, schema]) => {
20 | const isRecursive = parents?.some(
21 | (parent) => parent === schema || (schema.type === 'array' && parent === schema.items),
22 | )
23 |
24 | return (
25 |
26 | {isRecursive ? (
27 |
28 | {getType(schema)}
29 | recursive
30 |
31 | ) : (
32 |
33 | )}
34 |
35 | )
36 | })
37 | }
38 |
39 |
46 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/schema/SchemaObjects.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Tabs, TabItem } from '@astrojs/starlight/components'
3 |
4 | import { getType } from '../../libs/items'
5 | import type { Discriminator, SchemaObjects, SchemaObject as SchemaObjectType } from '../../libs/schemaObject'
6 | import Tag from '../Tag.astro'
7 |
8 | import SchemaObject from './SchemaObject.astro'
9 |
10 | interface Props {
11 | discriminator: Discriminator
12 | nested: boolean
13 | parents?: SchemaObjectType[]
14 | schemaObjects: SchemaObjects
15 | }
16 |
17 | const {
18 | discriminator,
19 | nested,
20 | parents = [],
21 | schemaObjects: { schemaObjects, type },
22 | } = Astro.props
23 |
24 | const discriminatorPropertyName =
25 | typeof discriminator === 'string'
26 | ? discriminator
27 | : typeof discriminator === 'object'
28 | ? discriminator.propertyName
29 | : undefined
30 |
31 | const humanReadableType: Record = {
32 | anyOf: 'Any of',
33 | oneOf: 'One of',
34 | }
35 | ---
36 |
37 |
38 | {humanReadableType[type]}:
39 | {discriminatorPropertyName && discriminator: {discriminatorPropertyName}}
40 |
41 |
42 | {
43 | schemaObjects.map((schemaObject) => (
44 |
45 |
46 |
47 | ))
48 | }
49 |
50 |
51 |
62 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/security/Security.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import context from 'virtual:starlight-openapi-context'
3 |
4 | import type { Operation } from '../../libs/operation'
5 | import { getBaseLink, slug } from '../../libs/path'
6 | import type { Schema } from '../../libs/schema'
7 | import { getSecurityRequirements } from '../../libs/security'
8 | import Section from '../Section.astro'
9 | import Tags from '../Tags.astro'
10 |
11 | interface Props {
12 | operation: Operation
13 | schema?: Schema
14 | }
15 |
16 | const { operation, schema } = Astro.props
17 |
18 | const requirements = getSecurityRequirements(operation, schema)
19 | ---
20 |
21 | {
22 | requirements && requirements.length > 0 ? (
23 |
24 |
25 | {requirements.map((requirement) => {
26 | const schemes = Object.keys(requirement)
27 |
28 | if (schemes.length === 0) {
29 | return (
30 | -
31 | None
32 |
33 | )
34 | }
35 |
36 | return Object.entries(requirement).map(([scheme, scopes]) => {
37 | return (
38 | -
39 |
40 | {schema ? {scheme} : scheme}
41 |
42 | {scopes.length > 0 ? : null}
43 |
44 | )
45 | })
46 | })}
47 |
48 |
49 | ) : null
50 | }
51 |
52 |
66 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/security/SecurityDefinitions.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import {
3 | isOpenAPIV2OAuth2SecurityScheme,
4 | isOpenAPIV3OAuth2SecurityScheme,
5 | type SecurityDefinitions,
6 | } from '../../libs/security'
7 | import { capitalize } from '../../libs/utils'
8 | import Heading from '../Heading.astro'
9 | import Md from '../Md.astro'
10 | import Section from '../Section.astro'
11 | import Text from '../Text.astro'
12 |
13 | import SecurityOAuth2Flow from './SecurityOAuth2Flow.astro'
14 |
15 | interface Props {
16 | definitions: SecurityDefinitions | undefined
17 | }
18 |
19 | const { definitions } = Astro.props
20 | ---
21 |
22 | {
23 | definitions && (
24 | <>
25 |
26 | Authentication
27 |
28 | {Object.entries(definitions).map(([name, scheme]) => (
29 |
30 |
31 |
32 | {scheme.type}
33 |
34 | {'bearerFormat' in scheme && (
35 |
36 | {scheme.bearerFormat}
37 |
38 | )}
39 | {'openIdConnectUrl' in scheme && (
40 |
41 | {scheme.openIdConnectUrl}
42 |
43 | )}
44 | {scheme.type === 'apiKey' && (
45 |
46 | {scheme.name}
47 |
48 | )}
49 | {isOpenAPIV2OAuth2SecurityScheme(scheme) ? (
50 |
51 | ) : isOpenAPIV3OAuth2SecurityScheme(scheme) ? (
52 | Object.entries(scheme.flows).map(([type, flow]) => )
53 | ) : null}
54 |
55 | ))}
56 | >
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/components/security/SecurityOAuth2Flow.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type { SecuritySchemeOAuth2Flow } from '../../libs/security'
3 | import Tag from '../Tag.astro'
4 | import Text from '../Text.astro'
5 |
6 | interface Props {
7 | flow: SecuritySchemeOAuth2Flow
8 | type: string
9 | }
10 |
11 | const { flow, type } = Astro.props
12 | ---
13 |
14 |
15 | {type}
16 |
17 |
18 | {
19 | 'authorizationUrl' in flow && (
20 |
21 | {flow.authorizationUrl}
22 |
23 | )
24 | }
25 | {
26 | 'tokenUrl' in flow && (
27 |
28 | {flow.tokenUrl}
29 |
30 | )
31 | }
32 | {
33 | 'refreshUrl' in flow && (
34 |
35 | {flow.refreshUrl}
36 |
37 | )
38 | }
39 | {
40 | Object.keys(flow.scopes).length > 0 && (
41 | <>
42 |
43 | Scopes:
44 |
45 |
46 | {Object.entries(flow.scopes).map(([scope, description]) => (
47 | -
48 | {scope} - {description}
49 |
50 | ))}
51 |
52 | >
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/index.ts:
--------------------------------------------------------------------------------
1 | import type { StarlightPlugin } from '@astrojs/starlight/types'
2 |
3 | import { validateConfig, type StarlightOpenAPIUserConfig } from './libs/config'
4 | import { starlightOpenAPIIntegration } from './libs/integration'
5 | import { parseSchema } from './libs/parser'
6 | import { getSidebarGroupsPlaceholder } from './libs/starlight'
7 |
8 | export const openAPISidebarGroups = getSidebarGroupsPlaceholder()
9 |
10 | export default function starlightOpenAPIPlugin(userConfig: StarlightOpenAPIUserConfig): StarlightPlugin {
11 | return {
12 | name: 'starlight-openapi-plugin',
13 | hooks: {
14 | 'config:setup': async ({
15 | addIntegration,
16 | addRouteMiddleware,
17 | command,
18 | config: starlightConfig,
19 | logger,
20 | updateConfig,
21 | }) => {
22 | if (command !== 'build' && command !== 'dev') {
23 | return
24 | }
25 |
26 | const config = validateConfig(logger, userConfig)
27 | const schemas = await Promise.all(config.map((schemaConfig) => parseSchema(logger, schemaConfig)))
28 |
29 | addRouteMiddleware({ entrypoint: 'starlight-openapi/middleware', order: 'post' })
30 | addIntegration(starlightOpenAPIIntegration(schemas))
31 |
32 | const updatedConfig: Parameters[0] = {
33 | customCss: [...(starlightConfig.customCss ?? []), 'starlight-openapi/styles'],
34 | }
35 |
36 | if (updatedConfig.expressiveCode !== false) {
37 | updatedConfig.expressiveCode =
38 | updatedConfig.expressiveCode === true || updatedConfig.expressiveCode === undefined
39 | ? {}
40 | : updatedConfig.expressiveCode
41 | updatedConfig.expressiveCode.removeUnusedThemes = false
42 | }
43 |
44 | updateConfig(updatedConfig)
45 | },
46 | },
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/callback.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
2 |
3 | import type { Operation } from './operation'
4 |
5 | export function getCallbacks(operation: Operation): Callbacks | undefined {
6 | if ('callbacks' in operation) {
7 | return operation.callbacks
8 | }
9 |
10 | return
11 | }
12 |
13 | export function getCallbackObject(callbacks: Callbacks, identifier: keyof Callbacks) {
14 | return callbacks[identifier] as CallbackObject
15 | }
16 |
17 | type Callbacks = NonNullable
18 | type CallbackObject = OpenAPIV3.CallbackObject
19 | type CallbackUrl = keyof CallbackObject
20 | export type Callback = CallbackObject[CallbackUrl]
21 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/config.ts:
--------------------------------------------------------------------------------
1 | import type { AstroIntegrationLogger } from 'astro'
2 | import { AstroError } from 'astro/errors'
3 | import { z } from 'astro/zod'
4 |
5 | import { SchemaConfigSchema } from './schema'
6 |
7 | const configSchema = z.array(SchemaConfigSchema).min(1)
8 |
9 | export function validateConfig(logger: AstroIntegrationLogger, userConfig: unknown): StarlightOpenAPIConfig {
10 | const config = configSchema.safeParse(userConfig)
11 |
12 | if (!config.success) {
13 | const errors = config.error.flatten()
14 |
15 | logger.error('Invalid starlight-openapi configuration.')
16 |
17 | throw new AstroError(
18 | `
19 | ${errors.formErrors.map((formError) => ` - ${formError}`).join('\n')}
20 | ${Object.entries(errors.fieldErrors)
21 | .map(([fieldName, fieldErrors]) => ` - ${fieldName}: ${(fieldErrors ?? []).join(' - ')}`)
22 | .join('\n')}
23 | `,
24 | `See the error report above for more informations.\n\nIf you believe this is a bug, please file an issue at https://github.com/HiDeoo/starlight-openapi/issues/new/choose`,
25 | )
26 | }
27 |
28 | return config.data
29 | }
30 |
31 | export type StarlightOpenAPIUserConfig = z.input
32 | export type StarlightOpenAPIConfig = z.output
33 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/content.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
2 |
3 | export type Content = Record
4 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/document.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAPIV2 } from 'openapi-types'
2 |
3 | import type { Schema } from './schema'
4 |
5 | export function getOpenAPIVersion(document: Document) {
6 | return isOpenAPIV2Document(document) ? document.swagger : document.openapi
7 | }
8 |
9 | export function getSummary(document: Document) {
10 | return 'summary' in document.info ? document.info.summary : undefined
11 | }
12 |
13 | export function isOpenAPIV2Document(document: Document): document is DocumentV2 {
14 | return 'swagger' in document
15 | }
16 |
17 | export type Document = Schema['document']
18 | type DocumentV2 = OpenAPIV2.Document
19 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/env.d.ts:
--------------------------------------------------------------------------------
1 | interface ImportMetaEnv {
2 | readonly BASE_URL: string
3 | }
4 |
5 | interface ImportMeta {
6 | readonly env: ImportMetaEnv
7 | }
8 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/example.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
2 |
3 | import type { Response } from './response'
4 |
5 | export function isExamples(examples: unknown): examples is ExamplesV3 {
6 | return typeof examples === 'object'
7 | }
8 |
9 | export function isOpenAPIV2ResponseWithExamples(response: Response): response is Response & { examples: ExamplesV2 } {
10 | return 'examples' in response && typeof response.examples === 'object'
11 | }
12 |
13 | export type ExampleV3 = OpenAPIV3.ExampleObject | OpenAPIV3_1.ExampleObject
14 | export type ExamplesV2 = OpenAPIV2.ExampleObject
15 | export type ExamplesV3 = Record
16 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/header.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
2 |
3 | import type { Response } from './response'
4 |
5 | export function isResponseWithHeaders(response: Response): response is Response & { headers: Headers } {
6 | return 'headers' in response && typeof response.headers === 'object'
7 | }
8 |
9 | export type Header = OpenAPIV2.HeaderObject | OpenAPIV3.HeaderObject | OpenAPIV3_1.HeaderObject
10 | export type Headers = Record
11 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/integration.ts:
--------------------------------------------------------------------------------
1 | import type { AstroIntegration } from 'astro'
2 |
3 | import type { Schema } from './schema'
4 | import { vitePluginStarlightOpenAPI } from './vite'
5 |
6 | export function starlightOpenAPIIntegration(schemas: Schema[]): AstroIntegration {
7 | const starlightOpenAPI: AstroIntegration = {
8 | name: 'starlight-openapi',
9 | hooks: {
10 | 'astro:config:setup': ({ config, injectRoute, updateConfig }) => {
11 | injectRoute({
12 | entrypoint: 'starlight-openapi/route',
13 | pattern: `[...openAPISlug]`,
14 | prerender: true,
15 | })
16 |
17 | updateConfig({
18 | vite: {
19 | plugins: [vitePluginStarlightOpenAPI(schemas, { trailingSlash: config.trailingSlash })],
20 | },
21 | })
22 | },
23 | },
24 | }
25 |
26 | return starlightOpenAPI
27 | }
28 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/items.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAPIV2 } from 'openapi-types'
2 |
3 | import type { SchemaObject } from './schemaObject'
4 |
5 | export function isOpenAPIV2Items(items: unknown): items is Items {
6 | return (
7 | items !== undefined && typeof items === 'object' && 'type' in (items as Items) && !('schema' in (items as Items))
8 | )
9 | }
10 |
11 | export function getType(items: Items): string | undefined {
12 | if (items.type === 'array' && items.items) {
13 | const arrayType = getType(items.items)
14 |
15 | return arrayType ? `Array<${arrayType}>` : 'Array'
16 | }
17 |
18 | return Array.isArray(items.type) ? items.type.join(' | ') : items.type
19 | }
20 |
21 | export function getBound(items: Items, type: 'maximum' | 'minimum'): string | undefined {
22 | const exclusive = items[type === 'maximum' ? 'exclusiveMaximum' : 'exclusiveMinimum']
23 | const sign = type === 'maximum' ? '<' : '>'
24 | const value = items[type]
25 |
26 | if (typeof exclusive === 'number') {
27 | return `${sign} ${exclusive}`
28 | } else if (value) {
29 | return `${sign}${exclusive ? '' : '='} ${value}`
30 | }
31 |
32 | return
33 | }
34 |
35 | export type Items = Omit & {
36 | exclusiveMaximum?: boolean | number
37 | exclusiveMinimum?: boolean | number
38 | items?: SchemaObject
39 | type?: string | string[]
40 | }
41 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/markdown.ts:
--------------------------------------------------------------------------------
1 | import { createMarkdownProcessor } from '@astrojs/markdown-remark'
2 |
3 | const processor = await createMarkdownProcessor()
4 |
5 | export async function transformMarkdown(markdown: string) {
6 | const result = await processor.render(markdown)
7 |
8 | return result.code
9 | }
10 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/operation.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAPI } from 'openapi-types'
2 |
3 | import type { Callback } from './callback'
4 | import { type Document, isOpenAPIV2Document } from './document'
5 | import { slug } from './path'
6 | import { isPathItem, type PathItem } from './pathItem'
7 | import type { Schema } from './schema'
8 |
9 | const defaultOperationTag = 'Operations'
10 | const operationHttpMethods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'] as const
11 |
12 | export function getOperationsByTag({ config, document }: Schema) {
13 | const operationsByTag = new Map()
14 |
15 | for (const [pathItemPath, pathItem] of Object.entries(document.paths ?? {})) {
16 | if (!isPathItem(pathItem)) {
17 | continue
18 | }
19 |
20 | const allOperationIds = operationHttpMethods.map((method) => {
21 | return isPathItemOperation(pathItem, method) ? (pathItem[method].operationId ?? pathItemPath) : undefined
22 | })
23 |
24 | for (const [index, method] of operationHttpMethods.entries()) {
25 | const operationId = allOperationIds[index]
26 |
27 | if (!operationId || !isPathItemOperation(pathItem, method)) {
28 | continue
29 | }
30 |
31 | const operation = pathItem[method]
32 | const isDuplicateOperationId = allOperationIds.filter((id) => id === operationId).length > 1
33 | const operationIdSlug = slug(operationId)
34 |
35 | for (const tag of operation.tags ?? [defaultOperationTag]) {
36 | const operations = operationsByTag.get(tag) ?? { entries: [], tag: { name: tag } }
37 |
38 | const title =
39 | operation.summary ?? (isDuplicateOperationId ? `${operationId} (${method.toUpperCase()})` : operationId)
40 |
41 | operations.entries.push({
42 | method,
43 | operation,
44 | path: pathItemPath,
45 | pathItem,
46 | sidebar: {
47 | label: config.sidebar.operations.labels === 'summary' && operation.summary ? title : operationId,
48 | },
49 | slug: isDuplicateOperationId
50 | ? `operations/${operationIdSlug}/${slug(method)}`
51 | : `operations/${operationIdSlug}`,
52 | title,
53 | })
54 |
55 | operationsByTag.set(tag, operations)
56 | }
57 | }
58 | }
59 |
60 | if (document.tags) {
61 | const orderedTags = new Map(document.tags.map((tag, index) => [tag.name, { index, tag }]))
62 | const operationsByTagArray = [...operationsByTag.entries()].sort(([tagA], [tagB]) => {
63 | const orderA = orderedTags.get(tagA)?.index ?? Number.POSITIVE_INFINITY
64 | const orderB = orderedTags.get(tagB)?.index ?? Number.POSITIVE_INFINITY
65 |
66 | return orderA - orderB
67 | })
68 |
69 | operationsByTag.clear()
70 |
71 | for (const [tag, operations] of operationsByTagArray) {
72 | operationsByTag.set(tag, { ...operations, tag: orderedTags.get(tag)?.tag ?? operations.tag })
73 | }
74 | }
75 |
76 | return operationsByTag
77 | }
78 |
79 | export function getWebhooksOperations({ config, document }: Schema): PathItemOperation[] {
80 | if (!('webhooks' in document)) {
81 | return []
82 | }
83 |
84 | const operations: PathItemOperation[] = []
85 |
86 | for (const [webhookKey, pathItem] of Object.entries(document.webhooks)) {
87 | if (!isPathItem(pathItem)) {
88 | continue
89 | }
90 |
91 | for (const method of operationHttpMethods) {
92 | if (!isPathItemOperation(pathItem, method)) {
93 | continue
94 | }
95 |
96 | const operation = pathItem[method]
97 | const operationId = operation.operationId ?? webhookKey
98 |
99 | const title = operation.summary ?? operationId
100 |
101 | operations.push({
102 | method,
103 | operation,
104 | pathItem,
105 | sidebar: {
106 | label: config.sidebar.operations.labels === 'summary' && operation.summary ? title : operationId,
107 | },
108 | slug: `webhooks/${slug(operationId)}`,
109 | title,
110 | })
111 | }
112 | }
113 |
114 | return operations
115 | }
116 |
117 | export function getCallbackOperations(callback: Callback): CallbackOperation[] {
118 | const operations: CallbackOperation[] = []
119 |
120 | for (const method of operationHttpMethods) {
121 | const operation = callback[method]
122 | if (!operation) continue
123 |
124 | operations.push({ method, operation })
125 | }
126 |
127 | return operations
128 | }
129 |
130 | export function isPathItemOperation(
131 | pathItem: PathItem,
132 | method: TMethod,
133 | ): pathItem is Record {
134 | return method in pathItem
135 | }
136 |
137 | export function isMinimalOperationTag(tag: OperationTag): boolean {
138 | return (tag.description === undefined || tag.description.length === 0) && tag.externalDocs === undefined
139 | }
140 |
141 | export function getOperationURLs(document: Document, { operation, path, pathItem }: PathItemOperation): OperationURL[] {
142 | const urls: OperationURL[] = []
143 |
144 | if (isOpenAPIV2Document(document) && 'host' in document) {
145 | let url = document.host
146 | url += document.basePath ?? ''
147 | url += path ?? ''
148 |
149 | if (url.length > 0) {
150 | urls.push(makeOperationURL(url))
151 | }
152 | } else {
153 | const servers =
154 | 'servers' in operation
155 | ? operation.servers
156 | : 'servers' in pathItem
157 | ? pathItem.servers
158 | : 'servers' in document
159 | ? document.servers
160 | : []
161 |
162 | for (const server of servers) {
163 | let url = server.url
164 | url += path ?? ''
165 |
166 | if (url.length > 0) {
167 | urls.push(makeOperationURL(url, server.description))
168 | }
169 | }
170 | }
171 |
172 | return urls
173 | }
174 |
175 | function makeOperationURL(url: string, description?: string): OperationURL {
176 | return { description, url: url.replace(/^\/\//, '') }
177 | }
178 |
179 | export interface PathItemOperation {
180 | method: OperationHttpMethod
181 | operation: Operation
182 | path?: string
183 | pathItem: PathItem
184 | sidebar: {
185 | label: string
186 | }
187 | slug: string
188 | title: string
189 | }
190 |
191 | export interface CallbackOperation {
192 | method: OperationHttpMethod
193 | operation: Operation
194 | }
195 |
196 | export type Operation = OpenAPI.Operation
197 | export type OperationHttpMethod = (typeof operationHttpMethods)[number]
198 | export type OperationTag = NonNullable[number]
199 |
200 | export interface OperationURL {
201 | description?: string | undefined
202 | url: string
203 | }
204 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/parameter.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAPI, OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
2 |
3 | import type { Header } from './header'
4 |
5 | const ignoredHeaderParameters = new Set(['accept', 'authorization', 'content-type'])
6 |
7 | export function getParametersByLocation(
8 | operationParameters: OpenAPI.Parameters | undefined,
9 | pathItemParameters: OpenAPI.Parameters | undefined,
10 | ) {
11 | const parametersByLocation = new Map()
12 |
13 | for (const parameter of [...(pathItemParameters ?? []), ...(operationParameters ?? [])]) {
14 | if (!isParameter(parameter) || isIgnoredParameter(parameter)) {
15 | continue
16 | }
17 |
18 | const id = getParameterId(parameter)
19 | const parametersById: Parameters = parametersByLocation.get(parameter.in) ?? new Map()
20 |
21 | parametersById.set(id, parameter)
22 |
23 | parametersByLocation.set(parameter.in, parametersById)
24 | }
25 |
26 | return new Map(
27 | [...parametersByLocation].sort((locationA, locationB) =>
28 | locationA[0] === 'path' ? -1 : locationB[0] === 'path' ? 1 : 0,
29 | ),
30 | )
31 | }
32 |
33 | export function isHeaderParameter(parameter: Header): parameter is Omit {
34 | return typeof parameter === 'object' && !('name' in parameter) && !('in' in parameter)
35 | }
36 |
37 | function getParameterId(parameter: Parameter): ParameterId {
38 | return `${parameter.name}:${parameter.in}`
39 | }
40 |
41 | function isParameter(parameter: OpenAPI.Parameter): parameter is Parameter {
42 | return typeof parameter === 'object' && !('$ref' in parameter)
43 | }
44 |
45 | function isIgnoredParameter(parameter: Parameter): boolean {
46 | return (
47 | parameter.in === 'body' || (parameter.in === 'header' && ignoredHeaderParameters.has(parameter.name.toLowerCase()))
48 | )
49 | }
50 |
51 | export type Parameter = OpenAPIV2.Parameter | OpenAPIV3.ParameterObject | OpenAPIV3_1.ParameterObject
52 | type ParameterId = `${Parameter['name']}:${Parameter['in']}`
53 | type Parameters = Map
54 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/parser.ts:
--------------------------------------------------------------------------------
1 | import OpenAPIParser from '@readme/openapi-parser'
2 | import type { AstroIntegrationLogger } from 'astro'
3 |
4 | import type { Schema, StarlightOpenAPISchemaConfig } from './schema'
5 |
6 | export async function parseSchema(
7 | logger: AstroIntegrationLogger,
8 | config: StarlightOpenAPISchemaConfig,
9 | ): Promise {
10 | try {
11 | logger.info(`Parsing OpenAPI schema at '${config.schema}'.`)
12 |
13 | const document = await OpenAPIParser.bundle(config.schema)
14 |
15 | return { config, document }
16 | } catch (error) {
17 | if (error instanceof Error) {
18 | logger.error(error.message)
19 | }
20 |
21 | throw error
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/path.ts:
--------------------------------------------------------------------------------
1 | import type { AstroConfig } from 'astro'
2 | import { slug } from 'github-slugger'
3 |
4 | import type { StarlightOpenAPISchemaConfig } from './schema'
5 | import type { StarlightOpenAPIContext } from './vite'
6 |
7 | export { slug } from 'github-slugger'
8 |
9 | const base = stripTrailingSlash(import.meta.env.BASE_URL)
10 |
11 | const trailingSlashTransformers: Record = {
12 | always: ensureTrailingSlash,
13 | ignore: ensureTrailingSlash,
14 | never: stripTrailingSlash,
15 | }
16 |
17 | export function getTrailingSlashTransformer(context: StarlightOpenAPIContext) {
18 | return trailingSlashTransformers[context.trailingSlash]
19 | }
20 |
21 | /**
22 | * Does not take the Astro `base` configuration option into account.
23 | * @see {@link getBaseLink} for a link that does.
24 | */
25 | export function getBasePath(config: StarlightOpenAPISchemaConfig) {
26 | const path = config.base
27 | .split('/')
28 | .map((part) => slug(part))
29 | .join('/')
30 |
31 | return `/${path}/`
32 | }
33 |
34 | /**
35 | * Takes the Astro `base` configuration option into account.
36 | * @see {@link getBasePath} for a slug that does not.
37 | */
38 | export function getBaseLink(config: StarlightOpenAPISchemaConfig, context?: StarlightOpenAPIContext) {
39 | const path = stripLeadingSlash(getBasePath(config))
40 | const baseLink = path ? `${base}/${path}` : `${base}/`
41 |
42 | return context ? getTrailingSlashTransformer(context)(baseLink) : baseLink
43 | }
44 |
45 | export function stripLeadingAndTrailingSlashes(path: string): string {
46 | return stripLeadingSlash(stripTrailingSlash(path))
47 | }
48 |
49 | function stripLeadingSlash(path: string) {
50 | if (!path.startsWith('/')) {
51 | return path
52 | }
53 |
54 | return path.slice(1)
55 | }
56 |
57 | function stripTrailingSlash(path: string) {
58 | if (!path.endsWith('/')) {
59 | return path
60 | }
61 |
62 | return path.slice(0, -1)
63 | }
64 |
65 | function ensureTrailingSlash(path: string) {
66 | if (path.endsWith('/')) {
67 | return path
68 | }
69 |
70 | return `${path}/`
71 | }
72 |
73 | type TrailingSlashTransformer = (path: string) => string
74 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/pathItem.ts:
--------------------------------------------------------------------------------
1 | import { getOperationsByTag, getWebhooksOperations, isMinimalOperationTag } from './operation'
2 | import { getBaseLink, getTrailingSlashTransformer, slug } from './path'
3 | import type { Schema } from './schema'
4 | import { getMethodSidebarBadge, makeSidebarGroup, makeSidebarLink, type SidebarGroup } from './starlight'
5 | import type { StarlightOpenAPIContext } from './vite'
6 |
7 | export function getPathItemSidebarGroups(
8 | pathname: string,
9 | schema: Schema,
10 | context: StarlightOpenAPIContext,
11 | ): SidebarGroup['entries'] {
12 | const { config } = schema
13 | const baseLink = getBaseLink(config)
14 | const operations = getOperationsByTag(schema)
15 |
16 | const tags =
17 | config.sidebar.tags.sort === 'alphabetical'
18 | ? [...operations.entries()].sort((a, b) => a[0].localeCompare(b[0]))
19 | : [...operations.entries()]
20 |
21 | return tags.map(([tag, operations]) => {
22 | const entries =
23 | config.sidebar.operations.sort === 'alphabetical'
24 | ? operations.entries.sort((a, b) => a.sidebar.label.localeCompare(b.sidebar.label))
25 | : operations.entries
26 |
27 | const items = entries.map(({ method, sidebar, slug }) => {
28 | return makeSidebarLink(
29 | pathname,
30 | sidebar.label,
31 | getTrailingSlashTransformer(context)(baseLink + slug),
32 | config.sidebar.operations.badges ? getMethodSidebarBadge(method) : undefined,
33 | )
34 | })
35 |
36 | if (!isMinimalOperationTag(operations.tag)) {
37 | items.unshift(
38 | makeSidebarLink(
39 | pathname,
40 | 'Overview',
41 | getTrailingSlashTransformer(context)(`${baseLink}operations/tags/${slug(operations.tag.name)}`),
42 | ),
43 | )
44 | }
45 |
46 | return makeSidebarGroup(tag, items, config.sidebar.collapsed)
47 | })
48 | }
49 |
50 | export function getWebhooksSidebarGroups(
51 | pathname: string,
52 | schema: Schema,
53 | context: StarlightOpenAPIContext,
54 | ): SidebarGroup['entries'] {
55 | const { config } = schema
56 | const baseLink = getBaseLink(config)
57 | const operations = getWebhooksOperations(schema)
58 |
59 | if (operations.length === 0) {
60 | return []
61 | }
62 |
63 | const entries =
64 | config.sidebar.operations.sort === 'alphabetical'
65 | ? operations.sort((a, b) => a.sidebar.label.localeCompare(b.sidebar.label))
66 | : operations
67 |
68 | return [
69 | makeSidebarGroup(
70 | 'Webhooks',
71 | entries.map(({ method, sidebar, slug }) =>
72 | makeSidebarLink(
73 | pathname,
74 | sidebar.label,
75 | getTrailingSlashTransformer(context)(baseLink + slug),
76 | config.sidebar.operations.badges ? getMethodSidebarBadge(method) : undefined,
77 | ),
78 | ),
79 | config.sidebar.collapsed,
80 | ),
81 | ]
82 | }
83 |
84 | export function isPathItem(pathItem: unknown): pathItem is PathItem {
85 | return typeof pathItem === 'object'
86 | }
87 |
88 | type Paths = NonNullable
89 | export type PathItem = NonNullable
90 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/requestBody.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
2 |
3 | import type { Operation } from './operation'
4 | import type { Parameter } from './parameter'
5 | import type { Schema } from './schema'
6 |
7 | export function getOpenAPIV2RequestBodyParameter(operation: Operation): OpenAPIV2.InBodyParameterObject | undefined {
8 | if ('requestBody' in operation || operation.parameters === undefined) {
9 | return
10 | }
11 |
12 | return (operation.parameters as Parameter[]).find(isOpenAPIV2RequestBodyParameter)
13 | }
14 |
15 | export function getOpenAPIV3RequestBody(operation: Operation): RequestBody | undefined {
16 | if (!isOperationWithRequestBody(operation)) {
17 | return
18 | }
19 |
20 | return operation.requestBody
21 | }
22 |
23 | export function hasRequestBody(operation: Operation): boolean {
24 | return getOpenAPIV2RequestBodyParameter(operation) !== undefined || getOpenAPIV3RequestBody(operation) !== undefined
25 | }
26 |
27 | export function getOpenAPIV2OperationConsumes(schema: Schema, operation: Operation): OpenAPIV2.MimeTypes | undefined {
28 | if ('consumes' in operation) {
29 | return operation.consumes
30 | } else if ('consumes' in schema.document) {
31 | return schema.document.consumes
32 | }
33 |
34 | return
35 | }
36 |
37 | export function getOpenAPIV2OperationProduces(schema: Schema, operation: Operation): OpenAPIV2.MimeTypes | undefined {
38 | if ('produces' in operation) {
39 | return operation.produces
40 | } else if ('produces' in schema.document) {
41 | return schema.document.produces
42 | }
43 |
44 | return
45 | }
46 |
47 | function isOpenAPIV2RequestBodyParameter(parameter: Parameter): parameter is OpenAPIV2.InBodyParameterObject {
48 | return parameter.in === 'body'
49 | }
50 |
51 | function isOperationWithRequestBody(operation: Operation): operation is Operation & { requestBody: RequestBody } {
52 | return 'requestBody' in operation && typeof operation.requestBody === 'object'
53 | }
54 |
55 | type RequestBody = OpenAPIV3.RequestBodyObject | OpenAPIV3_1.RequestBodyObject
56 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/response.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
2 |
3 | import type { Content } from './content'
4 | import { isSchemaObject, type SchemaObject } from './schemaObject'
5 |
6 | export function includesDefaultResponse(responses: Responses): responses is Responses & { default: Response } {
7 | return 'default' in responses && typeof responses.default === 'object'
8 | }
9 |
10 | export function getOpenAPIV2ResponseSchema(response: Response): SchemaObject | undefined {
11 | return 'schema' in response && isSchemaObject(response.schema) ? response.schema : undefined
12 | }
13 |
14 | export function getOpenAPIV3ResponseContent(response: Response): Content | undefined {
15 | return 'content' in response ? response.content : undefined
16 | }
17 |
18 | export type Response = OpenAPIV2.ResponseObject | OpenAPIV3.ResponseObject | OpenAPIV3_1.ResponseObject
19 | export type Responses = OpenAPIV2.ResponsesObject | OpenAPIV3.ResponsesObject | OpenAPIV3_1.ResponsesObject
20 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/route.ts:
--------------------------------------------------------------------------------
1 | import schemas from 'virtual:starlight-openapi-schemas'
2 |
3 | import {
4 | getOperationsByTag,
5 | getWebhooksOperations,
6 | isMinimalOperationTag,
7 | type OperationTag,
8 | type PathItemOperation,
9 | } from './operation'
10 | import { getBasePath, slug, stripLeadingAndTrailingSlashes } from './path'
11 | import type { Schema } from './schema'
12 |
13 | export function getSchemaStaticPaths(): StarlighOpenAPIRoute[] {
14 | return Object.values(schemas).flatMap((schema) => [
15 | {
16 | params: {
17 | openAPISlug: stripLeadingAndTrailingSlashes(getBasePath(schema.config)),
18 | },
19 | props: {
20 | schema,
21 | type: 'overview',
22 | },
23 | },
24 | ...getPathItemStaticPaths(schema),
25 | ...getWebhooksStaticPaths(schema),
26 | ])
27 | }
28 |
29 | function getPathItemStaticPaths(schema: Schema): StarlighOpenAPIRoute[] {
30 | const baseLink = getBasePath(schema.config)
31 | const operations = getOperationsByTag(schema)
32 |
33 | return [...operations.entries()].flatMap(([, operations]) => {
34 | const paths: StarlighOpenAPIRoute[] = operations.entries.map((operation) => {
35 | return {
36 | params: {
37 | openAPISlug: stripLeadingAndTrailingSlashes(baseLink + operation.slug),
38 | },
39 | props: {
40 | operation,
41 | schema,
42 | type: 'operation',
43 | },
44 | }
45 | })
46 |
47 | if (!isMinimalOperationTag(operations.tag)) {
48 | paths.unshift({
49 | params: {
50 | openAPISlug: stripLeadingAndTrailingSlashes(`${baseLink}operations/tags/${slug(operations.tag.name)}`),
51 | },
52 | props: {
53 | schema,
54 | tag: operations.tag,
55 | type: 'operation-tag',
56 | },
57 | })
58 | }
59 |
60 | return paths
61 | })
62 | }
63 |
64 | function getWebhooksStaticPaths(schema: Schema): StarlighOpenAPIRoute[] {
65 | const baseLink = getBasePath(schema.config)
66 | const operations = getWebhooksOperations(schema)
67 |
68 | return operations.map((operation) => ({
69 | params: {
70 | openAPISlug: stripLeadingAndTrailingSlashes(baseLink + operation.slug),
71 | },
72 | props: {
73 | operation,
74 | schema,
75 | type: 'operation',
76 | },
77 | }))
78 | }
79 |
80 | interface StarlighOpenAPIRoute {
81 | params: {
82 | openAPISlug: string
83 | }
84 | props: StarlighOpenAPIRouteOverviewProps | StarlighOpenAPIRouteOperationProps | StarlighOpenAPIRouteOperationTagProps
85 | }
86 |
87 | interface StarlighOpenAPIRouteOverviewProps {
88 | schema: Schema
89 | type: 'overview'
90 | }
91 |
92 | interface StarlighOpenAPIRouteOperationProps {
93 | operation: PathItemOperation
94 | schema: Schema
95 | type: 'operation'
96 | }
97 |
98 | interface StarlighOpenAPIRouteOperationTagProps {
99 | schema: Schema
100 | tag: OperationTag
101 | type: 'operation-tag'
102 | }
103 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'astro/zod'
2 | import type { OpenAPI } from 'openapi-types'
3 |
4 | import { getBaseLink, stripLeadingAndTrailingSlashes } from './path'
5 | import { getPathItemSidebarGroups, getWebhooksSidebarGroups } from './pathItem'
6 | import { makeSidebarGroup, makeSidebarLink, type SidebarGroup } from './starlight'
7 | import type { StarlightOpenAPIContext } from './vite'
8 |
9 | export const SchemaConfigSchema = z
10 | .object({
11 | /**
12 | * The base path containing the generated documentation.
13 | * @example 'api/petstore'
14 | */
15 | base: z.string().min(1).transform(stripLeadingAndTrailingSlashes),
16 | /**
17 | * Wheter the generated documentation sidebar group should be collapsed by default.\
18 | * @deprecated
19 | * @default true
20 | */
21 | collapsed: z.boolean().default(true),
22 | /**
23 | * The generated documentation sidebar group label.
24 | * @deprecated
25 | * @defaultValue
26 | * Defaults to the OpenAPI document title.
27 | */
28 | label: z.string().optional(),
29 | /**
30 | * The OpenAPI/Swagger schema path or URL.
31 | */
32 | schema: z.string().min(1),
33 | /**
34 | * The generated sidebar group configuration.
35 | */
36 | sidebar: z
37 | .object({
38 | /**
39 | * Wheter the generated documentation sidebar group should be collapsed by default.
40 | * @default true
41 | */
42 | collapsed: z.boolean().default(true),
43 | /**
44 | * The generated documentation sidebar group label.
45 | * @defaultValue
46 | * Defaults to the OpenAPI document title.
47 | */
48 | label: z.string().optional(),
49 | /**
50 | * The generated documentation operations sidebar links configuration.
51 | */
52 | operations: z
53 | .object({
54 | /**
55 | * Defines if the sidebar should display badges next to operation links with the associated HTTP method.
56 | * @default false
57 | */
58 | badges: z.boolean().default(false),
59 | /**
60 | * Whether the operation sidebar link labels should use the operation ID or summary.
61 | * @default 'operationId'
62 | */
63 | labels: z.enum(['operationId', 'summary']).default('summary'),
64 | /**
65 | * Defines the sorting method for the operation sidebar links.
66 | * @default 'document'
67 | */
68 | sort: z.enum(['alphabetical', 'document']).default('document'),
69 | })
70 | .default({}),
71 | /**
72 | * The generated documentation tags sidebar groups configuration.
73 | */
74 | tags: z
75 | .object({
76 | /**
77 | * Defines the sorting method for the tag sidebar groups.
78 | * @default 'document'
79 | */
80 | sort: z.enum(['alphabetical', 'document']).default('document'),
81 | })
82 | .default({}),
83 | })
84 | .default({}),
85 | /**
86 | * Defines if the sidebar should display badges next to operation links with the associated HTTP method.
87 | * @deprecated
88 | * @default false
89 | */
90 | sidebarMethodBadges: z.boolean().default(false),
91 | })
92 | .transform((value) => {
93 | // eslint-disable-next-line @typescript-eslint/no-deprecated
94 | const { collapsed, label, sidebarMethodBadges, ...rest } = value
95 |
96 | if (!collapsed) {
97 | rest.sidebar.collapsed = collapsed
98 | }
99 |
100 | if (label) {
101 | rest.sidebar.label = label
102 | }
103 |
104 | if (sidebarMethodBadges) {
105 | rest.sidebar.operations.badges = sidebarMethodBadges
106 | }
107 |
108 | return rest
109 | })
110 |
111 | export function getSchemaSidebarGroups(
112 | pathname: string,
113 | schema: Schema,
114 | context: StarlightOpenAPIContext,
115 | ): SidebarGroup {
116 | const { config, document } = schema
117 |
118 | return makeSidebarGroup(
119 | config.sidebar.label ?? document.info.title,
120 | [
121 | makeSidebarLink(pathname, 'Overview', getBaseLink(config, context)),
122 | ...getPathItemSidebarGroups(pathname, schema, context),
123 | ...getWebhooksSidebarGroups(pathname, schema, context),
124 | ],
125 | config.sidebar.collapsed,
126 | )
127 | }
128 |
129 | export type StarlightOpenAPISchemaConfig = z.infer
130 |
131 | export interface Schema {
132 | config: StarlightOpenAPISchemaConfig
133 | document: OpenAPI.Document
134 | }
135 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/schemaObject.ts:
--------------------------------------------------------------------------------
1 | import type { IJsonSchema, OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
2 |
3 | import type { Parameter } from './parameter'
4 |
5 | export function getNullable(schemaObject: SchemaObject) {
6 | return 'nullable' in schemaObject ? (schemaObject as OpenAPIV3.NonArraySchemaObject).nullable : undefined
7 | }
8 |
9 | export function isParameterWithSchemaObject(parameter: Parameter): parameter is Parameter & { schema: SchemaObject } {
10 | return 'schema' in parameter && typeof parameter.schema === 'object'
11 | }
12 |
13 | export function isSchemaObjectObject(schemaObject: SchemaObject): schemaObject is SchemaObject {
14 | return (
15 | schemaObject.type === 'object' ||
16 | 'properties' in schemaObject ||
17 | ('oneOf' in schemaObject && (schemaObject.oneOf as SchemaObject[]).some(isSchemaObjectObject)) ||
18 | ('anyOf' in schemaObject && (schemaObject.anyOf as SchemaObject[]).some(isSchemaObjectObject)) ||
19 | ('allOf' in schemaObject && (schemaObject.allOf as SchemaObject[]).some(isSchemaObjectObject))
20 | )
21 | }
22 |
23 | export function isSchemaObjectAllOf(schemaObject: SchemaObject): schemaObject is SchemaObject {
24 | return schemaObject.type === 'object' || 'allOf' in schemaObject
25 | }
26 |
27 | export function getProperties(schemaObject: SchemaObject): Properties {
28 | return (schemaObject.properties ?? {}) as Properties
29 | }
30 |
31 | export function isAdditionalPropertiesWithSchemaObject(
32 | additionalProperties: SchemaObject['additionalProperties'],
33 | ): additionalProperties is SchemaObject {
34 | return typeof additionalProperties === 'object'
35 | }
36 |
37 | export function isSchemaObject(
38 | schemaObject: OpenAPIV2.SchemaObject | OpenAPIV3.SchemaObject | OpenAPIV3_1.SchemaObject | IJsonSchema | undefined,
39 | ): schemaObject is SchemaObject {
40 | return typeof schemaObject === 'object'
41 | }
42 |
43 | export function getSchemaObjects(schemaObject: SchemaObject): SchemaObjects | undefined {
44 | if (schemaObject.oneOf && schemaObject.oneOf.length > 0) {
45 | const { oneOf, ...otherProperties } = schemaObject
46 |
47 | return {
48 | schemaObjects: sanitizeSchemaObjects(oneOf as SchemaObject[], otherProperties),
49 | type: 'oneOf',
50 | }
51 | } else if (schemaObject.anyOf && schemaObject.anyOf.length > 0) {
52 | const { anyOf, ...otherProperties } = schemaObject
53 |
54 | return {
55 | schemaObjects: sanitizeSchemaObjects(anyOf as SchemaObject[], otherProperties),
56 | type: 'anyOf',
57 | }
58 | }
59 |
60 | return
61 | }
62 |
63 | function sanitizeSchemaObjects(schemaObjects: SchemaObject[], parentProperties: SchemaObject) {
64 | if (schemaObjects.some((schemaObjectsObject) => schemaObjectsObject.type !== undefined)) {
65 | return schemaObjects
66 | }
67 |
68 | return schemaObjects.map((schemaObjectsObject) => {
69 | const sanitizeSchemaObject = {
70 | ...parentProperties,
71 | ...schemaObjectsObject,
72 | } as SchemaObject
73 |
74 | if (!sanitizeSchemaObject.type && sanitizeSchemaObject.properties) {
75 | sanitizeSchemaObject.type = 'object'
76 | }
77 |
78 | return sanitizeSchemaObject
79 | })
80 | }
81 |
82 | export type SchemaObject = OpenAPIV2.SchemaObject | OpenAPIV3.NonArraySchemaObject | OpenAPIV3_1.NonArraySchemaObject
83 | export type Properties = Record
84 | export type Discriminator = SchemaObject['discriminator']
85 |
86 | export interface SchemaObjects {
87 | schemaObjects: SchemaObject[]
88 | type: 'anyOf' | 'oneOf'
89 | }
90 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/security.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
2 |
3 | import type { Document } from './document'
4 | import type { Operation } from './operation'
5 | import type { Schema } from './schema'
6 |
7 | export function getSecurityRequirements(operation: Operation, schema?: Schema): SecurityRequirement[] | undefined {
8 | if ('security' in operation) {
9 | return operation.security
10 | } else if (schema && 'security' in schema.document) {
11 | return schema.document.security
12 | }
13 |
14 | return
15 | }
16 |
17 | export function getSecurityDefinitions(document: Document): SecurityDefinitions | undefined {
18 | if ('securityDefinitions' in document) {
19 | return document.securityDefinitions
20 | } else if ('components' in document && 'securitySchemes' in document.components) {
21 | return document.components.securitySchemes as SecurityDefinitions
22 | }
23 |
24 | return
25 | }
26 |
27 | export function isOpenAPIV2OAuth2SecurityScheme(
28 | securityScheme: SecurityScheme,
29 | ): securityScheme is OpenAPIV2.SecuritySchemeOauth2 {
30 | return securityScheme.type === 'oauth2' && 'flow' in securityScheme
31 | }
32 |
33 | export function isOpenAPIV3OAuth2SecurityScheme(
34 | securityScheme: SecurityScheme,
35 | ): securityScheme is OpenAPIV3.OAuth2SecurityScheme | OpenAPIV3_1.OAuth2SecurityScheme {
36 | return securityScheme.type === 'oauth2' && 'flows' in securityScheme
37 | }
38 |
39 | type SecurityScheme = OpenAPIV2.SecuritySchemeObject | OpenAPIV3.SecuritySchemeObject | OpenAPIV3_1.SecuritySchemeObject
40 | export type SecuritySchemeOAuth2Flow = NonNullable<
41 | | OpenAPIV2.SecuritySchemeOauth2
42 | | OpenAPIV3.OAuth2SecurityScheme['flows'][keyof OpenAPIV3.OAuth2SecurityScheme['flows']]
43 | >
44 | export type SecurityDefinitions = Record
45 | type SecurityRequirement =
46 | | OpenAPIV2.SecurityRequirementObject
47 | | OpenAPIV3.SecurityRequirementObject
48 | | OpenAPIV3_1.SecurityRequirementObject
49 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/starlight.ts:
--------------------------------------------------------------------------------
1 | import type { StarlightRouteData } from '@astrojs/starlight/route-data'
2 | import type { HookParameters } from '@astrojs/starlight/types'
3 | import type { MarkdownHeading } from 'astro'
4 |
5 | import { getCallbacks } from './callback'
6 | import type { OperationHttpMethod, OperationTag, PathItemOperation } from './operation'
7 | import { getParametersByLocation } from './parameter'
8 | import { slug, stripLeadingAndTrailingSlashes } from './path'
9 | import { hasRequestBody } from './requestBody'
10 | import { includesDefaultResponse } from './response'
11 | import { getSchemaSidebarGroups, type Schema } from './schema'
12 | import { getSecurityDefinitions, getSecurityRequirements } from './security'
13 | import { capitalize } from './utils'
14 | import type { StarlightOpenAPIContext } from './vite'
15 |
16 | const starlightOpenAPISidebarGroupsLabel = Symbol('StarlightOpenAPISidebarGroupsLabel')
17 |
18 | export function getSidebarGroupsPlaceholder(): SidebarManualGroupConfig[] {
19 | return [
20 | {
21 | collapsed: false,
22 | items: [],
23 | label: starlightOpenAPISidebarGroupsLabel.toString(),
24 | },
25 | ]
26 | }
27 |
28 | export function getPageProps(
29 | title: string,
30 | schema: Schema,
31 | pathItemOperation?: PathItemOperation,
32 | tag?: OperationTag,
33 | ): StarlightPageProps {
34 | const isOverview = pathItemOperation === undefined
35 | const isOperationTag = tag !== undefined
36 |
37 | return {
38 | frontmatter: {
39 | title,
40 | },
41 | headings: isOperationTag
42 | ? getOperationTagHeadings(tag)
43 | : isOverview
44 | ? getOverviewHeadings(schema)
45 | : getOperationHeadings(schema, pathItemOperation),
46 | }
47 | }
48 |
49 | export function getSidebarFromSchemas(
50 | pathname: string,
51 | sidebar: StarlightRouteData['sidebar'],
52 | schemas: Schema[],
53 | context: StarlightOpenAPIContext,
54 | ): StarlightRouteData['sidebar'] {
55 | if (sidebar.length === 0) {
56 | return sidebar
57 | }
58 |
59 | const sidebarGroups = schemas.map((schema) => getSchemaSidebarGroups(pathname, schema, context))
60 |
61 | function replaceSidebarGroupsPlaceholder(group: SidebarGroup): SidebarGroup | SidebarGroup[] {
62 | if (group.label === starlightOpenAPISidebarGroupsLabel.toString()) {
63 | return sidebarGroups
64 | }
65 |
66 | if (isSidebarGroup(group)) {
67 | return {
68 | ...group,
69 | entries: group.entries.flatMap((item) => {
70 | return isSidebarGroup(item) ? replaceSidebarGroupsPlaceholder(item) : item
71 | }),
72 | }
73 | }
74 |
75 | return group
76 | }
77 |
78 | return sidebar.flatMap((item) => {
79 | return isSidebarGroup(item) ? replaceSidebarGroupsPlaceholder(item) : item
80 | })
81 | }
82 |
83 | export function makeSidebarGroup(label: string, entries: SidebarItem[], collapsed: boolean): SidebarGroup {
84 | return { type: 'group', collapsed, entries, label, badge: undefined }
85 | }
86 |
87 | export function makeSidebarLink(pathname: string, label: string, href: string, badge?: SidebarBadge): SidebarLink {
88 | return { type: 'link', isCurrent: pathname === stripLeadingAndTrailingSlashes(href), label, href, badge, attrs: {} }
89 | }
90 |
91 | export function getMethodSidebarBadge(method: OperationHttpMethod): SidebarBadge {
92 | return { class: `sl-openapi-method-${method}`, text: method.toUpperCase(), variant: 'caution' }
93 | }
94 |
95 | function isSidebarGroup(item: SidebarItem): item is SidebarGroup {
96 | return item.type === 'group'
97 | }
98 |
99 | function getOverviewHeadings({ document }: Schema): MarkdownHeading[] {
100 | const items: MarkdownHeading[] = [makeHeading(2, `${document.info.title} (${document.info.version})`, 'overview')]
101 |
102 | const securityDefinitions = getSecurityDefinitions(document)
103 |
104 | if (securityDefinitions) {
105 | items.push(
106 | makeHeading(2, 'Authentication'),
107 | ...Object.keys(securityDefinitions).map((name) => makeHeading(3, name)),
108 | )
109 | }
110 |
111 | return makeHeadings(items)
112 | }
113 |
114 | function getOperationTagHeadings(tag: OperationTag): MarkdownHeading[] {
115 | return [makeHeading(2, tag.name, 'overview')]
116 | }
117 |
118 | function getOperationHeadings(schema: Schema, { operation, pathItem }: PathItemOperation): MarkdownHeading[] {
119 | const items: MarkdownHeading[] = []
120 |
121 | const securityRequirements = getSecurityRequirements(operation, schema)
122 |
123 | if (securityRequirements && securityRequirements.length > 0) {
124 | items.push(makeHeading(2, 'Authorizations'))
125 | }
126 |
127 | const parametersByLocation = getParametersByLocation(operation.parameters, pathItem.parameters)
128 |
129 | if (parametersByLocation.size > 0) {
130 | items.push(
131 | makeHeading(2, 'Parameters'),
132 | ...[...parametersByLocation.keys()].map((location) => makeHeading(3, `${capitalize(location)} Parameters`)),
133 | )
134 | }
135 |
136 | if (hasRequestBody(operation)) {
137 | items.push(makeHeading(2, 'Request Body'))
138 | }
139 |
140 | const callbacks = getCallbacks(operation)
141 | const callbackIdentifiers = Object.keys(callbacks ?? {})
142 |
143 | if (callbackIdentifiers.length > 0) {
144 | items.push(makeHeading(2, 'Callbacks'), ...callbackIdentifiers.map((identifier) => makeHeading(3, identifier)))
145 | }
146 |
147 | if (operation.responses) {
148 | const responseItems: MarkdownHeading[] = []
149 |
150 | for (const name of Object.keys(operation.responses)) {
151 | if (name !== 'default') {
152 | responseItems.push(makeHeading(3, name))
153 | }
154 | }
155 |
156 | if (includesDefaultResponse(operation.responses)) {
157 | responseItems.push(makeHeading(3, 'default'))
158 | }
159 |
160 | items.push(makeHeading(2, 'Responses'), ...responseItems)
161 | }
162 |
163 | return makeHeadings(items)
164 | }
165 |
166 | function makeHeadings(items: MarkdownHeading[]): MarkdownHeading[] {
167 | return [makeHeading(1, 'Overview', '_top'), ...items]
168 | }
169 |
170 | function makeHeading(depth: number, text: string, customSlug?: string): MarkdownHeading {
171 | return { depth, slug: customSlug ?? slug(text), text }
172 | }
173 |
174 | type SidebarUserConfig = NonNullable['config']['sidebar']>
175 |
176 | type SidebarItemConfig = SidebarUserConfig[number]
177 | type SidebarManualGroupConfig = Extract
178 |
179 | type SidebarItem = StarlightRouteData['sidebar'][number]
180 | type SidebarLink = Extract
181 | export type SidebarGroup = Extract
182 |
183 | type SidebarBadge = SidebarItem['badge']
184 |
185 | interface StarlightPageProps {
186 | frontmatter: {
187 | title: string
188 | }
189 | headings: MarkdownHeading[]
190 | }
191 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/utils.ts:
--------------------------------------------------------------------------------
1 | export function capitalize(str: string) {
2 | return str.charAt(0).toUpperCase() + str.slice(1)
3 | }
4 |
5 | export function interspece(
6 | separator: TSeparator,
7 | elements: TElement[],
8 | ): (TElement | TSeparator)[] {
9 | const result: (TElement | TSeparator)[] = []
10 |
11 | for (const [index, element] of elements.entries()) {
12 | if (index === elements.length - 1) {
13 | result.push(element)
14 | } else {
15 | result.push(element, separator)
16 | }
17 | }
18 |
19 | return result
20 | }
21 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/libs/vite.ts:
--------------------------------------------------------------------------------
1 | import type { AstroConfig, ViteUserConfig } from 'astro'
2 |
3 | import type { Schema } from './schema'
4 |
5 | export function vitePluginStarlightOpenAPI(schemas: Schema[], context: StarlightOpenAPIContext): VitePlugin {
6 | const modules = {
7 | 'virtual:starlight-openapi-schemas': `export default ${JSON.stringify(
8 | Object.fromEntries(schemas.map((schema) => [schema.config.base, schema])),
9 | )}`,
10 | 'virtual:starlight-openapi-context': `export default ${JSON.stringify(context)}`,
11 | }
12 |
13 | const moduleResolutionMap = Object.fromEntries(
14 | (Object.keys(modules) as (keyof typeof modules)[]).map((key) => [resolveVirtualModuleId(key), key]),
15 | )
16 |
17 | return {
18 | name: 'vite-plugin-starlight-openapi',
19 | load(id) {
20 | const moduleId = moduleResolutionMap[id]
21 | return moduleId ? modules[moduleId] : undefined
22 | },
23 | resolveId(id) {
24 | return id in modules ? resolveVirtualModuleId(id) : undefined
25 | },
26 | }
27 | }
28 |
29 | function resolveVirtualModuleId(id: TModuleId): `\0${TModuleId}` {
30 | return `\0${id}`
31 | }
32 |
33 | export interface StarlightOpenAPIContext {
34 | trailingSlash: AstroConfig['trailingSlash']
35 | }
36 |
37 | type VitePlugin = NonNullable[number]
38 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/middleware.ts:
--------------------------------------------------------------------------------
1 | import { defineRouteMiddleware } from '@astrojs/starlight/route-data'
2 | import projectContext from 'virtual:starlight-openapi-context'
3 | import schemas from 'virtual:starlight-openapi-schemas'
4 |
5 | import { stripLeadingAndTrailingSlashes } from './libs/path'
6 | import { getSidebarFromSchemas } from './libs/starlight'
7 |
8 | const allSchemas = Object.values(schemas)
9 |
10 | export const onRequest = defineRouteMiddleware((context) => {
11 | const { starlightRoute } = context.locals
12 | const { sidebar } = starlightRoute
13 |
14 | starlightRoute.sidebar = getSidebarFromSchemas(
15 | stripLeadingAndTrailingSlashes(context.url.pathname),
16 | sidebar,
17 | allSchemas,
18 | projectContext,
19 | )
20 | })
21 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "starlight-openapi",
3 | "version": "0.18.0",
4 | "license": "MIT",
5 | "description": "Starlight plugin to generate documentation from OpenAPI/Swagger specifications.",
6 | "author": "HiDeoo (https://hideoo.dev)",
7 | "type": "module",
8 | "exports": {
9 | ".": "./index.ts",
10 | "./middleware": "./middleware.ts",
11 | "./route": "./components/Route.astro",
12 | "./styles": "./styles.css",
13 | "./package.json": "./package.json"
14 | },
15 | "scripts": {
16 | "test": "playwright install --with-deps chromium && playwright test",
17 | "lint": "eslint . --cache --max-warnings=0"
18 | },
19 | "dependencies": {
20 | "@readme/openapi-parser": "^2.7.0",
21 | "github-slugger": "^2.0.0",
22 | "url-template": "^3.1.1"
23 | },
24 | "devDependencies": {
25 | "@playwright/test": "^1.49.1",
26 | "@types/node": "^18.19.68",
27 | "openapi-types": "^12.1.3"
28 | },
29 | "peerDependencies": {
30 | "@astrojs/markdown-remark": ">=6.0.1",
31 | "@astrojs/starlight": ">=0.34.0",
32 | "astro": ">=5.5.0"
33 | },
34 | "engines": {
35 | "node": ">=18.17.1"
36 | },
37 | "packageManager": "pnpm@8.6.12",
38 | "publishConfig": {
39 | "access": "public",
40 | "provenance": true
41 | },
42 | "sideEffects": false,
43 | "keywords": [
44 | "starlight",
45 | "plugin",
46 | "openapi",
47 | "swagger",
48 | "documentation",
49 | "astro"
50 | ],
51 | "homepage": "https://github.com/HiDeoo/starlight-openapi",
52 | "repository": {
53 | "type": "git",
54 | "url": "https://github.com/HiDeoo/starlight-openapi.git",
55 | "directory": "packages/starlight-openapi"
56 | },
57 | "bugs": "https://github.com/HiDeoo/starlight-openapi/issues"
58 | }
59 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, devices } from '@playwright/test'
2 |
3 | export default defineConfig({
4 | forbidOnly: !!process.env['CI'],
5 | projects: [
6 | {
7 | name: 'chromium',
8 | use: { ...devices['Desktop Chrome'], headless: true },
9 | },
10 | ],
11 | use: {
12 | baseURL: 'http://localhost:4321',
13 | },
14 | webServer: [
15 | {
16 | command: 'TEST=1 pnpm build && pnpm preview',
17 | cwd: '../../docs',
18 | reuseExistingServer: !process.env['CI'],
19 | url: 'http://localhost:4321',
20 | },
21 | ],
22 | })
23 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/styles.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --sl-openapi-method-hue-get: 204;
3 | --sl-openapi-method-hue-put: var(--sl-hue-orange);
4 | --sl-openapi-method-hue-post: var(--sl-hue-green);
5 | --sl-openapi-method-hue-delete: var(--sl-hue-red);
6 | --sl-openapi-method-hue-options: var(--sl-hue-blue);
7 | --sl-openapi-method-hue-head: var(--sl-hue-purple);
8 | --sl-openapi-method-hue-patch: 181;
9 | --sl-openapi-method-hue-trace: 224;
10 |
11 | --sl-openapi-method-bg-get: hsl(var(--sl-openapi-method-hue-get), 100%, 10%);
12 | --sl-openapi-method-bg-put: hsl(var(--sl-openapi-method-hue-put), 82%, 13%);
13 | --sl-openapi-method-bg-post: hsl(var(--sl-openapi-method-hue-post), 82%, 13%);
14 | --sl-openapi-method-bg-delete: hsl(var(--sl-openapi-method-hue-delete), 82%, 13%);
15 | --sl-openapi-method-bg-options: hsl(var(--sl-openapi-method-hue-options), 100%, 10%);
16 | --sl-openapi-method-bg-head: hsl(var(--sl-openapi-method-hue-head), 82%, 13%);
17 | --sl-openapi-method-bg-patch: hsl(var(--sl-openapi-method-hue-patch), 82%, 13%);
18 | --sl-openapi-method-bg-trace: hsl(var(--sl-openapi-method-hue-trace), 10%, 12%);
19 |
20 | --sl-openapi-method-border-get: hsl(var(--sl-openapi-method-hue-get), 100%, 60%);
21 | --sl-openapi-method-border-put: hsl(var(--sl-openapi-method-hue-put), 82%, 63%);
22 | --sl-openapi-method-border-post: hsl(var(--sl-openapi-method-hue-post), 82%, 63%);
23 | --sl-openapi-method-border-delete: hsl(var(--sl-openapi-method-hue-delete), 82%, 63%);
24 | --sl-openapi-method-border-options: hsl(var(--sl-openapi-method-hue-options), 100%, 60%);
25 | --sl-openapi-method-border-head: hsl(var(--sl-openapi-method-hue-head), 82%, 63%);
26 | --sl-openapi-method-border-patch: hsl(var(--sl-openapi-method-hue-patch), 82%, 63%);
27 | --sl-openapi-method-border-trace: hsl(var(--sl-openapi-method-hue-trace), 10%, 70%);
28 | }
29 |
30 | :root[data-theme='light'] {
31 | --sl-openapi-method-bg-get: hsl(var(--sl-openapi-method-hue-get), 90%, 40%);
32 | --sl-openapi-method-bg-put: hsl(var(--sl-openapi-method-hue-put), 90%, 32%);
33 | --sl-openapi-method-bg-post: hsl(var(--sl-openapi-method-hue-post), 90%, 21%);
34 | --sl-openapi-method-bg-delete: hsl(var(--sl-openapi-method-hue-delete), 90%, 35%);
35 | --sl-openapi-method-bg-options: hsl(var(--sl-openapi-method-hue-options), 90%, 35%);
36 | --sl-openapi-method-bg-head: hsl(var(--sl-openapi-method-hue-head), 90%, 35%);
37 | --sl-openapi-method-bg-patch: hsl(var(--sl-openapi-method-hue-patch), 90%, 25%);
38 | --sl-openapi-method-bg-trace: hsl(var(--sl-openapi-method-hue-trace), 10%, 12%);
39 |
40 | --sl-openapi-method-border-get: hsl(var(--sl-openapi-method-hue-get), 90%, 60%);
41 | --sl-openapi-method-border-put: hsl(var(--sl-openapi-method-hue-put), 90%, 60%);
42 | --sl-openapi-method-border-post: hsl(var(--sl-openapi-method-hue-post), 90%, 46%);
43 | --sl-openapi-method-border-delete: hsl(var(--sl-openapi-method-hue-delete), 90%, 60%);
44 | --sl-openapi-method-border-options: hsl(var(--sl-openapi-method-hue-options), 90%, 65%);
45 | --sl-openapi-method-border-head: hsl(var(--sl-openapi-method-hue-head), 90%, 60%);
46 | --sl-openapi-method-border-patch: hsl(var(--sl-openapi-method-hue-patch), 90%, 40%);
47 | --sl-openapi-method-border-trace: hsl(var(--sl-openapi-method-hue-trace), 10%, 70%);
48 | }
49 |
50 | .sidebar-content a:not([aria-current='page']) .sl-openapi-method-get {
51 | --sl-color-bg-badge: var(--sl-openapi-method-bg-get);
52 | --sl-color-border-badge: var(--sl-openapi-method-border-get);
53 | }
54 |
55 | .sidebar-content a:not([aria-current='page']) .sl-openapi-method-put {
56 | --sl-color-bg-badge: var(--sl-openapi-method-bg-put);
57 | --sl-color-border-badge: var(--sl-openapi-method-border-put);
58 | }
59 |
60 | .sidebar-content a:not([aria-current='page']) .sl-openapi-method-post {
61 | --sl-color-bg-badge: var(--sl-openapi-method-bg-post);
62 | --sl-color-border-badge: var(--sl-openapi-method-border-post);
63 | }
64 |
65 | .sidebar-content a:not([aria-current='page']) .sl-openapi-method-delete {
66 | --sl-color-bg-badge: var(--sl-openapi-method-bg-delete);
67 | --sl-color-border-badge: var(--sl-openapi-method-border-delete);
68 | }
69 |
70 | .sidebar-content a:not([aria-current='page']) .sl-openapi-method-options {
71 | --sl-color-bg-badge: var(--sl-openapi-method-bg-options);
72 | --sl-color-border-badge: var(--sl-openapi-method-border-options);
73 | }
74 |
75 | .sidebar-content a:not([aria-current='page']) .sl-openapi-method-head {
76 | --sl-color-bg-badge: var(--sl-openapi-method-bg-head);
77 | --sl-color-border-badge: var(--sl-openapi-method-border-head);
78 | }
79 |
80 | .sidebar-content a:not([aria-current='page']) .sl-openapi-method-patch {
81 | --sl-color-bg-badge: var(--sl-openapi-method-bg-patch);
82 | --sl-color-border-badge: var(--sl-openapi-method-border-patch);
83 | }
84 |
85 | .sidebar-content a:not([aria-current='page']) .sl-openapi-method-trace {
86 | --sl-color-bg-badge: var(--sl-openapi-method-bg-trace);
87 | --sl-color-border-badge: var(--sl-openapi-method-border-trace);
88 | }
89 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/tests/callback.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from './test'
2 |
3 | test('hides the callback section with no callbacks', async ({ docPage }) => {
4 | await docPage.goto('/v3/animals/operations/listcats/')
5 |
6 | await expect(docPage.page.getByRole('heading', { level: 2, name: 'Callbacks' })).not.toBeVisible()
7 | })
8 |
9 | test('displays callback sections', async ({ docPage }) => {
10 | await docPage.goto('/v3/animals/operations/feed/')
11 |
12 | await expect(docPage.getCallback('onData')).toBeVisible()
13 |
14 | await expect(docPage.getByText('{$request.query.callbackUrl}/data')).toBeVisible()
15 |
16 | const requestBody = docPage.getCallbackRequestBody('onData')
17 | await expect(requestBody).toBeVisible()
18 | await expect(requestBody.getByText('Subscription payload')).toBeVisible()
19 |
20 | const TwoOTwoResponse = docPage.getCallbackRequestResponse('onData', '202')
21 | await expect(TwoOTwoResponse).toBeVisible()
22 | await expect(
23 | TwoOTwoResponse.getByText(
24 | 'Your server implementation should return this HTTP status code if the data was received successfully',
25 | ),
26 | ).toBeVisible()
27 |
28 | const TwoOFourResponse = docPage.getCallbackRequestResponse('onData', '204')
29 | await expect(TwoOFourResponse).toBeVisible()
30 | await expect(
31 | TwoOFourResponse.getByText(
32 | 'Your server should return this HTTP status code if no longer interested in further updates',
33 | ),
34 | ).toBeVisible()
35 | })
36 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/tests/fixtures/DocPage.ts:
--------------------------------------------------------------------------------
1 | import { expect, type Locator, type Page } from '@playwright/test'
2 |
3 | import { capitalize } from '../../libs/utils'
4 |
5 | export class DocPage {
6 | constructor(public readonly page: Page) {}
7 |
8 | goto(url: string) {
9 | return this.page.goto(`/api${url}`)
10 | }
11 |
12 | getByText(...args: Parameters) {
13 | return this.page.getByText(...args)
14 | }
15 |
16 | getByRole(...args: Parameters) {
17 | return this.page.getByRole(...args)
18 | }
19 |
20 | getContent() {
21 | return this.page.locator('.sl-markdown-content')
22 | }
23 |
24 | async expectToHaveTitle(title: string) {
25 | await expect(this.page).toHaveTitle(`${title} | Starlight OpenAPI`)
26 | await expect(this.page.getByRole('heading', { exact: true, level: 1, name: title })).toBeVisible()
27 | }
28 |
29 | getOperation() {
30 | return this.getContent().getByRole('group').first()
31 | }
32 |
33 | getParameters(location: string) {
34 | return this.page.locator(`.sl-heading-wrapper:has(> h3:text-is("${capitalize(location)} Parameters")) + div > div`)
35 | }
36 |
37 | getRequestBody() {
38 | return this.page.locator('section:has(> .sl-heading-wrapper h2:first-child:text-is("Request Body"))')
39 | }
40 |
41 | getRequestBodyParameter(name: string) {
42 | return this.getRequestBody().locator('.key').filter({ hasText: name })
43 | }
44 |
45 | getCallback(identifier: string, siblingLocator?: string) {
46 | return this.page.locator(
47 | `.sl-heading-wrapper:has(> h2:text-is("Callbacks")) + .sl-heading-wrapper:has(> h3:first-child:has-text("${identifier}"))${
48 | siblingLocator ? ` ${siblingLocator}` : ''
49 | }`,
50 | )
51 | }
52 |
53 | getCallbackRequestBody(identifier: string) {
54 | return this.getCallback(identifier, '~ section:has(> .sl-heading-wrapper h4:first-child:has-text("Request Body"))')
55 | }
56 |
57 | getCallbackRequestResponse(identifier: string, status: string) {
58 | return this.getCallback(identifier, `~ section:has(> .sl-heading-wrapper h5:first-child:text-is("${status}"))`)
59 | }
60 |
61 | getParameter(location: string, name: string) {
62 | return this.getParameters(location).filter({ hasText: name })
63 | }
64 |
65 | getResponse(status: string) {
66 | return this.page.locator(`section:has(> .sl-heading-wrapper h3:first-child:text-is("${status}"))`)
67 | }
68 |
69 | getResponseHeaders(status: string) {
70 | return this.getResponse(status).locator(`.sl-heading-wrapper:has(> h4:text-is("Headers"))`)
71 | }
72 |
73 | getResponseHeader(status: string, name: string) {
74 | return this.getResponseHeaders(status).locator('+ div > div').filter({ hasText: name })
75 | }
76 |
77 | getResponseExamples(status: string) {
78 | return this.getResponse(status).locator('section:has(> .sl-heading-wrapper h4:first-child:text-is("Examples"))')
79 | }
80 |
81 | getAuthorizations() {
82 | return this.page.locator('section:has(> .sl-heading-wrapper h2:first-child:text-is("Authorizations"))')
83 | }
84 |
85 | getAuthentication() {
86 | return this.page.getByRole('heading', { level: 2, name: 'Authentication' })
87 | }
88 |
89 | getAuthenticationMethod(name: string) {
90 | return this.page.locator(`section:has(> .sl-heading-wrapper h3:first-child:text-is("${name}"))`)
91 | }
92 |
93 | getTocItems() {
94 | return this.#getTocChildrenItems(this.page.getByRole('complementary').locator('starlight-toc > nav > ul'))
95 | }
96 |
97 | async #getTocChildrenItems(list: Locator): Promise {
98 | const items: TocItem[] = []
99 |
100 | for (const item of await list.locator('> li').all()) {
101 | const link = await item.locator(`> a`).textContent()
102 | const name = link?.trim() ?? null
103 |
104 | if ((await item.locator('> ul').count()) > 0) {
105 | items.push({
106 | label: name,
107 | items: await this.#getTocChildrenItems(item.locator('> ul')),
108 | })
109 | } else {
110 | items.push({ name })
111 | }
112 | }
113 |
114 | return items
115 | }
116 | }
117 |
118 | type TocItem = TocItemGroup | TocItemLink
119 |
120 | interface TocItemLink {
121 | name: string | null
122 | }
123 |
124 | interface TocItemGroup {
125 | items: (TocItemGroup | TocItemLink)[]
126 | label: string | null
127 | }
128 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/tests/fixtures/SidebarPage.ts:
--------------------------------------------------------------------------------
1 | import type { Locator, Page } from '@playwright/test'
2 |
3 | export class SidebarPage {
4 | constructor(public readonly page: Page) {}
5 |
6 | goto() {
7 | return this.page.goto('/getting-started/')
8 | }
9 |
10 | getSidebarGroupItems(label: string) {
11 | return this.#getSidebarChildrenItems(this.#getSidebarRootDetails(label).locator('> ul'))
12 | }
13 |
14 | get #sidebar() {
15 | return this.page.getByRole('navigation', { name: 'Main' }).locator('div.sidebar-content')
16 | }
17 |
18 | #getSidebarRootDetails(label: string) {
19 | return this.#sidebar.getByRole('listitem').locator(`details:has(summary > div > span:text-is("${label}"))`).last()
20 | }
21 |
22 | async #getSidebarChildrenItems(list: Locator): Promise {
23 | const items: SidebarItem[] = []
24 |
25 | for (const item of await list.locator('> li > :is(a, details)').all()) {
26 | const href = await item.getAttribute('href')
27 |
28 | if (href) {
29 | const name = await item.textContent()
30 |
31 | items.push({ name: name ? name.trim() : null })
32 | } else {
33 | items.push({
34 | collapsed: (await item.getAttribute('open')) === null,
35 | label: await item.locator(`> summary > div > span`).textContent(),
36 | items: await this.#getSidebarChildrenItems(item.locator('> ul')),
37 | })
38 | }
39 | }
40 |
41 | return items
42 | }
43 | }
44 |
45 | type SidebarItem = SidebarItemGroup | SidebarItemLink
46 |
47 | interface SidebarItemLink {
48 | name: string | null
49 | }
50 |
51 | interface SidebarItemGroup {
52 | collapsed: boolean
53 | items: (SidebarItemGroup | SidebarItemLink)[]
54 | label: string | null
55 | }
56 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/tests/header.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from './test'
2 |
3 | test('hides the response headers section with no headers', async ({ docPage }) => {
4 | await docPage.goto('/v2/animals/operations/addwolf/')
5 |
6 | await expect(docPage.getResponseHeaders('200')).not.toBeVisible()
7 | })
8 |
9 | test('displays response headers in v2.0 schema', async ({ docPage }) => {
10 | await docPage.goto('/v2/animals/operations/addbear/')
11 |
12 | const limitHeader = docPage.getResponseHeader('200', 'X-Rate-Limit-Limit')
13 |
14 | await expect(limitHeader).toBeVisible()
15 | await expect(limitHeader.getByText('integer')).toBeVisible()
16 | await expect(limitHeader.getByText('The number of allowed requests in the current period')).toBeVisible()
17 |
18 | await expect(docPage.getResponseHeader('200', 'X-Rate-Limit-Reset')).toBeVisible()
19 | })
20 |
21 | test('displays response headers in v3.0 schema', async ({ docPage }) => {
22 | await docPage.goto('/v3/animals/operations/listbears/')
23 |
24 | const limitHeader = docPage.getResponseHeader('200', 'X-Rate-Limit-Limit')
25 |
26 | await expect(limitHeader).toBeVisible()
27 | await expect(limitHeader.getByText('integer')).toBeVisible()
28 | await expect(limitHeader.getByText('The number of allowed requests in the current period')).toBeVisible()
29 |
30 | await expect(docPage.getResponseHeader('200', 'X-Rate-Limit-Reset')).toBeVisible()
31 | })
32 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/tests/operation.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from './test'
2 |
3 | test('uses the operation summary for title', async ({ docPage }) => {
4 | await docPage.goto('/v3/petstore-simple/operations/listpets/')
5 |
6 | await docPage.expectToHaveTitle('List all pets')
7 | })
8 |
9 | test('falls back to the operation ID for title', async ({ docPage }) => {
10 | await docPage.goto('/petstore/operations/findpets/')
11 |
12 | await docPage.expectToHaveTitle('findPets')
13 | })
14 |
15 | test('displays basic informations', async ({ docPage }) => {
16 | await docPage.goto('/v3/animals/operations/listanimals/')
17 |
18 | await expect(docPage.getByText('Deprecated', { exact: true })).toBeVisible()
19 |
20 | await expect(docPage.getByText('GET', { exact: true })).toBeVisible()
21 | await expect(docPage.getByText('/animals')).toBeVisible()
22 |
23 | await expect(docPage.getByText('Returns all animals')).toBeVisible()
24 |
25 | const externalDocsLink = docPage.getByRole('link', { name: 'Find out more about our animals' })
26 | await expect(externalDocsLink).toBeVisible()
27 | expect(await externalDocsLink.getAttribute('href')).toBe('https://example.com/more-info')
28 | })
29 |
30 | test('displays the operation URL for a v2.0 schema', async ({ docPage }) => {
31 | await docPage.goto('/v2/animals/operations/findanimals/')
32 |
33 | await docPage.getOperation().click()
34 |
35 | expect(await docPage.getOperation().getByRole('textbox').inputValue()).toBe('example.com/api/animals')
36 | })
37 |
38 | test('displays the operation URLs for a v3.0 schema', async ({ docPage }) => {
39 | await docPage.goto('/v3/animals/operations/listdogs/')
40 |
41 | await docPage.getOperation().click()
42 |
43 | await expect(docPage.getOperation().getByText('Default server')).toBeVisible()
44 | expect(await docPage.getOperation().getByRole('textbox').first().inputValue()).toBe('example.com/api/dogs')
45 | await expect(docPage.getOperation().getByText('Sandbox server')).toBeVisible()
46 | expect(await docPage.getOperation().getByRole('textbox').last().inputValue()).toBe('sandbox.example.com/api/dogs')
47 | })
48 |
49 | test('displays overriden operation URLs for a v3.0 schema', async ({ docPage }) => {
50 | await docPage.goto('/v3/animals/operations/listbears/')
51 |
52 | await docPage.getOperation().click()
53 |
54 | await expect(docPage.getOperation().getByText('Custom server')).toBeVisible()
55 | expect(await docPage.getOperation().getByRole('textbox').inputValue()).toBe('custom.example.com/api/bears')
56 | })
57 |
58 | test('generates multiple pages for operations with identical IDs but different methods', async ({ docPage }) => {
59 | await docPage.goto('/v3/animals/operations/turtles/get')
60 |
61 | await docPage.expectToHaveTitle('List all turtles')
62 |
63 | await docPage.goto('/v3/animals/operations/turtles/post')
64 |
65 | await docPage.expectToHaveTitle('/turtles (POST)')
66 | })
67 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/tests/operationTag.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from './test'
2 |
3 | test('displays an operation tag overview', async ({ docPage }) => {
4 | await docPage.goto('/1password/operations/tags/items/')
5 |
6 | await docPage.expectToHaveTitle('Overview')
7 |
8 | await expect(docPage.getByRole('heading', { level: 2, name: 'Items' })).toBeVisible()
9 |
10 | await expect(docPage.getByText('Access and manage items inside 1Password Vaults')).toBeVisible()
11 | })
12 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/tests/overview.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from './test'
2 |
3 | test('displays a basic overview', async ({ docPage }) => {
4 | await docPage.goto('/v3/petstore-simple/')
5 |
6 | await docPage.expectToHaveTitle('Overview')
7 |
8 | await expect(docPage.getByRole('heading', { level: 2, name: 'Swagger Petstore (1.0.0)' })).toBeVisible()
9 |
10 | const details = docPage.getByRole('listitem')
11 |
12 | await expect(details.getByText('License: MIT')).toBeVisible()
13 | await expect(details.getByText('OpenAPI version: 3.0.0')).toBeVisible()
14 | })
15 |
16 | test('displays advanced overviews', async ({ docPage }) => {
17 | await docPage.goto('/petstore/')
18 |
19 | await docPage.expectToHaveTitle('Overview')
20 |
21 | await expect(docPage.getByRole('heading', { level: 2, name: 'Swagger Petstore (1.0.0)' })).toBeVisible()
22 |
23 | await expect(
24 | docPage.getByText(
25 | 'A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification',
26 | ),
27 | ).toBeVisible()
28 |
29 | const details = docPage.getByRole('list')
30 |
31 | await expect(details.getByText('Swagger API Team')).toBeVisible()
32 | expect(await details.getByRole('link', { name: 'http://swagger.io' }).getAttribute('href')).toBe('http://swagger.io')
33 | expect(await details.getByRole('link', { name: 'apiteam@swagger.io' }).getAttribute('href')).toBe(
34 | 'mailto:apiteam@swagger.io',
35 | )
36 |
37 | await expect(details.getByText('License: Apache 2.0')).toBeVisible()
38 | expect(await details.getByRole('link', { name: 'Apache 2.0' }).getAttribute('href')).toBe(
39 | 'https://www.apache.org/licenses/LICENSE-2.0.html',
40 | )
41 |
42 | expect(await details.getByRole('link', { name: 'Terms of Service' }).getAttribute('href')).toBe(
43 | 'http://swagger.io/terms/',
44 | )
45 |
46 | await expect(details.getByText('OpenAPI version: 3.1.0')).toBeVisible()
47 | })
48 |
49 | test('displays external docs link in the overview', async ({ docPage }) => {
50 | await docPage.goto('/v3/animals/')
51 |
52 | const externalDocsLink = docPage.getByRole('link', { name: 'Find out more about our animals' })
53 | await expect(externalDocsLink).toBeVisible()
54 | expect(await externalDocsLink.getAttribute('href')).toBe('https://example.com/more-info')
55 | })
56 |
57 | test('does not display the authentication section if not required', async ({ docPage }) => {
58 | await docPage.goto('/v2/petstore-simple/')
59 |
60 | await expect(docPage.getAuthentication()).not.toBeVisible()
61 | })
62 |
63 | test('displays the authentication section for a v2.0 schema', async ({ docPage }) => {
64 | await docPage.goto('/v2/animals/')
65 |
66 | const basicAuth = docPage.getAuthenticationMethod('basic_auth')
67 |
68 | await expect(basicAuth.getByText('HTTP Basic Authentication')).toBeVisible()
69 | await expect(basicAuth.getByText('Security scheme type: basic')).toBeVisible()
70 |
71 | const apiKey = docPage.getAuthenticationMethod('api_key')
72 |
73 | await expect(apiKey.getByText('API Key Authentication')).toBeVisible()
74 | await expect(apiKey.getByText('Security scheme type: apiKey')).toBeVisible()
75 | await expect(apiKey.getByText('Header parameter name: api_key')).toBeVisible()
76 |
77 | const oAuth2 = docPage.getAuthenticationMethod('animals_auth')
78 |
79 | await expect(oAuth2.getByText('Security scheme type: oauth2')).toBeVisible()
80 | await expect(oAuth2.getByText('Flow type: implicit')).toBeVisible()
81 | await expect(oAuth2.getByText('Authorization URL: ')).toBeVisible()
82 | expect(
83 | await oAuth2.getByRole('link').filter({ hasText: 'https://example.com/api/oauth/dialog' }).getAttribute('href'),
84 | ).toBe('https://example.com/api/oauth/dialog')
85 | await expect(oAuth2.getByText('Token URL: ')).toBeVisible()
86 | expect(
87 | await oAuth2.getByRole('link').filter({ hasText: 'https://example.com/api/oauth/token' }).getAttribute('href'),
88 | ).toBe('https://example.com/api/oauth/token')
89 | await expect(oAuth2.getByText('Scopes:')).toBeVisible()
90 | await expect(oAuth2.getByRole('listitem').getByText('write:animals - write animals')).toBeVisible()
91 | await expect(oAuth2.getByRole('listitem').getByText('read:animals - read animals')).toBeVisible()
92 | })
93 |
94 | test('displays the authentication section for a v3.0 schema', async ({ docPage }) => {
95 | await docPage.goto('/v3/animals/')
96 |
97 | const basicAuth = docPage.getAuthenticationMethod('basic_auth')
98 |
99 | await expect(basicAuth.getByText('Security scheme type: http')).toBeVisible()
100 |
101 | const bearer = docPage.getAuthenticationMethod('bearer_auth')
102 |
103 | await expect(bearer.getByText('Security scheme type: http')).toBeVisible()
104 | await expect(bearer.getByText('Bearer format: JWT')).toBeVisible()
105 |
106 | const apiKey = docPage.getAuthenticationMethod('api_key')
107 |
108 | await expect(apiKey.getByText('Security scheme type: apiKey')).toBeVisible()
109 | await expect(apiKey.getByText('Cookie parameter name: api_key')).toBeVisible()
110 |
111 | const mutualTLS = docPage.getAuthenticationMethod('mutual_tls_auth')
112 |
113 | await expect(mutualTLS.getByText('Security scheme type: mutualTLS')).toBeVisible()
114 |
115 | const oAuth2 = docPage.getAuthenticationMethod('animals_auth')
116 |
117 | await expect(oAuth2.getByText('Security scheme type: oauth2')).toBeVisible()
118 | await expect(oAuth2.getByText('Flow type: implicit')).toBeVisible()
119 | await expect(oAuth2.getByText('Authorization URL: ')).toBeVisible()
120 | expect(
121 | await oAuth2.getByRole('link').filter({ hasText: 'https://example.com/api/oauth/dialog' }).getAttribute('href'),
122 | ).toBe('https://example.com/api/oauth/dialog')
123 | await expect(oAuth2.getByText('Token URL: ')).toBeVisible()
124 | expect(
125 | await oAuth2.getByRole('link').filter({ hasText: 'https://example.com/api/oauth/token' }).getAttribute('href'),
126 | ).toBe('https://example.com/api/oauth/token')
127 | await expect(oAuth2.getByText('Refresh URL: ')).toBeVisible()
128 | expect(
129 | await oAuth2.getByRole('link').filter({ hasText: 'https://example.com/api/oauth/refresh' }).getAttribute('href'),
130 | ).toBe('https://example.com/api/oauth/refresh')
131 | await expect(oAuth2.getByText('Scopes:')).toBeVisible()
132 | await expect(oAuth2.getByRole('listitem').getByText('write:animals - write animals')).toBeVisible()
133 | await expect(oAuth2.getByRole('listitem').getByText('read:animals - read animals')).toBeVisible()
134 |
135 | const openID = docPage.getAuthenticationMethod('openIdConnect')
136 |
137 | await expect(openID.getByText('Security scheme type: openIdConnect')).toBeVisible()
138 | await expect(openID.getByText('OpenID Connect URL: ')).toBeVisible()
139 | expect(
140 | await openID
141 | .getByRole('link')
142 | .filter({ hasText: 'https://example.com/.well-known/openid-configuration' })
143 | .getAttribute('href'),
144 | ).toBe('https://example.com/.well-known/openid-configuration')
145 | })
146 |
--------------------------------------------------------------------------------
/packages/starlight-openapi/tests/recursion.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from './test'
2 |
3 | test('displays the recursive tag for a recursive category schema', async ({ docPage }) => {
4 | await docPage.goto('/v3/recursive/operations/listcategories')
5 |
6 | const okResponse = docPage.getResponse('200')
7 |
8 | await expect(okResponse.getByText('recursive')).toHaveCount(1)
9 | })
10 |
11 | test('displays the recursive tag for a recursive post schema', async ({ docPage }) => {
12 | await docPage.goto('/v3/recursive/operations/listposts')
13 |
14 | const okResponse = docPage.getResponse('200')
15 |
16 | await expect(okResponse.getByText('recursive')).toHaveCount(1)
17 | })
18 |
19 | test('displays the recursive tag for simple and array recursive schema', async ({ docPage }) => {
20 | await docPage.goto('/v3/recursive-simple/operations/listcategories')
21 |
22 | const okResponse = docPage.getResponse('200')
23 |
24 | await expect(okResponse.getByText('recursive')).toHaveCount(2)
25 |
26 | const descriptions = okResponse.locator('.description')
27 |
28 | await expect(descriptions.nth(2)).toHaveText(/object\s+recursive/)
29 | await expect(descriptions.nth(3)).toHaveText(/Array