├── .gitattributes
├── .github
├── dependabot.yml
├── stale.yml
├── tests_checker.yml
└── workflows
│ ├── ci.yml
│ ├── package-manager-ci.yml
│ └── playwright.yml
├── .gitignore
├── .npmignore
├── .npmrc
├── .taprc
├── LICENSE
├── README.md
├── e2e
└── custom.spec.js
├── eslint.config.js
├── examples
├── collection-format.js
├── dynamic-openapi.js
├── dynamic-overwrite-endpoint.js
├── dynamic-swagger.js
├── example-e2e.js
├── example-static-specification.js
├── example-static-specification.json
├── example-static-specification.yaml
├── json-in-querystring.js
├── options.js
├── static-json-file.js
├── static-yaml-file.js
├── static
│ └── example-logo.svg
├── test-package.json
└── theme.js
├── favicon-16x16.png
├── favicon-32x32.png
├── index.js
├── lib
├── index-html.js
├── routes.js
├── serialize.js
└── swagger-initializer.js
├── logo.svg
├── package.json
├── playwright.config.js
├── scripts
└── prepare-swagger-ui.js
├── test
├── .gitkeep
├── csp.test.js
├── decorator.test.js
├── hooks.test.js
├── integration.test.js
├── prepare.test.js
├── route.test.js
├── serialize.test.js
├── static.test.js
├── swagger-initializer.test.js
├── theme.test.js
└── transform-swagger.test.js
└── types
├── .gitkeep
├── http2-types.test-d.ts
├── imports.test-d.ts
├── index.d.ts
├── swagger-ui-vendor-extensions.test-d.ts
└── types.test-d.ts
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set the default behavior, in case people don't have core.autocrlf set
2 | * text=auto
3 |
4 | # Require Unix line endings
5 | * text eol=lf
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 | open-pull-requests-limit: 10
8 |
9 | - package-ecosystem: "npm"
10 | directory: "/"
11 | schedule:
12 | interval: "monthly"
13 | open-pull-requests-limit: 10
14 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 15
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 7
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - "discussion"
8 | - "feature request"
9 | - "bug"
10 | - "help wanted"
11 | - "plugin suggestion"
12 | - "good first issue"
13 | # Label to use when marking an issue as stale
14 | staleLabel: stale
15 | # Comment to post when marking an issue as stale. Set to `false` to disable
16 | markComment: >
17 | This issue has been automatically marked as stale because it has not had
18 | recent activity. It will be closed if no further activity occurs. Thank you
19 | for your contributions.
20 | # Comment to post when closing a stale issue. Set to `false` to disable
21 | closeComment: false
--------------------------------------------------------------------------------
/.github/tests_checker.yml:
--------------------------------------------------------------------------------
1 | comment: |
2 | Hello! Thank you for contributing!
3 | It appears that you have changed the code, but the tests that verify your change are missing. Could you please add them?
4 | fileExtensions:
5 | - '.ts'
6 | - '.js'
7 |
8 | testDir: 'test'
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - next
8 | - 'v*'
9 | paths-ignore:
10 | - 'docs/**'
11 | - '*.md'
12 | pull_request:
13 | paths-ignore:
14 | - 'docs/**'
15 | - '*.md'
16 |
17 | permissions:
18 | contents: read
19 |
20 | jobs:
21 | e2e:
22 | uses: ./.github/workflows/playwright.yml
23 |
24 | test:
25 | permissions:
26 | contents: write
27 | pull-requests: write
28 | uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
29 | needs: e2e
30 | with:
31 | license-check: true
32 | lint: true
33 |
--------------------------------------------------------------------------------
/.github/workflows/package-manager-ci.yml:
--------------------------------------------------------------------------------
1 | name: package-manager-ci
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - next
8 | - 'v*'
9 | paths-ignore:
10 | - 'docs/**'
11 | - '*.md'
12 | pull_request:
13 | paths-ignore:
14 | - 'docs/**'
15 | - '*.md'
16 |
17 | permissions:
18 | contents: read
19 |
20 | jobs:
21 | test:
22 | permissions:
23 | contents: read
24 | uses: fastify/workflows/.github/workflows/plugins-ci-package-manager.yml@v5
25 |
--------------------------------------------------------------------------------
/.github/workflows/playwright.yml:
--------------------------------------------------------------------------------
1 | name: Playwright Tests
2 |
3 | on:
4 | workflow_dispatch:
5 | workflow_call:
6 |
7 | permissions:
8 | contents: read
9 |
10 | jobs:
11 | test:
12 | runs-on: ubuntu-latest
13 | permissions:
14 | contents: read
15 |
16 | timeout-minutes: 60
17 |
18 | steps:
19 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
20 | with:
21 | persist-credentials: false
22 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
23 | with:
24 | check-latest: true
25 | node-version: lts/*
26 | - name: Install dependencies
27 | run: npm i
28 | - name: Install Playwright Browsers
29 | run: npx playwright@1 install chromium --with-deps
30 | - name: Run Playwright tests
31 | run: npx playwright@1 test
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # @fastify/swagger-ui specific
2 | /static
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 | lerna-debug.log*
11 | .pnpm-debug.log*
12 |
13 | # Diagnostic reports (https://nodejs.org/api/report.html)
14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
15 |
16 | # Runtime data
17 | pids
18 | *.pid
19 | *.seed
20 | *.pid.lock
21 |
22 | # Directory for instrumented libs generated by jscoverage/JSCover
23 | lib-cov
24 |
25 | # Tap
26 | .tap
27 |
28 | # Coverage directory used by tools like istanbul
29 | coverage
30 | *.lcov
31 |
32 | # nyc test coverage
33 | .nyc_output
34 |
35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
36 | .grunt
37 |
38 | # Bower dependency directory (https://bower.io/)
39 | bower_components
40 |
41 | # node-waf configuration
42 | .lock-wscript
43 |
44 | # Compiled binary addons (https://nodejs.org/api/addons.html)
45 | build/Release
46 |
47 | # Dependency directories
48 | node_modules/
49 | jspm_packages/
50 |
51 | # Snowpack dependency directory (https://snowpack.dev/)
52 | web_modules/
53 |
54 | # TypeScript cache
55 | *.tsbuildinfo
56 |
57 | # Optional npm cache directory
58 | .npm
59 |
60 | # Optional eslint cache
61 | .eslintcache
62 |
63 | # Optional stylelint cache
64 | .stylelintcache
65 |
66 | # Microbundle cache
67 | .rpt2_cache/
68 | .rts2_cache_cjs/
69 | .rts2_cache_es/
70 | .rts2_cache_umd/
71 |
72 | # Optional REPL history
73 | .node_repl_history
74 |
75 | # Output of 'npm pack'
76 | *.tgz
77 |
78 | # Yarn Integrity file
79 | .yarn-integrity
80 |
81 | # dotenv environment variable files
82 | .env
83 | .env.development.local
84 | .env.test.local
85 | .env.production.local
86 | .env.local
87 |
88 | # parcel-bundler cache (https://parceljs.org/)
89 | .cache
90 | .parcel-cache
91 |
92 | # Next.js build output
93 | .next
94 | out
95 |
96 | # Nuxt.js build / generate output
97 | .nuxt
98 | dist
99 |
100 | # Gatsby files
101 | .cache/
102 | # Comment in the public line in if your project uses Gatsby and not Next.js
103 | # https://nextjs.org/blog/next-9-1#public-directory-support
104 | # public
105 |
106 | # vuepress build output
107 | .vuepress/dist
108 |
109 | # vuepress v2.x temp and cache directory
110 | .temp
111 | .cache
112 |
113 | # Docusaurus cache and generated files
114 | .docusaurus
115 |
116 | # Serverless directories
117 | .serverless/
118 |
119 | # FuseBox cache
120 | .fusebox/
121 |
122 | # DynamoDB Local files
123 | .dynamodb/
124 |
125 | # TernJS port file
126 | .tern-port
127 |
128 | # Stores VSCode versions used for testing VSCode extensions
129 | .vscode-test
130 |
131 | # yarn v2
132 | .yarn/cache
133 | .yarn/unplugged
134 | .yarn/build-state.yml
135 | .yarn/install-state.gz
136 | .pnp.*
137 |
138 | # Vim swap files
139 | *.swp
140 |
141 | # macOS files
142 | .DS_Store
143 |
144 | # Clinic
145 | .clinic
146 |
147 | # lock files
148 | bun.lockb
149 | package-lock.json
150 | pnpm-lock.yaml
151 | yarn.lock
152 |
153 | # editor files
154 | .vscode
155 | .idea
156 |
157 | #tap files
158 | .tap/
159 | /test-results/
160 | /playwright-report/
161 | /playwright/.cache/
162 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # @fastify/swagger-ui specific
2 | # /static
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 | lerna-debug.log*
11 | .pnpm-debug.log*
12 |
13 | # Diagnostic reports (https://nodejs.org/api/report.html)
14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
15 |
16 | # Runtime data
17 | pids
18 | *.pid
19 | *.seed
20 | *.pid.lock
21 |
22 | # Directory for instrumented libs generated by jscoverage/JSCover
23 | lib-cov
24 |
25 | # Coverage directory used by tools like istanbul
26 | coverage
27 | *.lcov
28 |
29 | # nyc test coverage
30 | .nyc_output
31 |
32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
33 | .grunt
34 |
35 | # Bower dependency directory (https://bower.io/)
36 | bower_components
37 |
38 | # node-waf configuration
39 | .lock-wscript
40 |
41 | # Compiled binary addons (https://nodejs.org/api/addons.html)
42 | build/Release
43 |
44 | # Dependency directories
45 | node_modules/
46 | jspm_packages/
47 |
48 | # Snowpack dependency directory (https://snowpack.dev/)
49 | web_modules/
50 |
51 | # TypeScript cache
52 | *.tsbuildinfo
53 |
54 | # Optional npm cache directory
55 | .npm
56 |
57 | # Optional eslint cache
58 | .eslintcache
59 |
60 | # Optional stylelint cache
61 | .stylelintcache
62 |
63 | # Microbundle cache
64 | .rpt2_cache/
65 | .rts2_cache_cjs/
66 | .rts2_cache_es/
67 | .rts2_cache_umd/
68 |
69 | # Optional REPL history
70 | .node_repl_history
71 |
72 | # Output of 'npm pack'
73 | *.tgz
74 |
75 | # Yarn Integrity file
76 | .yarn-integrity
77 |
78 | # dotenv environment variable files
79 | .env
80 | .env.development.local
81 | .env.test.local
82 | .env.production.local
83 | .env.local
84 |
85 | # parcel-bundler cache (https://parceljs.org/)
86 | .cache
87 | .parcel-cache
88 |
89 | # Next.js build output
90 | .next
91 | out
92 |
93 | # Nuxt.js build / generate output
94 | .nuxt
95 | dist
96 |
97 | # Gatsby files
98 | .cache/
99 | # Comment in the public line in if your project uses Gatsby and not Next.js
100 | # https://nextjs.org/blog/next-9-1#public-directory-support
101 | # public
102 |
103 | # vuepress build output
104 | .vuepress/dist
105 |
106 | # vuepress v2.x temp and cache directory
107 | .temp
108 | .cache
109 |
110 | # Docusaurus cache and generated files
111 | .docusaurus
112 |
113 | # Serverless directories
114 | .serverless/
115 |
116 | # FuseBox cache
117 | .fusebox/
118 |
119 | # DynamoDB Local files
120 | .dynamodb/
121 |
122 | # TernJS port file
123 | .tern-port
124 |
125 | # Stores VSCode versions used for testing VSCode extensions
126 | .vscode-test
127 |
128 | # yarn v2
129 | .yarn/cache
130 | .yarn/unplugged
131 | .yarn/build-state.yml
132 | .yarn/install-state.gz
133 | .pnp.*
134 |
135 | # Vim swap files
136 | *.swp
137 |
138 | # macOS files
139 | .DS_Store
140 |
141 | # lock files
142 | package-lock.json
143 | pnpm-lock.yaml
144 | yarn.lock
145 |
146 | # editor files
147 | .vscode
148 | .idea
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/.taprc:
--------------------------------------------------------------------------------
1 | files:
2 | - test/**/*.test.js
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Fastify
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 | # @fastify/swagger-ui
2 |
3 | [](https://www.npmjs.com/package/@fastify/swagger-ui)
4 | [](https://github.com/fastify/fastify-swagger-ui/actions/workflows/ci.yml)
5 | [](https://github.com/neostandard/neostandard)
6 |
7 | A Fastify plugin for serving [Swagger UI](https://swagger.io/tools/swagger-ui/).
8 |
9 | 
10 |
11 |
12 | ## Install
13 | ```
14 | npm i @fastify/swagger-ui
15 | ```
16 |
17 | ### Compatibility
18 |
19 | | Plugin version | Fastify version | Swagger Plugin Version |
20 | | -------------- | --------------- | ---------------------- |
21 | | `^5.x` | `^5.x` | `^9.x` |
22 | | `^2.x` | `^4.x` | `^8.x` |
23 | | `^1.x` | `^4.x` | `^8.x` |
24 |
25 |
26 | Please note that if a Fastify version is out of support, then so are the corresponding versions of this plugin
27 | in the table above.
28 | See [Fastify's LTS policy](https://github.com/fastify/fastify/blob/main/docs/Reference/LTS.md) for more details.
29 |
30 |
31 | ## Usage
32 | Add it with `@fastify/swagger` to your project with `register`, pass it some options, call the `swagger` API, and you are done!
33 |
34 | ```js
35 | import fastify from 'fastify'
36 |
37 | const app = fastify()
38 |
39 | await app.register(import('@fastify/swagger'))
40 |
41 | await app.register(import('@fastify/swagger-ui'), {
42 | routePrefix: '/documentation',
43 | uiConfig: {
44 | docExpansion: 'full',
45 | deepLinking: false
46 | },
47 | uiHooks: {
48 | onRequest: function (request, reply, next) { next() },
49 | preHandler: function (request, reply, next) { next() }
50 | },
51 | staticCSP: true,
52 | transformStaticCSP: (header) => header,
53 | transformSpecification: (swaggerObject, request, reply) => { return swaggerObject },
54 | transformSpecificationClone: true
55 | })
56 |
57 | app.put('/some-route/:id', {
58 | schema: {
59 | description: 'post some data',
60 | tags: ['user', 'code'],
61 | summary: 'qwerty',
62 | params: {
63 | type: 'object',
64 | properties: {
65 | id: {
66 | type: 'string',
67 | description: 'user id'
68 | }
69 | }
70 | },
71 | body: {
72 | type: 'object',
73 | properties: {
74 | hello: { type: 'string' },
75 | obj: {
76 | type: 'object',
77 | properties: {
78 | some: { type: 'string' }
79 | }
80 | }
81 | }
82 | },
83 | response: {
84 | 201: {
85 | description: 'Successful response',
86 | type: 'object',
87 | properties: {
88 | hello: { type: 'string' }
89 | }
90 | },
91 | default: {
92 | description: 'Default response',
93 | type: 'object',
94 | properties: {
95 | foo: { type: 'string' }
96 | }
97 | }
98 | },
99 | security: [
100 | {
101 | "apiKey": []
102 | }
103 | ]
104 | }
105 | }, (req, reply) => {})
106 |
107 | await app.ready()
108 | ```
109 |
110 | ## API
111 |
112 |
113 | ### Register options
114 |
115 | #### Options
116 |
117 | | Option | Default | Description |
118 | | ------------------ | --------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
119 | | baseDir | undefined | Specify the directory where all spec files that are included in the main one using $ref will be located. By default, this is the directory where the main spec file is located. Provided value should be an absolute path without trailing slash. |
120 | | initOAuth | {} | Configuration options for [Swagger UI initOAuth](https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/). |
121 | | routePrefix | '/documentation' | Overwrite the default Swagger UI route prefix. |
122 | | indexPrefix | '' | Add an additional prefix. This is for when the Fastify server is behind path based routing. ex. NGINX |
123 | | staticCSP | false | Enable CSP header for static resources. |
124 | | transformStaticCSP | undefined | Synchronous function to transform CSP header for static resources if the header has been previously set. |
125 | | transformSpecification | undefined | Synchronous function to transform the swagger document. |
126 | | transformSpecificationClone| true | Provide a deepcloned swaggerObject to transformSpecification |
127 | | uiConfig | {} | Configuration options for [Swagger UI](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md). |
128 | | uiHooks | {} | Additional hooks for the documentation's routes. You can provide the `onRequest` and `preHandler` hooks with the same [route's options](https://fastify.dev/docs/latest/Reference/Routes/#routes-options) interface. |
129 | | theme | {} | Add custom JavaScript and CSS to the Swagger UI web page |
130 | | logLevel | info | Allow to define route log level. |
131 |
132 | The plugin will expose the documentation with the following APIs:
133 |
134 | | URL | Description |
135 | | ----------------------- | ------------------------------------------ |
136 | | `'/documentation/json'` | The JSON object representing the API |
137 | | `'/documentation/yaml'` | The YAML object representing the API |
138 | | `'/documentation/'` | The swagger UI |
139 | | `'/documentation/*'` | External files that you may use in `$ref` |
140 |
141 | #### uiConfig
142 |
143 | To configure Swagger UI, you need to modify the `uiConfig` option.
144 | It's important to ensure that functions are self-contained. Keep in mind that
145 | you cannot modify the backend code within the `uiConfig` functions, as these
146 | functions are processed only by the browser. You can reference the Swagger UI
147 | element using `ui`, which is assigned to `window.ui`.
148 |
149 | ##### Example
150 | ```js
151 | const fastify = require('fastify')()
152 |
153 | await fastify.register(require('@fastify/swagger'))
154 |
155 | await fastify.register(require('@fastify/swagger-ui'), {
156 | uiConfig: {
157 | onComplete: function () {
158 | alert('ui has type of ' + typeof ui) // 'ui has type of object'
159 | alert('fastify has type of ' + typeof fastify) // 'fastify has type of undefined'
160 | alert('window has type of ' + typeof window) // 'window has type of object'
161 | alert('global has type of ' + typeof global) // 'global has type of undefined'
162 | }
163 | }
164 | })
165 | ```
166 |
167 | #### transformSpecification
168 |
169 | There can be use cases, where you want to modify the swagger definition on request. E.g. you want to modify the server
170 | definition based on the hostname of the request object. In such a case you can utilize the transformSpecification-option.
171 |
172 | ##### Example
173 | ```js
174 | const fastify = require('fastify')()
175 |
176 | await fastify.register(require('@fastify/swagger'))
177 |
178 | await fastify.register(require('@fastify/swagger-ui'), {
179 | transformSpecification: (swaggerObject, req, reply) => {
180 | swaggerObject.host = req.hostname
181 | return swaggerObject
182 | }
183 | })
184 | ```
185 |
186 | By default fastify.swagger() will be deepcloned and passed to the transformSpecification-function, as fastify.swagger()
187 | returns a mutatable Object. You can disable the deepcloning by setting transformSpecificationClone to false. This is useful,
188 | if you want to handle the deepcloning in the transformSpecification function.
189 |
190 | ##### Example with caching
191 | ```js
192 | const fastify = require('fastify')()
193 | const LRU = require('tiny-lru').lru
194 | const rfdc = require('rfdc')()
195 |
196 | await fastify.register(require('@fastify/swagger'))
197 |
198 | const swaggerLru = new LRU(1000)
199 | await fastify.register(require('@fastify/swagger-ui'), {
200 | transformSpecificationClone: false,
201 | transformSpecification: (swaggerObject, req, reply) => {
202 | if (swaggerLru.has(req.hostname)) {
203 | return swaggerLru.get(req.hostname)
204 | }
205 | const clonedSwaggerObject = rfdc(swaggerObject)
206 | clonedSwaggerObject.host = req.hostname
207 | swaggerLru.set(req.hostname, clonedSwaggerObject)
208 | return clonedSwaggerObject
209 | }
210 | })
211 | ```
212 |
213 | #### theme
214 |
215 | You can add custom JavaScript and CSS to the Swagger UI web page by using the theme option.
216 |
217 | ##### Example
218 |
219 | ```js
220 | const fastify = require('fastify')()
221 |
222 | await fastify.register(require('@fastify/swagger'))
223 |
224 | await fastify.register(require('@fastify/swagger-ui'), {
225 | theme: {
226 | title: 'My custom title',
227 | js: [
228 | { filename: 'special.js', content: 'alert("client javascript")' }
229 | ],
230 | css: [
231 | { filename: 'theme.css', content: '* { border: 1px red solid; }' }
232 | ],
233 | favicon: [
234 | {
235 | filename: 'favicon.png',
236 | rel: 'icon',
237 | sizes: '16x16',
238 | type: 'image/png',
239 | content: Buffer.from('iVBOR...', 'base64')
240 | }
241 | ]
242 | }
243 | })
244 | ```
245 |
246 | You can add custom JavaScript and CSS to the Swagger UI web page by using the theme option.
247 |
248 | #### logo
249 |
250 | It's possible to override the logo displayed in the top bar by specifying:
251 |
252 | ```js
253 | await fastify.register(require('@fastify/swagger-ui'), {
254 | logo: {
255 | type: 'image/png',
256 | content: Buffer.from('iVBOR...', 'base64'),
257 | href: '/documentation',
258 | target: '_blank'
259 | },
260 | theme: {
261 | favicon: [
262 | {
263 | filename: 'favicon.png',
264 | rel: 'icon',
265 | sizes: '16x16',
266 | type: 'image/png',
267 | content: Buffer.from('iVBOR...', 'base64')
268 | }
269 | ]
270 | }
271 | })
272 | ```
273 |
274 | #### Protect your documentation routes
275 |
276 | You can protect your documentation by configuring an authentication hook.
277 | Here is an example using the [`@fastify/basic-auth`](https://github.com/fastify/fastify-basic-auth) plugin:
278 |
279 | ##### Example
280 | ```js
281 | const fastify = require('fastify')()
282 | const crypto = require('node:crypto')
283 |
284 | await fastify.register(require('@fastify/swagger'))
285 |
286 | // perform constant-time comparison to prevent timing attacks
287 | function compare (a, b) {
288 | a = Buffer.from(a)
289 | b = Buffer.from(b)
290 | if (a.length !== b.length) {
291 | // Delay return with cryptographically secure timing check.
292 | crypto.timingSafeEqual(a, a)
293 | return false
294 | }
295 |
296 | return crypto.timingSafeEqual(a, b)
297 | }
298 |
299 | await fastify.register(require('@fastify/basic-auth'), {
300 | validate (username, password, req, reply, done) {
301 | let result = true
302 | result = compare(username, validUsername) && result
303 | result = compare(password, validPassword) && result
304 | if (result) {
305 | done()
306 | } else {
307 | done(new Error('Access denied'))
308 | }
309 | },
310 | authenticate: true
311 | })
312 |
313 | await fastify.register(require('@fastify/swagger-ui', {
314 | uiHooks: {
315 | onRequest: fastify.basicAuth
316 | }
317 | })
318 | ```
319 |
320 | #### Rendering models at the bottom of the page
321 |
322 | To ensure that models are correctly rendered at the bottom of the Swagger UI page, it's important to define your schemas using $refs through [fastify.addSchema](https://fastify.dev/docs/latest/Reference/Validation-and-Serialization/#adding-a-shared-schema). Directly embedding JSON schemas within the schema property of your route definitions in Fastify may lead to them not being displayed in Swagger UI.
323 |
324 | #### validatorUrl
325 |
326 | [SwaggerUI](https://github.com/swagger-api/swagger-ui/) can automatically validate the given specification using an online validator.
327 | To enable this behavior you can pass the [`validatorUrl`](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md) option
328 | to this plugin which will be forwarded to SwaggerUI.
329 |
330 | ```js
331 | await fastify.register('@fastify/swagger-ui', {
332 | validatorUrl: 'https://validator.swagger.io/validator'
333 | })
334 | ```
335 |
336 | Note that this behavior is disabled by default in `@fastify/swagger-ui`.
337 |
338 | ### Bundling
339 |
340 | To bundle Swagger UI with your application, the swagger-ui static files need to be copied to the server and the `baseDir` option set to point to the file directory.
341 |
342 | Copy files with esbuild
343 |
344 | ```js
345 | import { build } from 'esbuild'
346 | import { copy } from 'esbuild-plugin-copy'
347 |
348 | await build({
349 | // ...
350 | plugins: [
351 | copy({
352 | resolveFrom: 'cwd',
353 | assets: {
354 | from: ['node_modules/@fastify/swagger-ui/static/*'],
355 | to: ['dist/static'],
356 | },
357 | }),
358 | ],
359 | })
360 | ```
361 |
362 |
363 |
364 |
365 | Copy files with docker
366 |
367 | ```Dockerfile
368 | COPY ./node_modules/@fastify/swagger-ui/static /app/static
369 | ```
370 |
371 |
372 |
373 | #### Configure Swagger UI to use a custom baseDir
374 | Set the `baseDir` option to point to your folder.
375 |
376 | ```js
377 | await fastify.register(require('@fastify/swagger-ui'), {
378 | baseDir: isDev ? undefined : path.resolve('static'),
379 | })
380 | ```
381 |
382 |
383 | ## License
384 |
385 | Licensed under [MIT](./LICENSE).
386 |
--------------------------------------------------------------------------------
/e2e/custom.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { test, expect } = require('@playwright/test')
4 |
5 | const URL_DOCUMENTATION = '/documentation'
6 | const URL_FAVICON = '/documentation/static/theme/favicon.svg'
7 |
8 | test.describe('Check customizations', () => {
9 | test('Check JS injection', async ({ page }) => {
10 | await page.goto(URL_DOCUMENTATION)
11 | await page.waitForLoadState('networkidle')
12 |
13 | page.on('dialog', async dialog => {
14 | expect(dialog.type() === 'beforeunload').toBeTruthy()
15 | expect(dialog.message() === 'unloaded test-theme').toBeTruthy()
16 | await dialog.dismiss()
17 | })
18 | await page.close({ runBeforeUnload: true })
19 | })
20 |
21 | test('Check CSS injection', async ({ page }) => {
22 | await page.goto(URL_DOCUMENTATION)
23 | await page.waitForLoadState('networkidle')
24 |
25 | const element = await page.waitForSelector('button.download-url-button')
26 | const color = await element.evaluate(el => window.getComputedStyle(el).getPropertyValue('background-color'))
27 | expect(color).toBe('rgb(255, 0, 0)')
28 | })
29 |
30 | test('Check custom favicon', async ({ page }) => {
31 | await page.goto(URL_FAVICON)
32 |
33 | const faviconId = await (await page.waitForSelector('svg')).getAttribute('id')
34 | expect(faviconId).toBe('example-logo') // it is included in the svg file
35 | })
36 |
37 | test('Check custom logo', async ({ page }) => {
38 | await page.goto(URL_DOCUMENTATION)
39 | await page.waitForLoadState('networkidle')
40 |
41 | const logoSrc = await page.locator('img').first().getAttribute('src')
42 | await page.goto(logoSrc)
43 |
44 | const logoId = await (await page.waitForSelector('svg')).getAttribute('id')
45 | expect(logoId).toBe('example-logo') // it is included in the svg file
46 | })
47 | })
48 |
49 | test.describe('Check redirection and url handling of static assets', () => {
50 | test('Check static/index.html redirects', async ({ page }) => {
51 | const jsonResponsePromise = page.waitForResponse(/json/)
52 | await page.goto(`${URL_DOCUMENTATION}/static/index.html`)
53 |
54 | // Check if the page is redirected to /documentation
55 | const url = await page.url()
56 | expect(url).toContain(`${URL_DOCUMENTATION}`)
57 | expect(url).not.toContain('static/index.html')
58 |
59 | // Check if the page has requested the json spec, and if so has it succeeded
60 | const jsonResponse = await jsonResponsePromise
61 | expect(jsonResponse.ok()).toBe(true)
62 | })
63 |
64 | test('Check root UI without slash loads json spec', async ({ page }) => {
65 | const jsonResponsePromise = page.waitForResponse(/json/)
66 | await page.goto(`${URL_DOCUMENTATION}`)
67 |
68 | // Check if the page has requested the json spec, and if so has it succeeded
69 | const jsonResponse = await jsonResponsePromise
70 | expect(jsonResponse.ok()).toBe(true)
71 | })
72 |
73 | test('Check root UI with trailing slash loads json spec', async ({ page }) => {
74 | const jsonResponsePromise = page.waitForResponse(/json/)
75 | await page.goto(`${URL_DOCUMENTATION}/`)
76 |
77 | // Check if the page has requested the json spec, and if so has it succeeded
78 | const jsonResponse = await jsonResponsePromise
79 | expect(jsonResponse.ok()).toBe(true)
80 | })
81 |
82 | test('Check root UI with hash loads json spec', async ({ page }) => {
83 | const jsonResponsePromise = page.waitForResponse(/json/)
84 | await page.goto(`${URL_DOCUMENTATION}#default/get_example`)
85 |
86 | // Check if the page has requested the json spec, and if so has it succeeded
87 | const jsonResponse = await jsonResponsePromise
88 | expect(jsonResponse.ok()).toBe(true)
89 | })
90 | })
91 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = require('neostandard')({
4 | ignores: [
5 | ...require('neostandard').resolveIgnoresFromGitignore(),
6 | 'dist'
7 | ],
8 | ts: true
9 | })
10 |
--------------------------------------------------------------------------------
/examples/collection-format.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const Fastify = require('fastify')
4 |
5 | ; (async () => {
6 | const fastify = Fastify({
7 | logger: true,
8 | // Need to add a collectionFormat keyword to ajv in fastify instance
9 | ajv: {
10 | customOptions: {
11 | keywords: ['collectionFormat']
12 | }
13 | }
14 | })
15 |
16 | await fastify.register(require('@fastify/swagger'))
17 | await fastify.register(require('../index'))
18 |
19 | fastify.route({
20 | method: 'GET',
21 | url: '/',
22 | schema: {
23 | querystring: {
24 | type: 'object',
25 | required: ['fields'],
26 | additionalProperties: false,
27 | properties: {
28 | fields: {
29 | type: 'array',
30 | items: {
31 | type: 'string'
32 | },
33 | minItems: 1,
34 | //
35 | // Note that this is an Open API version 2 configuration option. The
36 | // options changed in version 3. The plugin currently only supports
37 | // version 2 of Open API.
38 | //
39 | // Put `collectionFormat` on the same property which you are defining
40 | // as an array of values. (i.e. `collectionFormat` should be a sibling
41 | // of the `type: "array"` specification.)
42 | collectionFormat: 'multi'
43 | }
44 | }
45 | }
46 | },
47 | handler (request, reply) {
48 | reply.send(request.query.fields)
49 | }
50 | })
51 |
52 | fastify.listen({ port: 3000 }, (err, addr) => {
53 | if (err) throw err
54 | fastify.log.info(`Visit the documentation at ${addr}/documentation/`)
55 | })
56 | }
57 | )()
58 |
--------------------------------------------------------------------------------
/examples/dynamic-openapi.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const Fastify = require('fastify')
4 |
5 | ; (async () => {
6 | const fastify = Fastify({ logger: true })
7 | await fastify.register(require('@fastify/swagger'), {
8 | openapi: {
9 | info: {
10 | title: 'Test swagger',
11 | description: 'testing the fastify swagger api',
12 | version: '0.1.0'
13 | },
14 | servers: [{
15 | url: 'http://localhost'
16 | }],
17 | components: {
18 | securitySchemes: {
19 | apiKey: {
20 | type: 'apiKey',
21 | name: 'apiKey',
22 | in: 'header'
23 | }
24 | }
25 | }
26 | },
27 | hideUntagged: true
28 | })
29 |
30 | await fastify.register(require('../index'), {
31 | validatorUrl: false
32 | })
33 |
34 | await fastify.register(async function (fastify) {
35 | fastify.put('/some-route/:id', {
36 | schema: {
37 | description: 'post some data',
38 | tags: ['user', 'code'],
39 | summary: 'qwerty',
40 | security: [{ apiKey: [] }],
41 | params: {
42 | type: 'object',
43 | properties: {
44 | id: {
45 | type: 'string',
46 | description: 'user id'
47 | }
48 | }
49 | },
50 | body: {
51 | type: 'object',
52 | properties: {
53 | hello: { type: 'string' },
54 | obj: {
55 | type: 'object',
56 | properties: {
57 | some: { type: 'string' }
58 | }
59 | }
60 | }
61 | },
62 | response: {
63 | 201: {
64 | description: 'Succesful response',
65 | type: 'object',
66 | properties: {
67 | hello: { type: 'string' }
68 | }
69 | },
70 | default: {
71 | description: 'Default response',
72 | type: 'object',
73 | properties: {
74 | foo: { type: 'string' }
75 | }
76 | }
77 | }
78 | }
79 | }, (req, reply) => { reply.send({ hello: `Hello ${req.body.hello}` }) })
80 |
81 | fastify.post('/some-route/:id', {
82 | schema: {
83 | description: 'post some data',
84 | summary: 'qwerty',
85 | security: [{ apiKey: [] }],
86 | params: {
87 | type: 'object',
88 | properties: {
89 | id: {
90 | type: 'string',
91 | description: 'user id'
92 | }
93 | }
94 | },
95 | body: {
96 | type: 'object',
97 | properties: {
98 | hello: { type: 'string' },
99 | obj: {
100 | type: 'object',
101 | properties: {
102 | some: { type: 'string' }
103 | }
104 | }
105 | }
106 | },
107 | response: {
108 | 201: {
109 | description: 'Succesful response',
110 | type: 'object',
111 | properties: {
112 | hello: { type: 'string' }
113 | }
114 | }
115 | }
116 | }
117 | }, (req, reply) => { reply.send({ hello: `Hello ${req.body.hello}` }) })
118 | })
119 |
120 | fastify.listen({ port: 3000, hostname: '0.0.0.0' }, (err, addr) => {
121 | if (err) throw err
122 | fastify.log.info(`Visit the documentation at ${addr}/documentation/`)
123 | })
124 | })()
125 |
--------------------------------------------------------------------------------
/examples/dynamic-overwrite-endpoint.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const Fastify = require('fastify')
4 |
5 | ; (async () => {
6 | const fastify = Fastify({ logger: true })
7 |
8 | await fastify.register(require('@fastify/swagger'), {
9 | swagger: {
10 | info: {
11 | title: 'Test swagger',
12 | description: 'testing the fastify swagger api',
13 | version: '0.1.0'
14 | },
15 | host: 'localhost',
16 | schemes: ['http'],
17 | consumes: ['application/json'],
18 | produces: ['application/json']
19 | }
20 | })
21 |
22 | await fastify.register(require('../index'), {
23 | routePrefix: '/swagger-docs'
24 | })
25 |
26 | await fastify.register(async function (fastify) {
27 | fastify.put('/some-route/:id', {
28 | schema: {
29 | description: 'post some data',
30 | tags: ['user', 'code'],
31 | summary: 'qwerty',
32 | params: {
33 | type: 'object',
34 | properties: {
35 | id: {
36 | type: 'string',
37 | description: 'user id'
38 | }
39 | }
40 | },
41 | body: {
42 | type: 'object',
43 | properties: {
44 | hello: { type: 'string' },
45 | obj: {
46 | type: 'object',
47 | properties: {
48 | some: { type: 'string' }
49 | }
50 | }
51 | }
52 | },
53 | response: {
54 | 201: {
55 | description: 'Succesful response',
56 | type: 'object',
57 | properties: {
58 | hello: { type: 'string' }
59 | }
60 | }
61 | }
62 | }
63 | }, (req, reply) => {})
64 | })
65 |
66 | fastify.listen({ port: 3000, hostname: '0.0.0.0' }, (err, addr) => {
67 | if (err) throw err
68 | fastify.log.info(`Visit the documentation at ${addr}/swagger-docs/`)
69 | })
70 | })()
71 |
--------------------------------------------------------------------------------
/examples/dynamic-swagger.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const Fastify = require('fastify')
4 |
5 | ; (async () => {
6 | const fastify = Fastify({ logger: true })
7 |
8 | await fastify.register(require('@fastify/swagger'), {
9 | swagger: {
10 | info: {
11 | title: 'Test swagger',
12 | description: 'testing the fastify swagger api',
13 | version: '0.1.0'
14 | },
15 | securityDefinitions: {
16 | apiKey: {
17 | type: 'apiKey',
18 | name: 'apiKey',
19 | in: 'header'
20 | }
21 | },
22 | host: 'localhost:3000',
23 | schemes: ['http'],
24 | consumes: ['application/json'],
25 | produces: ['application/json']
26 | },
27 | hideUntagged: true
28 | })
29 |
30 | await fastify.register(require('../index'), {
31 | routePrefix: '/swagger-docs'
32 | })
33 |
34 | fastify.addSchema({
35 | $id: 'user',
36 | type: 'object',
37 | properties: {
38 | id: {
39 | type: 'string',
40 | description: 'user id'
41 | }
42 | }
43 | })
44 |
45 | fastify.addSchema({
46 | $id: 'some',
47 | type: 'object',
48 | properties: {
49 | some: { type: 'string' }
50 | }
51 | })
52 |
53 | await fastify.register(async function (fastify) {
54 | fastify.put('/some-route/:id', {
55 | schema: {
56 | description: 'post some data',
57 | tags: ['user', 'code'],
58 | summary: 'qwerty',
59 | security: [{ apiKey: [] }],
60 | params: { $ref: 'user#' },
61 | body: {
62 | type: 'object',
63 | properties: {
64 | hello: { type: 'string' },
65 | obj: { $ref: 'some#' }
66 | }
67 | },
68 | response: {
69 | 201: {
70 | description: 'Succesful response',
71 | type: 'object',
72 | properties: {
73 | hello: { type: 'string' }
74 | }
75 | },
76 | default: {
77 | description: 'Default response',
78 | type: 'object',
79 | properties: {
80 | foo: { type: 'string' }
81 | }
82 | }
83 | }
84 | }
85 | }, (req, reply) => { reply.send({ hello: `Hello ${req.body.hello}` }) })
86 |
87 | fastify.post('/some-route/:id', {
88 | schema: {
89 | description: 'post some data',
90 | summary: 'qwerty',
91 | security: [{ apiKey: [] }],
92 | params: { $ref: 'user#' },
93 | body: {
94 | type: 'object',
95 | properties: {
96 | hello: { type: 'string' },
97 | obj: { $ref: 'some#' }
98 | }
99 | },
100 | response: {
101 | 201: {
102 | description: 'Succesful response',
103 | type: 'object',
104 | properties: {
105 | hello: { type: 'string' }
106 | }
107 | }
108 | }
109 | }
110 | }, (req, reply) => { reply.send({ hello: `Hello ${req.body.hello}` }) })
111 | })
112 |
113 | fastify.listen({ port: 3000, hostname: '0.0.0.0' }, (err, addr) => {
114 | if (err) throw err
115 | fastify.log.info(`Visit the documentation at ${addr}/swagger-docs/`)
116 | })
117 | })()
118 |
--------------------------------------------------------------------------------
/examples/example-e2e.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const Fastify = require('fastify')
4 | const readFileSync = require('node:fs').readFileSync
5 | const resolve = require('node:path').resolve
6 |
7 | const exampleLogo = readFileSync(
8 | resolve(__dirname, '..', 'examples/static', 'example-logo.svg'),
9 | 'utf8'
10 | )
11 |
12 | ; (async () => {
13 | const fastify = Fastify({ logger: true })
14 |
15 | await fastify.register(require('@fastify/swagger'), {
16 | mode: 'static',
17 | specification: {
18 | path: './examples/example-static-specification.json'
19 | }
20 | })
21 |
22 | await fastify.register(require('../index'), {
23 | theme: {
24 | js: [
25 | { filename: 'unloaded.js', content: 'window.onbeforeunload = function(){alert("unloaded test-theme")}' }
26 | ],
27 | css: [
28 | { filename: 'theme.css', content: '.download-url-button {background: red !important;}' }
29 | ],
30 | favicon: [
31 | {
32 | filename: 'favicon.svg',
33 | rel: 'icon',
34 | sizes: '16x16',
35 | type: 'image/svg+xml',
36 | content: exampleLogo
37 | }
38 | ]
39 | },
40 | logo: {
41 | type: 'image/svg+xml',
42 | content: exampleLogo
43 | }
44 | })
45 |
46 | fastify.listen({ port: process.env.PORT }, (err, addr) => {
47 | if (err) throw err
48 | fastify.log.info(`Visit the documentation at ${addr}/documentation/`)
49 | })
50 | })()
51 |
--------------------------------------------------------------------------------
/examples/example-static-specification.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fastify/fastify-swagger-ui/197d6b2dda514b787db3f5628a7f95e4e7f603b5/examples/example-static-specification.js
--------------------------------------------------------------------------------
/examples/example-static-specification.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.0",
3 | "info": {
4 | "description": "Test swagger specification",
5 | "version": "1.0.0",
6 | "title": "Test swagger specification",
7 | "contact": {
8 | "email": "super.developer@gmail.com"
9 | }
10 | },
11 | "servers": [
12 | {
13 | "url": "http://localhost:3000/",
14 | "description": "Localhost (uses test data)"
15 | }
16 | ],
17 | "paths": {
18 | "/status": {
19 | "get": {
20 | "description": "Status route, so we can check if server is alive",
21 | "tags": [
22 | "Status"
23 | ],
24 | "responses": {
25 | "200": {
26 | "description": "Server is alive",
27 | "content": {
28 | "application/json": {
29 | "schema": {
30 | "type": "object",
31 | "properties": {
32 | "health": {
33 | "type": "boolean"
34 | },
35 | "date": {
36 | "type": "string"
37 | }
38 | },
39 | "example": {
40 | "health": true,
41 | "date": "2018-02-19T15:36:46.758Z"
42 | }
43 | }
44 | }
45 | }
46 | }
47 | }
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/examples/example-static-specification.yaml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.0
2 | info:
3 | description: "Test swagger specification"
4 | version: "1.0.0"
5 | title: "Test swagger specification"
6 | contact:
7 | email: "super.developer@gmail.com"
8 | servers:
9 | - url: http://localhost:3000/
10 | description: Localhost (uses test data)
11 | paths:
12 | /status:
13 | get:
14 | description: Status route, so we can check if server is alive
15 | tags:
16 | - Status
17 | responses:
18 | 200:
19 | description: 'Server is alive'
20 | content:
21 | application/json:
22 | schema:
23 | type: object
24 | properties:
25 | health:
26 | type: boolean
27 | date:
28 | type: string
29 | example:
30 | health: true
31 | date: "2018-02-19T15:36:46.758Z"
--------------------------------------------------------------------------------
/examples/json-in-querystring.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const Fastify = require('fastify')
4 | const qs = require('qs')
5 | const Ajv = require('ajv')
6 |
7 | ; (async () => {
8 | const ajv = new Ajv({
9 | removeAdditional: true,
10 | useDefaults: true,
11 | coerceTypes: true
12 | })
13 |
14 | const fastify = Fastify({
15 | logger: true,
16 | querystringParser: (str) => {
17 | const result = qs.parse(str)
18 |
19 | if (result.filter && typeof result.filter === 'string') {
20 | result.filter = JSON.parse(result.filter)
21 | }
22 |
23 | return result
24 | }
25 | })
26 |
27 | ajv.addKeyword({
28 | keyword: 'x-consume',
29 | code: (ctx) => Promise.resolve(true)
30 | })
31 |
32 | fastify.setValidatorCompiler(({ schema }) => ajv.compile(schema))
33 |
34 | await fastify.register(require('@fastify/swagger'), {
35 | openapi: {
36 | info: {
37 | title: 'Test swagger',
38 | description: 'testing the fastify swagger api',
39 | version: '0.1.0'
40 | }
41 | }
42 | })
43 | await fastify.register(require('../index'))
44 |
45 | await fastify.register(async function (fastify) {
46 | fastify.route({
47 | method: 'GET',
48 | url: '/',
49 | schema: {
50 | querystring: {
51 | type: 'object',
52 | required: ['filter'],
53 | additionalProperties: false,
54 | properties: {
55 | filter: {
56 | type: 'object',
57 | required: ['foo'],
58 | properties: {
59 | foo: { type: 'string' },
60 | bar: { type: 'string' }
61 | },
62 | 'x-consume': 'application/json'
63 | }
64 | }
65 | }
66 | },
67 | handler (request, reply) {
68 | reply.send(request.query.filter)
69 | }
70 | })
71 | })
72 |
73 | fastify.listen({ port: 3000 }, (err, addr) => {
74 | if (err) throw err
75 | fastify.log.info(`Visit the documentation at ${addr}/documentation/`)
76 | })
77 | })()
78 |
--------------------------------------------------------------------------------
/examples/options.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const swaggerOption = {
4 | swagger: {
5 | info: {
6 | title: 'Test swagger',
7 | description: 'testing the fastify swagger api',
8 | version: '0.1.0'
9 | },
10 | host: 'localhost',
11 | schemes: ['http'],
12 | consumes: ['application/json'],
13 | produces: ['application/json'],
14 | tags: [
15 | { name: 'tag' }
16 | ],
17 | externalDocs: {
18 | description: 'Find more info here',
19 | url: 'https://swagger.io'
20 | },
21 | securityDefinitions: {
22 | apiKey: {
23 | type: 'apiKey',
24 | name: 'apiKey',
25 | in: 'header'
26 | }
27 | },
28 | security: [{
29 | apiKey: []
30 | }]
31 | }
32 | }
33 |
34 | const openapiOption = {
35 | openapi: {
36 | info: {
37 | title: 'Test swagger',
38 | description: 'testing the fastify swagger api',
39 | version: '0.1.0'
40 | },
41 | servers: [
42 | {
43 | url: 'http://localhost'
44 | }
45 | ],
46 | tags: [
47 | { name: 'tag' }
48 | ],
49 | components: {
50 | securitySchemes: {
51 | apiKey: {
52 | type: 'apiKey',
53 | name: 'apiKey',
54 | in: 'header'
55 | }
56 | }
57 | },
58 | security: [{
59 | apiKey: []
60 | }],
61 | externalDocs: {
62 | description: 'Find more info here',
63 | url: 'https://swagger.io'
64 | }
65 | }
66 | }
67 |
68 | const openapiRelativeOptions = {
69 | openapi: {
70 | info: {
71 | title: 'Test swagger',
72 | description: 'testing the fastify swagger api',
73 | version: '0.1.0'
74 | },
75 | servers: [
76 | {
77 | url: '/test'
78 | }
79 | ],
80 | tags: [
81 | { name: 'tag' }
82 | ],
83 | components: {
84 | securitySchemes: {
85 | apiKey: {
86 | type: 'apiKey',
87 | name: 'apiKey',
88 | in: 'header'
89 | }
90 | }
91 | },
92 | security: [{
93 | apiKey: []
94 | }],
95 | externalDocs: {
96 | description: 'Find more info here',
97 | url: 'https://swagger.io'
98 | }
99 | },
100 | stripBasePath: false
101 | }
102 |
103 | const schemaQuerystring = {
104 | schema: {
105 | response: {
106 | 200: {
107 | type: 'object',
108 | properties: {
109 | hello: { type: 'string' }
110 | }
111 | }
112 | },
113 | querystring: {
114 | type: 'object',
115 | properties: {
116 | hello: { type: 'string' },
117 | world: { type: 'string' }
118 | }
119 | }
120 | }
121 | }
122 |
123 | const schemaBody = {
124 | schema: {
125 | body: {
126 | type: 'object',
127 | properties: {
128 | hello: { type: 'string' },
129 | obj: {
130 | type: 'object',
131 | properties: {
132 | some: { type: 'string' },
133 | constantProp: { const: 'my-const' }
134 | }
135 | }
136 | },
137 | required: ['hello']
138 | }
139 | }
140 | }
141 |
142 | const schemaParams = {
143 | schema: {
144 | params: {
145 | type: 'object',
146 | properties: {
147 | id: {
148 | type: 'string',
149 | description: 'user id'
150 | }
151 | }
152 | }
153 | }
154 | }
155 |
156 | const schemaHeaders = {
157 | schema: {
158 | headers: {
159 | type: 'object',
160 | properties: {
161 | authorization: {
162 | type: 'string',
163 | description: 'api token'
164 | }
165 | },
166 | required: ['authorization']
167 | }
168 | }
169 | }
170 |
171 | const schemaHeadersParams = {
172 | schema: {
173 | headers: {
174 | type: 'object',
175 | properties: {
176 | 'x-api-token': {
177 | type: 'string',
178 | description: 'optional api token'
179 | },
180 | 'x-api-version': {
181 | type: 'string',
182 | description: 'optional api version'
183 | }
184 | }
185 | },
186 | params: {
187 | type: 'object',
188 | properties: {
189 | id: {
190 | type: 'string',
191 | description: 'user id'
192 | }
193 | }
194 | }
195 | }
196 | }
197 |
198 | const schemaSecurity = {
199 | schema: {
200 | security: [
201 | {
202 | apiKey: []
203 | }
204 | ]
205 | }
206 | }
207 |
208 | const schemaConsumes = {
209 | schema: {
210 | consumes: ['application/x-www-form-urlencoded'],
211 | body: {
212 | type: 'object',
213 | properties: {
214 | hello: {
215 | description: 'hello',
216 | type: 'string'
217 | }
218 | },
219 | required: ['hello']
220 | }
221 | }
222 | }
223 |
224 | const schemaProduces = {
225 | schema: {
226 | produces: ['*/*'],
227 | response: {
228 | 200: {
229 | type: 'object',
230 | properties: {
231 | hello: {
232 | description: 'hello',
233 | type: 'string'
234 | }
235 | },
236 | required: ['hello']
237 | }
238 | }
239 | }
240 | }
241 |
242 | const schemaCookies = {
243 | schema: {
244 | cookies: {
245 | type: 'object',
246 | properties: {
247 | bar: { type: 'string' }
248 | }
249 | }
250 | }
251 | }
252 |
253 | const schemaAllOf = {
254 | schema: {
255 | querystring: {
256 | allOf: [
257 | {
258 | type: 'object',
259 | properties: {
260 | foo: { type: 'string' }
261 | }
262 | }
263 | ]
264 | }
265 | }
266 | }
267 |
268 | const schemaExtension = {
269 | schema: {
270 | 'x-tension': true
271 | }
272 | }
273 |
274 | const schemaOperationId = {
275 | schema: {
276 | operationId: 'helloWorld',
277 | response: {
278 | 200: {
279 | type: 'object',
280 | properties: {
281 | hello: {
282 | description: 'hello',
283 | type: 'string'
284 | }
285 | },
286 | required: ['hello']
287 | }
288 | }
289 | }
290 | }
291 |
292 | module.exports = {
293 | openapiOption,
294 | openapiRelativeOptions,
295 | swaggerOption,
296 | schemaQuerystring,
297 | schemaBody,
298 | schemaParams,
299 | schemaHeaders,
300 | schemaHeadersParams,
301 | schemaSecurity,
302 | schemaConsumes,
303 | schemaProduces,
304 | schemaCookies,
305 | schemaAllOf,
306 | schemaExtension,
307 | schemaOperationId
308 | }
309 |
--------------------------------------------------------------------------------
/examples/static-json-file.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const Fastify = require('fastify')
4 |
5 | ; (async () => {
6 | const fastify = Fastify({ logger: true })
7 |
8 | await fastify.register(require('@fastify/swagger'), {
9 | mode: 'static',
10 | specification: {
11 | path: './examples/example-static-specification.json'
12 | }
13 | })
14 |
15 | await fastify.register(require('../index'))
16 |
17 | fastify.listen({ port: 3000 }, (err, addr) => {
18 | if (err) throw err
19 | fastify.log.info(`Visit the documentation at ${addr}/documentation/`)
20 | })
21 | })()
22 |
--------------------------------------------------------------------------------
/examples/static-yaml-file.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const Fastify = require('fastify')
4 |
5 | ; (async () => {
6 | const fastify = Fastify({ logger: true })
7 |
8 | await fastify.register(require('@fastify/swagger'), {
9 | mode: 'static',
10 | specification: {
11 | path: './examples/example-static-specification.yaml'
12 | }
13 | })
14 |
15 | await fastify.register(require('../index'))
16 |
17 | fastify.listen({ port: 3000 }, (err, addr) => {
18 | if (err) throw err
19 | fastify.log.info(`Visit the documentation at ${addr}/documentation/`)
20 | })
21 | })()
22 |
--------------------------------------------------------------------------------
/examples/static/example-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/examples/test-package.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/examples/theme.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const Fastify = require('fastify')
4 |
5 | ; (async () => {
6 | const fastify = Fastify({ logger: true })
7 |
8 | await fastify.register(require('@fastify/swagger'), {
9 | mode: 'static',
10 | specification: {
11 | path: './examples/example-static-specification.json'
12 | }
13 | })
14 | await fastify.register(require('../index'), {
15 | theme: {
16 | js: [
17 | { filename: 'special.js', content: 'alert("loaded test-theme")' }
18 | ],
19 | css: [
20 | { filename: 'theme.css', content: '* {border: 1px red solid;}' }
21 | ],
22 | favicon: [
23 | {
24 | filename: 'favicon.png',
25 | rel: 'icon',
26 | sizes: '16x16',
27 | type: 'image/png',
28 | content: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAA8AAAAQCAQAAABjX+2PAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAACYktHRAD/h4/MvwAAAAd0SU1FB+cCEQ06N8A8CiUAAADnSURBVBjTrdE/K8QBAMbxz/0TLrnUWcTg7ySLewGEwWDRzSYpyULJbGG6wWBTlMEbkHsFNnVloAwXudIlnDru1O9nOCex3rM89TzL0/eh1Ypo//Zk5CdM6JP2IWFOxbmMKZVmPWzbrJSamG5FNXUFx42yV16oqCQUerNr2pghsSgS1sw4kxNVVvbu3rwjSwJ67Kgq2XMjtO/AnWsnVgwQNy6rQ8GkURWBpCebXnR5gA11j5b1OxT4EKq6dGurMWvQqqw2LPoUKDq1LqPzN4q0rCuvckbE/pOakHdhQfwvwKan8Nzad74AkR8/Ir6qAvAAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjMtMDItMTdUMTM6NTg6NTUrMDA6MDBjkr64AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIzLTAyLTE3VDEzOjU4OjU1KzAwOjAwEs8GBAAAACh0RVh0ZGF0ZTp0aW1lc3RhbXAAMjAyMy0wMi0xN1QxMzo1ODo1NSswMDowMEXaJ9sAAAAASUVORK5CYII=', 'base64')
29 | }
30 | ]
31 | }
32 | })
33 |
34 | fastify.listen({ port: 3000 }, (err, addr) => {
35 | if (err) throw err
36 | fastify.log.info(`Visit the documentation at ${addr}/documentation/`)
37 | })
38 | })()
39 |
--------------------------------------------------------------------------------
/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fastify/fastify-swagger-ui/197d6b2dda514b787db3f5628a7f95e4e7f603b5/favicon-16x16.png
--------------------------------------------------------------------------------
/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fastify/fastify-swagger-ui/197d6b2dda514b787db3f5628a7f95e4e7f603b5/favicon-32x32.png
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fsPromises = require('node:fs/promises')
4 | const path = require('node:path')
5 | const fp = require('fastify-plugin')
6 | const csp = require('./static/csp.json')
7 |
8 | async function fastifySwaggerUi (fastify, opts) {
9 | fastify.decorate('swaggerCSP', csp)
10 |
11 | // if no logo is provided, read default static logo
12 | let logoContent = opts.logo
13 | if (logoContent === undefined) {
14 | const bufferLogoContent = await fsPromises.readFile(path.join(__dirname, './static/logo.svg'))
15 | logoContent = { type: 'image/svg+xml', content: bufferLogoContent }
16 | }
17 |
18 | await fastify.register(require('./lib/routes'), {
19 | ...opts,
20 | prefix: opts.routePrefix || '/documentation',
21 | uiConfig: opts.uiConfig || {},
22 | initOAuth: opts.initOAuth || {},
23 | hooks: opts.uiHooks,
24 | theme: opts.theme || {},
25 | logo: logoContent
26 | })
27 | }
28 |
29 | module.exports = fp(fastifySwaggerUi, {
30 | fastify: '5.x',
31 | name: '@fastify/swagger-ui',
32 | dependencies: ['@fastify/swagger']
33 | })
34 | module.exports.default = fastifySwaggerUi
35 | module.exports.fastifySwaggerUi = fastifySwaggerUi
36 |
--------------------------------------------------------------------------------
/lib/index-html.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | function indexHtml (opts) {
4 | let routePrefix = opts.prefix
5 | if (opts.indexPrefix) {
6 | routePrefix = `${opts.indexPrefix.replace(/\/$/, '')}/${opts.prefix.replace(/^\//, '')}`
7 | }
8 | return (url) => {
9 | const hasTrailingSlash = /\/$/.test(url)
10 | const prefix = hasTrailingSlash ? `.${opts.staticPrefix}` : `${routePrefix}${opts.staticPrefix}`
11 | return `
12 |
13 |
14 |
15 |
16 | ${opts.theme?.title || 'Swagger UI'}
17 |
18 |
19 | ${opts.theme && opts.theme.css ? opts.theme.css.map(css => `\n`).join('') : ''}
20 | ${opts.theme && opts.theme.favicon
21 | ? opts.theme.favicon.map(favicon => `\n`).join('')
22 | : `
23 |
24 |
25 | `}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | ${opts.theme && opts.theme.js ? opts.theme.js.map(js => `\n`).join('') : ''}
34 |
35 |
36 | `
37 | }
38 | }
39 |
40 | module.exports = indexHtml
41 |
--------------------------------------------------------------------------------
/lib/routes.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const path = require('node:path')
4 | const yaml = require('yaml')
5 | const fastifyStatic = require('@fastify/static')
6 | const rfdc = require('rfdc')()
7 | const swaggerInitializer = require('./swagger-initializer')
8 | const indexHtml = require('./index-html')
9 |
10 | // URI prefix to separate static assets for swagger UI
11 | const staticPrefix = '/static'
12 |
13 | function fastifySwagger (fastify, opts, done) {
14 | let staticCSP = false
15 | if (opts.staticCSP === true) {
16 | const csp = fastify.swaggerCSP
17 | staticCSP = `default-src 'self'; base-uri 'self'; font-src 'self' https: data:; frame-ancestors 'self'; img-src 'self' data: validator.swagger.io; object-src 'none'; script-src 'self' ${csp.script.join(' ')}; script-src-attr 'none'; style-src 'self' https: ${csp.style.join(' ')}; upgrade-insecure-requests;`
18 | }
19 | if (typeof opts.staticCSP === 'string') {
20 | staticCSP = opts.staticCSP
21 | }
22 | if (typeof opts.staticCSP === 'object' && opts.staticCSP !== null) {
23 | staticCSP = ''
24 | Object.keys(opts.staticCSP).forEach(function (key) {
25 | const value = Array.isArray(opts.staticCSP[key]) ? opts.staticCSP[key].join(' ') : opts.staticCSP[key]
26 | staticCSP += `${key.toLowerCase()} ${value}; `
27 | })
28 | }
29 |
30 | if (typeof staticCSP === 'string' || typeof opts.transformStaticCSP === 'function') {
31 | fastify.addHook('onSend', function (_request, reply, _payload, done) {
32 | // set static csp when it is passed
33 | if (typeof staticCSP === 'string') {
34 | reply.header('content-security-policy', staticCSP.trim())
35 | }
36 | // mutate the header when it is passed
37 | const header = reply.getHeader('content-security-policy')
38 | if (header && typeof opts.transformStaticCSP === 'function') {
39 | reply.header('content-security-policy', opts.transformStaticCSP(header))
40 | }
41 | done()
42 | })
43 | }
44 |
45 | const hooks = Object.create(null)
46 | if (opts.hooks) {
47 | const additionalHooks = [
48 | 'onRequest',
49 | 'preHandler'
50 | ]
51 | for (const hook of additionalHooks) {
52 | hooks[hook] = opts.hooks[hook]
53 | }
54 | }
55 |
56 | if (opts.theme) {
57 | const themePrefix = `${staticPrefix}/theme`
58 | if (opts.theme.css) {
59 | for (const cssFile of opts.theme.css) {
60 | fastify.route({
61 | url: `${themePrefix}/${cssFile.filename}`,
62 | method: 'GET',
63 | schema: { hide: true },
64 | ...hooks,
65 | handler: (_req, reply) => {
66 | reply
67 | .header('content-type', 'text/css; charset=UTF-8')
68 | .send(cssFile.content)
69 | }
70 | })
71 | }
72 | }
73 |
74 | if (opts.theme.js) {
75 | for (const jsFile of opts.theme.js) {
76 | fastify.route({
77 | url: `${themePrefix}/${jsFile.filename}`,
78 | method: 'GET',
79 | schema: { hide: true },
80 | ...hooks,
81 | handler: (_req, reply) => {
82 | reply
83 | .header('content-type', 'application/javascript; charset=utf-8')
84 | .send(jsFile.content)
85 | }
86 | })
87 | }
88 | }
89 |
90 | if (opts.theme.favicon) {
91 | for (const favicon of opts.theme.favicon) {
92 | fastify.route({
93 | url: `${themePrefix}/${favicon.filename}`,
94 | method: 'GET',
95 | schema: { hide: true },
96 | ...hooks,
97 | handler: (_req, reply) => {
98 | reply
99 | .header('content-type', favicon.type)
100 | .send(favicon.content)
101 | }
102 | })
103 | }
104 | }
105 | }
106 |
107 | const indexHtmlContent = indexHtml({ ...opts, staticPrefix })
108 |
109 | fastify.route({
110 | url: '/',
111 | method: 'GET',
112 | schema: { hide: true },
113 | ...hooks,
114 | handler: (req, reply) => {
115 | reply
116 | .header('content-type', 'text/html; charset=utf-8')
117 | .send(indexHtmlContent(req.url)) // trailing slash alters the relative urls generated in the html
118 | }
119 | })
120 |
121 | fastify.route({
122 | url: `${staticPrefix}/index.html`,
123 | method: 'GET',
124 | schema: { hide: true },
125 | ...hooks,
126 | handler: (req, reply) => {
127 | reply.redirect(req.url.replace(/\/static\/index\.html$/, '/'))
128 | }
129 | })
130 |
131 | const swaggerInitializerContent = swaggerInitializer(opts)
132 |
133 | fastify.route({
134 | url: `${staticPrefix}/swagger-initializer.js`,
135 | method: 'GET',
136 | schema: { hide: true },
137 | ...hooks,
138 | handler: (_req, reply) => {
139 | reply
140 | .header('content-type', 'application/javascript; charset=utf-8')
141 | .send(swaggerInitializerContent)
142 | }
143 | })
144 |
145 | const hasTransformSpecificationFn = typeof opts.transformSpecification === 'function'
146 | const shouldCloneSwaggerObject = opts.transformSpecificationClone ?? true
147 | const transformSpecification = opts.transformSpecification
148 | fastify.route({
149 | url: '/json',
150 | method: 'GET',
151 | schema: { hide: true },
152 | ...hooks,
153 | handler: hasTransformSpecificationFn
154 | ? shouldCloneSwaggerObject
155 | ? function (req, reply) {
156 | reply.send(transformSpecification(rfdc(fastify.swagger()), req, reply))
157 | }
158 | : function (req, reply) {
159 | reply.send(transformSpecification(fastify.swagger(), req, reply))
160 | }
161 | : function (_req, reply) {
162 | reply.send(fastify.swagger())
163 | }
164 | })
165 |
166 | fastify.route({
167 | url: '/yaml',
168 | method: 'GET',
169 | schema: { hide: true },
170 | ...hooks,
171 | handler: hasTransformSpecificationFn
172 | ? shouldCloneSwaggerObject
173 | ? function (req, reply) {
174 | reply
175 | .type('application/x-yaml')
176 | .send(yaml.stringify(transformSpecification(rfdc(fastify.swagger()), req, reply)))
177 | }
178 | : function (req, reply) {
179 | reply
180 | .type('application/x-yaml')
181 | .send(yaml.stringify(transformSpecification(fastify.swagger(), req, reply)))
182 | }
183 | : function (_req, reply) {
184 | reply
185 | .type('application/x-yaml')
186 | .send(fastify.swagger({ yaml: true }))
187 | }
188 | })
189 |
190 | // serve swagger-ui with the help of @fastify/static
191 | fastify.register(fastifyStatic, {
192 | root: opts.baseDir || path.join(__dirname, '..', 'static'),
193 | prefix: staticPrefix,
194 | decorateReply: false
195 | })
196 |
197 | if (opts.baseDir) {
198 | fastify.register(fastifyStatic, {
199 | root: opts.baseDir,
200 | serve: false
201 | })
202 |
203 | // Handler for external documentation files passed via $ref
204 | fastify.route({
205 | url: '/*',
206 | method: 'GET',
207 | schema: { hide: true },
208 | ...hooks,
209 | handler: function (req, reply) {
210 | const file = req.params['*']
211 | reply.sendFile(file)
212 | }
213 | })
214 | }
215 |
216 | done()
217 | }
218 |
219 | module.exports = fastifySwagger
220 |
--------------------------------------------------------------------------------
/lib/serialize.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | function serialize (value) {
4 | switch (typeof value) {
5 | case 'bigint':
6 | return value.toString() + 'n'
7 | case 'boolean':
8 | return value ? 'true' : 'false'
9 | case 'function':
10 | return value.toString()
11 | case 'number':
12 | return '' + value
13 | case 'object':
14 | if (value === null) {
15 | return 'null'
16 | } else if (Array.isArray(value)) {
17 | return serializeArray(value)
18 | } else if (value instanceof RegExp) {
19 | return `/${value.source}/${value.flags}`
20 | } else if (value instanceof Date) {
21 | return `new Date(${value.getTime()})`
22 | } else if (value instanceof Set) {
23 | return `new Set(${serializeArray(Array.from(value))})`
24 | } else if (value instanceof Map) {
25 | return `new Map(${serializeArray(Array.from(value))})`
26 | } else {
27 | return serializeObject(value)
28 | }
29 | case 'string':
30 | return JSON.stringify(value)
31 | case 'symbol':
32 | return serializeSymbol(value)
33 | case 'undefined':
34 | return 'undefined'
35 | }
36 | }
37 | const symbolRE = /Symbol\((.+)\)/
38 | function serializeSymbol (value) {
39 | return symbolRE.test(value.toString())
40 | ? `Symbol("${value.toString().match(symbolRE)[1]}")`
41 | : 'Symbol()'
42 | }
43 |
44 | function serializeArray (value) {
45 | let result = '['
46 | const il = value.length
47 | const last = il - 1
48 | for (let i = 0; i < il; ++i) {
49 | result += serialize(value[i])
50 | i !== last && (result += ',')
51 | }
52 | return result + ']'
53 | }
54 |
55 | function serializeObject (value) {
56 | let result = '{'
57 | const keys = Object.keys(value)
58 | let i = 0
59 | const il = keys.length
60 | const last = il - 1
61 | for (; i < il; ++i) {
62 | const key = keys[i]
63 | result += `"${key}":${serialize(value[key])}`
64 | i !== last && (result += ',')
65 | }
66 | return result + '}'
67 | }
68 |
69 | module.exports = serialize
70 |
--------------------------------------------------------------------------------
/lib/swagger-initializer.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const serialize = require('./serialize')
4 |
5 | function swaggerInitializer (opts) {
6 | const hasLogo = opts.logo && opts.logo.content !== undefined
7 | const logoBase64 = hasLogo && Buffer.from(opts.logo.content).toString('base64')
8 | const logoData = hasLogo && `data:${opts.logo.type};base64,${logoBase64}`
9 | const logoHref = hasLogo && opts.logo.href
10 | const logoTarget = hasLogo && opts.logo.target
11 |
12 | return `window.onload = function () {
13 | function waitForElement(selector) {
14 | return new Promise(resolve => {
15 | if (document.querySelector(selector)) {
16 | return resolve(document.querySelector(selector));
17 | }
18 |
19 | const observer = new MutationObserver(mutations => {
20 | if (document.querySelector(selector)) {
21 | observer.disconnect();
22 | resolve(document.querySelector(selector));
23 | }
24 | });
25 |
26 | // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
27 | observer.observe(document.body, {
28 | childList: true,
29 | subtree: true
30 | });
31 | });
32 | }
33 | function resolveUrl(url) {
34 | let currentHref = window.location.href;
35 | currentHref = currentHref.split('#', 1)[0];
36 | currentHref = currentHref.endsWith('/') ? currentHref : currentHref + '/';
37 | const anchor = document.createElement('a');
38 | anchor.href = currentHref + url;
39 | return anchor.href
40 | }
41 |
42 | const config = ${serialize(opts.uiConfig)}
43 | const resConfig = Object.assign({}, {
44 | dom_id: '#swagger-ui',
45 | deepLinking: true,
46 | presets: [
47 | SwaggerUIBundle.presets.apis,
48 | SwaggerUIStandalonePreset
49 | ],
50 | plugins: [
51 | SwaggerUIBundle.plugins.DownloadUrl
52 | ],
53 | layout: "StandaloneLayout",
54 | validatorUrl: ${serialize(opts.validatorUrl || null)},
55 | }, config, {
56 | url: resolveUrl('./json'),
57 | oauth2RedirectUrl: resolveUrl('./static/oauth2-redirect.html')
58 | });
59 |
60 | const ui = SwaggerUIBundle(resConfig)
61 |
62 | ${logoData
63 | ? `
64 | if (resConfig.layout === 'StandaloneLayout') {
65 | // Replace the logo
66 | waitForElement('#swagger-ui > section > div.topbar > div > div > a').then((link) => {
67 | const img = document.createElement('img')
68 | img.height = 40
69 | img.src = '${logoData}'
70 | ${logoHref ? `img.href = '${logoHref}'` : 'img.href = resolveUrl(\'/\')'}
71 | ${logoTarget ? `img.target = '${logoTarget}'` : ''}
72 | link.innerHTML = ''
73 | link.appendChild(img)
74 | })
75 | }`
76 | : ''}
77 |
78 | ui.initOAuth(${serialize(opts.initOAuth)})
79 | }`
80 | }
81 |
82 | module.exports = swaggerInitializer
83 |
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
44 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@fastify/swagger-ui",
3 | "version": "5.2.3",
4 | "description": "Serve Swagger-ui for Fastify",
5 | "main": "index.js",
6 | "type": "commonjs",
7 | "types": "types/index.d.ts",
8 | "scripts": {
9 | "lint": "eslint",
10 | "lint:fix": "eslint --fix",
11 | "prepare": "node scripts/prepare-swagger-ui",
12 | "prepublishOnly": "npm run prepare",
13 | "test": "npm run prepare && npm run unit && npm run typescript",
14 | "test:dev": "npm run lint && npm run unit && npm run typescript",
15 | "test:e2e:command": "node ./examples/example-e2e.js",
16 | "test:e2e": "npx playwright test",
17 | "test:e2e:ui": "npx playwright test --ui",
18 | "typescript": "tsd",
19 | "unit": "c8 --100 node --test"
20 | },
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/fastify/fastify-swagger-ui.git"
24 | },
25 | "keywords": [
26 | "fastify",
27 | "swagger",
28 | "openapi",
29 | "swagger-ui",
30 | "serve",
31 | "static"
32 | ],
33 | "author": "Aras Abbasi ",
34 | "contributors": [
35 | {
36 | "name": "Matteo Collina",
37 | "email": "hello@matteocollina.com"
38 | },
39 | {
40 | "name": "Manuel Spigolon",
41 | "email": "behemoth89@gmail.com"
42 | },
43 | {
44 | "name": "KaKa Ng",
45 | "email": "kaka@kakang.dev",
46 | "url": "https://github.com/climba03003"
47 | },
48 | {
49 | "name": "Frazer Smith",
50 | "email": "frazer.dev@icloud.com",
51 | "url": "https://github.com/fdawgs"
52 | }
53 | ],
54 | "license": "MIT",
55 | "bugs": {
56 | "url": "https://github.com/fastify/fastify-swagger-ui/issues"
57 | },
58 | "homepage": "https://github.com/fastify/fastify-swagger-ui#readme",
59 | "funding": [
60 | {
61 | "type": "github",
62 | "url": "https://github.com/sponsors/fastify"
63 | },
64 | {
65 | "type": "opencollective",
66 | "url": "https://opencollective.com/fastify"
67 | }
68 | ],
69 | "devDependencies": {
70 | "@apidevtools/swagger-parser": "^10.1.0",
71 | "@fastify/basic-auth": "^6.0.0",
72 | "@fastify/helmet": "^13.0.0",
73 | "@fastify/pre-commit": "^2.1.0",
74 | "@fastify/swagger": "^9.0.0",
75 | "@playwright/test": "^1.43.1",
76 | "@types/node": "^22.0.0",
77 | "ajv": "^8.12.0",
78 | "c8": "^10.1.2",
79 | "eslint": "^9.17.0",
80 | "fastify": "^5.0.0",
81 | "fs-extra": "^11.2.0",
82 | "neostandard": "^0.12.0",
83 | "qs": "^6.12.1",
84 | "swagger-ui-dist": "5.21.0",
85 | "tsd": "^0.32.0"
86 | },
87 | "dependencies": {
88 | "@fastify/static": "^8.0.0",
89 | "fastify-plugin": "^5.0.0",
90 | "openapi-types": "^12.1.3",
91 | "rfdc": "^1.3.1",
92 | "yaml": "^2.4.1"
93 | },
94 | "tsd": {
95 | "directory": "types"
96 | },
97 | "pkg": {
98 | "assets": [
99 | "static/**/*"
100 | ]
101 | },
102 | "publishConfig": {
103 | "access": "public"
104 | },
105 | "pre-commit": [
106 | "lint",
107 | "test"
108 | ]
109 | }
110 |
--------------------------------------------------------------------------------
/playwright.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { defineConfig, devices } = require('@playwright/test')
4 |
5 | const PORT = 3000
6 |
7 | /**
8 | * @see https://playwright.dev/docs/test-configuration
9 | */
10 | module.exports = defineConfig({
11 | testDir: './e2e',
12 | fullyParallel: true,
13 | forbidOnly: !!process.env.CI,
14 | retries: process.env.CI ? 2 : 0,
15 | workers: process.env.CI ? 1 : undefined,
16 | reporter: 'html',
17 | use: {
18 | baseURL: `http://127.0.0.1:${PORT}/documentation`,
19 | trace: 'on-first-retry'
20 | },
21 | projects: [
22 | {
23 | name: 'chromium',
24 | use: { ...devices['Desktop Chrome'] }
25 | }
26 | ],
27 | webServer: {
28 | command: `PORT=${PORT} npm run test:e2e:command`,
29 | url: `http://127.0.0.1:${PORT}/documentation`,
30 | reuseExistingServer: !process.env.CI
31 | }
32 | })
33 |
--------------------------------------------------------------------------------
/scripts/prepare-swagger-ui.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fs = require('node:fs')
4 | const fse = require('fs-extra')
5 | const crypto = require('node:crypto')
6 | const swaggerUiAssetPath = require('swagger-ui-dist').getAbsoluteFSPath()
7 | const resolve = require('node:path').resolve
8 |
9 | const folderName = 'static'
10 |
11 | fse.emptyDirSync(resolve(`./${folderName}`))
12 |
13 | // since the original swagger-ui-dist folder contains non UI files
14 | const filesToCopy = [
15 | 'index.html',
16 | 'index.css',
17 | 'oauth2-redirect.html',
18 | 'swagger-ui-bundle.js',
19 | 'swagger-ui-standalone-preset.js',
20 | 'swagger-ui.css',
21 | 'swagger-ui.js'
22 | ]
23 | filesToCopy.forEach(filename => {
24 | fse.ensureFileSync(resolve(`./static/${filename}`))
25 | const readableStream = fs.createReadStream(`${swaggerUiAssetPath}/${filename}`, 'utf8')
26 | const writableStream = fs.createWriteStream(resolve(`./static/${filename}`))
27 | // Matches sourceMappingURL comments in .js and .css files
28 | const sourceMapRegex = new RegExp(String.raw`\/.# sourceMappingURL=${filename}.map(\*\/)?$`)
29 |
30 | readableStream.on('data', (chunk) => {
31 | // Copy file while removing sourceMappingURL comments
32 | writableStream.write(chunk.replace(sourceMapRegex, ''))
33 | })
34 | })
35 |
36 | const overrides = [
37 | 'favicon-16x16.png',
38 | 'favicon-32x32.png',
39 | 'logo.svg'
40 | ]
41 | overrides.forEach(filename => {
42 | fse.copySync(`./${filename}`, resolve(`./static/${filename}`))
43 | })
44 |
45 | const sha = {
46 | script: [],
47 | style: []
48 | }
49 | function computeCSPHashes (path) {
50 | const scriptRegex = /