From 97a7fef8c89f8724bf90226cde908d44e014ed06 Mon Sep 17 00:00:00 2001 From: Tiago Gimenes Date: Wed, 2 Jun 2021 18:30:31 -0300 Subject: [PATCH] Chore | Improve gatsby-plugin-nginx configurability (#747) --- packages/gatsby-plugin-nginx/README.md | 32 ++++++++++++- .../gatsby-plugin-nginx/src/gatsby-node.ts | 1 + .../src/nginx-generator.ts | 47 ++++++++++++++++--- .../gatsby-plugin-nginx/src/pluginOptions.ts | 4 ++ .../src/typings/types.d.ts | 21 +++++++++ .../test/nginx-generator.test.ts | 31 +++++++++--- 6 files changed, 122 insertions(+), 14 deletions(-) diff --git a/packages/gatsby-plugin-nginx/README.md b/packages/gatsby-plugin-nginx/README.md index db2cbedb84..cea1077733 100644 --- a/packages/gatsby-plugin-nginx/README.md +++ b/packages/gatsby-plugin-nginx/README.md @@ -45,7 +45,7 @@ module.exports = { plugins: [ // [...] { - resolve: require.resolve('@vtex/gatsby-plugin-nginx'), + resolve: '@vtex/gatsby-plugin-nginx', options: { transformHeaders: (headers, path) => { const DEFAULT_SECURITY_HEADERS = [ @@ -180,3 +180,33 @@ This location finally handles pages with mismatching *path* and *matchPath*. `createRedirect` with relative paths are not yet implemented. +### Adding custom blocks to nginx config +Our default nginx config may not be suited for all use cases. For those use cases where you need to enable/disable some extra flags in the `server` and `http` block you can use the `serverOptions` and `httpOptions` params respectively. + +For instance, say we don't want to use Google's dns server, but use the AWS one instead. One could configure the plugin like: +```js +// gatsby-config.js +module.exports = { + // [...] + plugins: [ + // [...] + { + resolve: '@vtex/gatsby-plugin-nginx', + options: { + // other options + serverOptions: [['resolver', '169.254.169.253']], + } + }, + ], +} +``` + +This will create an `nginx.conf` file similar to: +``` +... + server { + resolver 169.254.169.253; + ... + } +... +``` diff --git a/packages/gatsby-plugin-nginx/src/gatsby-node.ts b/packages/gatsby-plugin-nginx/src/gatsby-node.ts index ecdb567321..9447cf7436 100644 --- a/packages/gatsby-plugin-nginx/src/gatsby-node.ts +++ b/packages/gatsby-plugin-nginx/src/gatsby-node.ts @@ -75,6 +75,7 @@ export const onPostBuild: GatsbyNode['onPostBuild'] = async ( const options = pluginOptions(opt) const timer = reporter.activityTimer(`write out nginx configuration`) + timer.start() const { program, pages: pagesMap, redirects } = store.getState() as { diff --git a/packages/gatsby-plugin-nginx/src/nginx-generator.ts b/packages/gatsby-plugin-nginx/src/nginx-generator.ts index 03dbb8fe7c..d00b6746f1 100644 --- a/packages/gatsby-plugin-nginx/src/nginx-generator.ts +++ b/packages/gatsby-plugin-nginx/src/nginx-generator.ts @@ -26,6 +26,13 @@ function generateNginxConfiguration({ files: string[] options: PluginOptions }): string { + const { + disableBrotliEncoding, + writeOnlyLocations, + serverOptions, + httpOptions, + } = options + const filesSet = new Set(files) const locations = [ ...Object.entries(headersMap) @@ -40,7 +47,7 @@ function generateNginxConfiguration({ ...generateRewrites(rewrites), ] - const brotliConf = options.disableBrotliEncoding + const brotliConf = disableBrotliEncoding ? [] : [ { cmd: ['brotli', 'on'] }, @@ -74,7 +81,7 @@ function generateNginxConfiguration({ }, ] - const conf = options.writeOnlyLocations + const conf = writeOnlyLocations ? locations : [ { cmd: ['worker_processes', '3'] }, @@ -88,6 +95,7 @@ function generateNginxConfiguration({ { cmd: ['http'], children: [ + ...httpOptions.map((cmd) => ({ cmd })), // $use_url_tmp = $host OR $http_origin { cmd: ['map', '$host', '$use_url_tmp'], @@ -149,9 +157,8 @@ function generateNginxConfiguration({ { cmd: ['server'], children: [ + ...serverOptions.map((cmd) => ({ cmd })), { cmd: ['listen', '0.0.0.0:$PORT', 'default_server'] }, - { cmd: ['resolver', '8.8.8.8'] }, - // https://www.gatsbyjs.com/docs/how-to/adding-common-features/add-404-page/ { cmd: ['error_page', '404', '/404.html'] }, @@ -185,16 +192,42 @@ function stringify(directives: NginxDirective[]): string { .join('\n') } +const wildcard = /\*/g +const namedSegment = /:[^/]+/g + +// Converts a gatsby path to nginx location path +// Ex: +// '/:p1/:p2/foo/*' => '^/([^/]+)/([^/]+)/foo/(.*)$' +// '/:splat' => '^/([^/]+)$' +// '/foo/bar/:splat' => '^/foo/bar/([^/]+)$' export function convertToRegExp(path: string) { - return `^${path.replace(/\*/g, '(.*)').replace(/:slug/g, '[^/]+')}$` + const converted = path + .replace(wildcard, '(.*)') // replace * with (.*) + .replace(namedSegment, '([^/]+)') // replace :param like with url component like regex ([^/]+) + + return `^${converted}$` } function isRegExpMatch(path: string) { - return /\*/g.test(path) || /:slug/g.test(path) + return wildcard.test(path) || namedSegment.test(path) } +// Converts a gatsby path to nginx proxy_pass path +// Ex: +// '/:p1/:p2/foo/*' => /$1/$2/foo/$3 +// '/:splat' => '/$1' +// '/foo/bar/:splat' => '/foo/bar/$1' function convertToPath(path: string) { - return path.replace(/:splat/g, '$1') + let it = 0 + + return path.replace( + new RegExp(`${wildcard.source}|${namedSegment.source}`, 'g'), + () => { + it += 1 + + return `$${it}` + } + ) } function parseRewrite({ diff --git a/packages/gatsby-plugin-nginx/src/pluginOptions.ts b/packages/gatsby-plugin-nginx/src/pluginOptions.ts index 88d972c96a..f536452b6d 100644 --- a/packages/gatsby-plugin-nginx/src/pluginOptions.ts +++ b/packages/gatsby-plugin-nginx/src/pluginOptions.ts @@ -10,6 +10,8 @@ const defaultOptions: PluginOptions = { disableBrotliEncoding: false, serveFileDirective: ['try_files', '/$file', '=404'], plugins: [], + httpOptions: [['proxy_http_version', '1.1']], + serverOptions: [['resolver', '8.8.8.8']], } export function pluginOptions(options: Partial): PluginOptions { @@ -33,6 +35,8 @@ export function pluginOptions(options: Partial): PluginOptions { options.serveFileDirective ?? defaultOptions.serveFileDirective, plugins: options.plugins ?? defaultOptions.plugins, + httpOptions: options.httpOptions ?? defaultOptions.httpOptions, + serverOptions: options.serverOptions ?? defaultOptions.serverOptions, } } diff --git a/packages/gatsby-plugin-nginx/src/typings/types.d.ts b/packages/gatsby-plugin-nginx/src/typings/types.d.ts index 107891d9bf..1445d4858e 100644 --- a/packages/gatsby-plugin-nginx/src/typings/types.d.ts +++ b/packages/gatsby-plugin-nginx/src/typings/types.d.ts @@ -1,3 +1,4 @@ +import { NginxDirective } from './../../dist/nginx-generator.d' import { PageProps, Actions, @@ -53,5 +54,25 @@ declare global { * @default false */ disableBrotliEncoding: boolean + /** + * Add attributes to nginx's server block options + * + * @example + * // Add Google dns server + * serverOptions: [['resolver', '8.8.8.8']] + * + * @default [['resolver', '8.8.8.8']] + */ + serverOptions: string[][] + /** + * Add attributes to nginx's http block options + * + * @example + * // Disable merge_slashes nginx config + * httpOptions: [['merge_slashes', 'off']] + * + * * @default [['proxy_http_version', '1.1']] + */ + httpOptions: string[][] } } diff --git a/packages/gatsby-plugin-nginx/test/nginx-generator.test.ts b/packages/gatsby-plugin-nginx/test/nginx-generator.test.ts index b7d27d5c15..44de07335a 100644 --- a/packages/gatsby-plugin-nginx/test/nginx-generator.test.ts +++ b/packages/gatsby-plugin-nginx/test/nginx-generator.test.ts @@ -43,9 +43,15 @@ describe('stringify', () => { describe('convert Gatsby paths into nginx RegExp', () => { it('handles :slug', () => { - expect(convertToRegExp('/:slug/p')).toEqual('^/[^/]+/p$') - expect(convertToRegExp('/:slug')).toEqual('^/[^/]+$') - expect(convertToRegExp('/pt/:slug/p')).toEqual('^/pt/[^/]+/p$') + expect(convertToRegExp('/:slug/p')).toEqual('^/([^/]+)/p$') + expect(convertToRegExp('/:slug')).toEqual('^/([^/]+)$') + expect(convertToRegExp('/pt/:slug/p')).toEqual('^/pt/([^/]+)/p$') + }) + + it('handles multiple params', () => { + expect(convertToRegExp('/:p1/:p2/p')).toEqual('^/([^/]+)/([^/]+)/p$') + expect(convertToRegExp('/base/:p1/:p2')).toEqual('^/base/([^/]+)/([^/]+)$') + expect(convertToRegExp('/:p1/foo/:p2')).toEqual('^/([^/]+)/foo/([^/]+)$') }) it('handles wildcard (*)', () => { @@ -90,11 +96,11 @@ describe('generateRewrites', () => { cmd: ['rewrite', '.+', '/__client-side-product__/p'], }, ], - cmd: ['location', '~*', '"^/[^/]+/p$"'], + cmd: ['location', '~*', '"^/([^/]+)/p$"'], }, { children: [{ cmd: ['rewrite', '.+', '/pt/__client-side-product__/p'] }], - cmd: ['location', '~*', '"^/pt/[^/]+/p$"'], + cmd: ['location', '~*', '"^/pt/([^/]+)/p$"'], }, { children: [{ cmd: ['rewrite', '.+', '/__client-side-search__'] }], @@ -104,6 +110,14 @@ describe('generateRewrites', () => { children: [{ cmd: ['rewrite', '.+', '/pt/__client-side-search__'] }], cmd: ['location', '~*', '"^/pt/(.*)$"'], }, + { + children: [{ cmd: ['rewrite', '.+', '/foo-path'] }], + cmd: ['location', '~*', '"^/([^/]+)/([^/]+)/foo$"'], + }, + { + children: [{ cmd: ['rewrite', '.+', '/bar-path'] }], + cmd: ['location', '~*', '"^/([^/]+)/bar/([^/]+)$"'], + }, ] expect( @@ -112,6 +126,8 @@ describe('generateRewrites', () => { { fromPath: '/pt/:slug/p', toPath: '/pt/__client-side-product__/p' }, { fromPath: '/*', toPath: '/__client-side-search__' }, { fromPath: '/pt/*', toPath: '/pt/__client-side-search__' }, + { fromPath: '/:p1/:p2/foo', toPath: '/foo-path' }, + { fromPath: '/:p1/bar/:p2', toPath: '/bar-path' }, ]) ).toEqual(expected) }) @@ -184,6 +200,8 @@ describe('generateNginxConfiguration', () => { serveFileDirective: ['try_files', '/$file', '=404'], transformHeaders: undefined, writeOnlyLocations: false, + serverOptions: [], + httpOptions: [], } expect( @@ -230,7 +248,6 @@ describe('generateNginxConfiguration', () => { gzip_types text/plain text/css text/xml application/javascript application/x-javascript application/xml application/xml+rss application/emacscript application/json image/svg+xml; server { listen 0.0.0.0:$PORT default_server; - resolver 8.8.8.8; error_page 404 /404.html; location /nginx.conf { deny all; @@ -277,6 +294,8 @@ describe('generateNginxConfiguration', () => { .filter((h) => !h.includes(`Cache-Control`)) .concat(`Cache-Control: public`), writeOnlyLocations: false, + serverOptions: [], + httpOptions: [], } const start = performance.now()