diff --git a/README.md b/README.md index aac0209..087a0e6 100644 --- a/README.md +++ b/README.md @@ -112,19 +112,20 @@ await fastify.ready() #### Options - | Option | Default | Description | - | ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------- | - | 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. | - | initOAuth | {} | Configuration options for [Swagger UI initOAuth](https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/). | - | routePrefix | '/documentation' | Overwrite the default Swagger UI route prefix. | - | staticCSP | false | Enable CSP header for static resources. | - | transformStaticCSP | undefined | Synchronous function to transform CSP header for static resources if the header has been previously set. | - | transformSpecification | undefined | Synchronous function to transform the swagger document. | - | transformSpecificationClone| true | Provide a deepcloned swaggerObject to transformSpecification | - | uiConfig | {} | Configuration options for [Swagger UI](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md). | - | 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.| - | theme | {} | Add custom JavaScript and CSS to the Swagger UI web page | - | logLevel | info | Allow to define route log level. | + | Option | Default | Description | + | ------------------ | --------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + | 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. | + | initOAuth | {} | Configuration options for [Swagger UI initOAuth](https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/). | + | routePrefix | '/documentation' | Overwrite the default Swagger UI route prefix. | + | indexPrefix | '' | Add an additional prefix. This is for when the Fastify server is behind path based routing. ex. NGINX | + | staticCSP | false | Enable CSP header for static resources. | + | transformStaticCSP | undefined | Synchronous function to transform CSP header for static resources if the header has been previously set. | + | transformSpecification | undefined | Synchronous function to transform the swagger document. | + | transformSpecificationClone| true | Provide a deepcloned swaggerObject to transformSpecification | + | uiConfig | {} | Configuration options for [Swagger UI](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md). | + | 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. | + | theme | {} | Add custom JavaScript and CSS to the Swagger UI web page | + | logLevel | info | Allow to define route log level. | The plugin will expose the documentation with the following APIs: diff --git a/lib/index-html.js b/lib/index-html.js index 5484a76..baafc29 100644 --- a/lib/index-html.js +++ b/lib/index-html.js @@ -1,10 +1,13 @@ 'use strict' function indexHtml (opts) { - const hasLeadingSlash = /^\//.test(opts.prefix) + let routePrefix = opts.prefix + if (opts.indexPrefix) { + routePrefix = `${opts.indexPrefix.replace(/\/$/, '')}/${opts.prefix.replace(/^\//, '')}` + } return (url) => { const hasTrailingSlash = /\/$/.test(url) - const prefix = hasTrailingSlash ? `.${opts.staticPrefix}` : `${hasLeadingSlash ? '.' : ''}${opts.prefix}${opts.staticPrefix}` + const prefix = hasTrailingSlash ? `.${opts.staticPrefix}` : `${routePrefix}${opts.staticPrefix}` return ` diff --git a/test/prepare.test.js b/test/prepare.test.js index 7af5eea..c4ed355 100644 --- a/test/prepare.test.js +++ b/test/prepare.test.js @@ -18,7 +18,7 @@ test('Swagger source does not contain sourceMaps', async (t) => { const includesSourceMap = res.payload.includes('sourceMappingURL') t.assert.deepStrictEqual(includesSourceMap, false) - t.assert.deepStrictEqual(res.headers['content-type'], 'application/javascript; charset=UTF-8') + t.assert.deepStrictEqual(res.headers['content-type'], 'application/javascript; charset=utf-8') }) test('Swagger css does not contain sourceMaps', async (t) => { @@ -34,5 +34,5 @@ test('Swagger css does not contain sourceMaps', async (t) => { const includesSourceMap = res.payload.includes('sourceMappingURL') t.assert.deepStrictEqual(includesSourceMap, false) - t.assert.deepStrictEqual(res.headers['content-type'], 'text/css; charset=UTF-8') + t.assert.deepStrictEqual(res.headers['content-type'], 'text/css; charset=utf-8') }) diff --git a/test/route.test.js b/test/route.test.js index d3efbc8..69d4b1f 100644 --- a/test/route.test.js +++ b/test/route.test.js @@ -1,7 +1,7 @@ 'use strict' -const t = require('node:test') -const test = t.test +const nodeTest = require('node:test') +const test = nodeTest.test const Fastify = require('fastify') const Swagger = require('@apidevtools/swagger-parser') const yaml = require('yaml') @@ -16,6 +16,7 @@ const { } = require('../examples/options') const resolve = require('node:path').resolve +const join = require('node:path').join const readFileSync = require('node:fs').readFileSync const schemaParamsWithoutDesc = { @@ -294,7 +295,7 @@ test('/documentation/static/:file should send back the correct file', async (t) url: '/documentation/static/' }) t.assert.deepStrictEqual(typeof res.payload, 'string') - t.assert.deepStrictEqual(res.headers['content-type'], 'text/html; charset=UTF-8') + t.assert.deepStrictEqual(res.headers['content-type'], 'text/html; charset=utf-8') t.assert.deepStrictEqual( readFileSync( resolve(__dirname, '..', 'static', 'index.html'), @@ -321,7 +322,7 @@ test('/documentation/static/:file should send back the correct file', async (t) url: '/documentation/static/oauth2-redirect.html' }) t.assert.deepStrictEqual(typeof res.payload, 'string') - t.assert.deepStrictEqual(res.headers['content-type'], 'text/html; charset=UTF-8') + t.assert.deepStrictEqual(res.headers['content-type'], 'text/html; charset=utf-8') t.assert.deepStrictEqual( readFileSync( resolve(__dirname, '..', 'static', 'oauth2-redirect.html'), @@ -337,7 +338,7 @@ test('/documentation/static/:file should send back the correct file', async (t) url: '/documentation/static/swagger-ui.css' }) t.assert.deepStrictEqual(typeof res.payload, 'string') - t.assert.deepStrictEqual(res.headers['content-type'], 'text/css; charset=UTF-8') + t.assert.deepStrictEqual(res.headers['content-type'], 'text/css; charset=utf-8') t.assert.deepStrictEqual( readFileSync( resolve(__dirname, '..', 'static', 'swagger-ui.css'), @@ -353,7 +354,7 @@ test('/documentation/static/:file should send back the correct file', async (t) url: '/documentation/static/swagger-ui-bundle.js' }) t.assert.deepStrictEqual(typeof res.payload, 'string') - t.assert.deepStrictEqual(res.headers['content-type'], 'application/javascript; charset=UTF-8') + t.assert.deepStrictEqual(res.headers['content-type'], 'application/javascript; charset=utf-8') t.assert.deepStrictEqual( readFileSync( resolve(__dirname, '..', 'static', 'swagger-ui-bundle.js'), @@ -369,7 +370,7 @@ test('/documentation/static/:file should send back the correct file', async (t) url: '/documentation/static/swagger-ui-standalone-preset.js' }) t.assert.deepStrictEqual(typeof res.payload, 'string') - t.assert.deepStrictEqual(res.headers['content-type'], 'application/javascript; charset=UTF-8') + t.assert.deepStrictEqual(res.headers['content-type'], 'application/javascript; charset=utf-8') t.assert.deepStrictEqual( readFileSync( resolve(__dirname, '..', 'static', 'swagger-ui-standalone-preset.js'), @@ -538,8 +539,35 @@ test('should return empty log level of route /documentation', async (t) => { t.assert.deepStrictEqual(res.headers['content-type'], 'text/html; charset=utf-8') }) +const assertIndexUrls = (t, indexHtml, prefix) => { + t.assert.deepStrictEqual(indexHtml.includes(`href="${prefix}/static/index.css"`), true) + t.assert.deepStrictEqual(indexHtml.includes(`src="${prefix}/static/theme/theme-js.js"`), true) + t.assert.deepStrictEqual(indexHtml.includes(`href="${prefix}/index.css"`), false) + t.assert.deepStrictEqual(indexHtml.includes(`src="${prefix}/theme/theme-js.js"`), false) +} + +const validateIndexUrls = async (t, fastify, indexHtml, prefix = '') => { + const hrefs = indexHtml.matchAll(/href="([^"]*)"/g) + for (const [, path] of hrefs) { + const res = await fastify.inject({ + method: 'GET', + url: join(prefix, path) + }) + + t.assert.equal(res.statusCode, 200) + } + const srcs = indexHtml.matchAll(/src="([^"]*)"/g) + for (const [, path] of srcs) { + const res = await fastify.inject({ + method: 'GET', + url: join(prefix, path) + }) + t.assert.equal(res.statusCode, 200) + } +} + test('/documentation should display index html with correct asset urls', async (t) => { - t.plan(6) + t.plan(13) const fastify = Fastify() await fastify.register(fastifySwagger, swaggerOption) await fastify.register(fastifySwaggerUi, { theme: { js: [{ filename: 'theme-js.js' }] } }) @@ -548,59 +576,66 @@ test('/documentation should display index html with correct asset urls', async ( method: 'GET', url: '/documentation' }) + t.assert.equal(res.statusCode, 200) - t.assert.deepStrictEqual(res.payload.includes('href="./documentation/static/index.css"'), true) - t.assert.deepStrictEqual(res.payload.includes('src="./documentation/static/theme/theme-js.js"'), true) - t.assert.deepStrictEqual(res.payload.includes('href="./documentation/index.css"'), false) - t.assert.deepStrictEqual(res.payload.includes('src="./documentation/theme/theme-js.js"'), false) + assertIndexUrls(t, res.payload, '/documentation') + await validateIndexUrls(t, fastify, res.payload) +}) - let cssRes = await fastify.inject({ - method: 'GET', - url: '/documentation/static/index.css' - }) - t.assert.equal(cssRes.statusCode, 200) - cssRes = await fastify.inject({ - method: 'GET', - url: './documentation/static/index.css' +/** + * This emulates when the server is inside an NGINX application that routes by path + */ +const testCases = [ + ['/swagger-app', undefined], + ['/swagger-app/', undefined], + ['/swagger-app', 'documentation'] +] +testCases.forEach(([prefix, pluginPrefix]) => { + test(`${prefix} ${pluginPrefix} should display index html with correct asset urls when nested`, async (t) => { + t.plan(13) + const fastify = Fastify() + await fastify.register( + async (childFastify) => { + await childFastify.register(fastifySwagger, swaggerOption) + await childFastify.register(fastifySwaggerUi, { indexPrefix: prefix, routePrefix: pluginPrefix, theme: { js: [{ filename: 'theme-js.js' }] } }) + }, + { + prefix: '/swagger-app' + } + ) + + const res = await fastify.inject({ + method: 'GET', + url: '/swagger-app/documentation' + }) + t.assert.equal(res.statusCode, 200) + + assertIndexUrls(t, res.payload, '/swagger-app/documentation') + + await validateIndexUrls(t, fastify, res.payload) }) - t.assert.equal(cssRes.statusCode, 200) }) /** * This emulates when the server is inside an NGINX application that routes by path */ -test('/documentation should display index html with correct asset urls when nested', async (t) => { - t.plan(5) +test('/api/v1/docs should display index html with correct asset urls', async (t) => { + t.plan(13) const fastify = Fastify() - await fastify.register( - async () => { - await fastify.register(fastifySwagger, swaggerOption) - await fastify.register(fastifySwaggerUi, { theme: { js: [{ filename: 'theme-js.js' }] } }) - }, - { - prefix: '/swagger-app' - } - ) + await fastify.register(fastifySwagger, swaggerOption) + await fastify.register(fastifySwaggerUi, { prefix: '/api/v1/docs', theme: { js: [{ filename: 'theme-js.js' }] } }) const res = await fastify.inject({ method: 'GET', - url: '/swagger-app/documentation' + url: '/api/v1/docs' }) - - t.assert.deepStrictEqual(res.payload.includes('href="./documentation/static/index.css"'), true) - t.assert.deepStrictEqual(res.payload.includes('src="./documentation/static/theme/theme-js.js"'), true) - t.assert.deepStrictEqual(res.payload.includes('href="./documentation/index.css"'), false) - t.assert.deepStrictEqual(res.payload.includes('src="./documentation/theme/theme-js.js"'), false) - - const cssRes = await fastify.inject({ - method: 'GET', - url: '/swagger-app/documentation/static/index.css' - }) - t.assert.equal(cssRes.statusCode, 200) + t.assert.equal(res.statusCode, 200) + assertIndexUrls(t, res.payload, '/api/v1/docs') + await validateIndexUrls(t, fastify, res.payload) }) test('/documentation/ should display index html with correct asset urls', async (t) => { - t.plan(4) + t.plan(13) const fastify = Fastify() await fastify.register(fastifySwagger, swaggerOption) await fastify.register(fastifySwaggerUi, { theme: { js: [{ filename: 'theme-js.js' }] } }) @@ -609,15 +644,14 @@ test('/documentation/ should display index html with correct asset urls', async method: 'GET', url: '/documentation/' }) + t.assert.equal(res.statusCode, 200) - t.assert.strictEqual(res.payload.includes('href="./static/index.css"'), true) - t.assert.strictEqual(res.payload.includes('src="./static/theme/theme-js.js"'), true) - t.assert.strictEqual(res.payload.includes('href="./index.css"'), false) - t.assert.strictEqual(res.payload.includes('src="./theme/theme-js.js"'), false) + assertIndexUrls(t, res.payload, '.') + await validateIndexUrls(t, fastify, res.payload, '/documentation/') }) test('/docs should display index html with correct asset urls when documentation prefix is set', async (t) => { - t.plan(4) + t.plan(13) const fastify = Fastify() await fastify.register(fastifySwagger, swaggerOption) await fastify.register(fastifySwaggerUi, { theme: { js: [{ filename: 'theme-js.js' }] }, routePrefix: '/docs' }) @@ -626,11 +660,10 @@ test('/docs should display index html with correct asset urls when documentation method: 'GET', url: '/docs' }) + t.assert.equal(res.statusCode, 200) - t.assert.strictEqual(res.payload.includes('href="./docs/static/index.css"'), true) - t.assert.strictEqual(res.payload.includes('src="./docs/static/theme/theme-js.js"'), true) - t.assert.strictEqual(res.payload.includes('href="./docs/index.css"'), false) - t.assert.strictEqual(res.payload.includes('src="./docs/theme/theme-js.js"'), false) + assertIndexUrls(t, res.payload, '/docs') + await validateIndexUrls(t, fastify, res.payload) }) test('/docs should display index html with correct asset urls when documentation prefix is set with no leading slash', async (t) => { @@ -668,7 +701,7 @@ test('/docs/ should display index html with correct asset urls when documentatio }) test('/documentation/ should display index html with correct asset urls', async (t) => { - t.plan(4) + t.plan(13) const fastify = Fastify() await fastify.register(fastifySwagger, swaggerOption) await fastify.register(fastifySwaggerUi, { theme: { js: [{ filename: 'theme-js.js' }] } }) @@ -677,15 +710,15 @@ test('/documentation/ should display index html with correct asset urls', async method: 'GET', url: '/documentation/' }) + t.assert.equal(res.statusCode, 200) - t.assert.strictEqual(res.payload.includes('href="./static/index.css"'), true) - t.assert.strictEqual(res.payload.includes('src="./static/theme/theme-js.js"'), true) - t.assert.strictEqual(res.payload.includes('href="./index.css"'), false) - t.assert.strictEqual(res.payload.includes('src="./theme/theme-js.js"'), false) + assertIndexUrls(t, res.payload, '.') + + await validateIndexUrls(t, fastify, res.payload, '/documentation') }) test('/docs should display index html with correct asset urls when documentation prefix is set', async (t) => { - t.plan(4) + t.plan(13) const fastify = Fastify() await fastify.register(fastifySwagger, swaggerOption) await fastify.register(fastifySwaggerUi, { theme: { js: [{ filename: 'theme-js.js' }] }, routePrefix: '/docs' }) @@ -694,11 +727,11 @@ test('/docs should display index html with correct asset urls when documentation method: 'GET', url: '/docs' }) + t.assert.equal(res.statusCode, 200) + + assertIndexUrls(t, res.payload, '/docs') - t.assert.strictEqual(res.payload.includes('href="./docs/static/index.css"'), true) - t.assert.strictEqual(res.payload.includes('src="./docs/static/theme/theme-js.js"'), true) - t.assert.strictEqual(res.payload.includes('href="./docs/index.css"'), false) - t.assert.strictEqual(res.payload.includes('src="./docs/theme/theme-js.js"'), false) + await validateIndexUrls(t, fastify, res.payload) }) test('/docs/ should display index html with correct asset urls when documentation prefix is set', async (t) => { diff --git a/types/index.d.ts b/types/index.d.ts index a3720ad..dd47358 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -45,6 +45,10 @@ declare namespace fastifySwaggerUi { * @default /documentation */ routePrefix?: string; + /** + * Add an index prefix. This is for when the Fastify server is behind path based routing. ex. NGINX + */ + indexPrefix?: string; /** * Make it explicit that this plugin overrides the prefix value */ diff --git a/types/types.test-d.ts b/types/types.test-d.ts index b9c11fd..e111889 100644 --- a/types/types.test-d.ts +++ b/types/types.test-d.ts @@ -29,10 +29,12 @@ app.register(fastifySwaggerUi); app.register(fastifySwaggerUi, {}); app.register(fastifySwaggerUi, { routePrefix: '/documentation', + indexPrefix: '/custom-prefix' }); const fastifySwaggerOptions: FastifySwaggerUiOptions = { routePrefix: '/documentation', + indexPrefix: '/custom-prefix' } app.register(fastifySwaggerUi, fastifySwaggerOptions); @@ -91,6 +93,7 @@ app.get('/public/route', { app .register(fastifySwaggerUi, { routePrefix: '/documentation', + indexPrefix: '/custom-prefix' }) app