From 264d268547ca788144eba015a44510525e7c87e5 Mon Sep 17 00:00:00 2001 From: Sarah Etter Date: Wed, 20 Apr 2022 08:43:26 -0400 Subject: [PATCH 1/4] feat: add conversion of country and language redirects to _redirects file --- src/create-redirects.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/create-redirects.ts b/src/create-redirects.ts index bee77f91..2e49fd8c 100644 --- a/src/create-redirects.ts +++ b/src/create-redirects.ts @@ -17,11 +17,15 @@ export default async function writeRedirectsFile(pluginData: any, redirects: any `headers`, `signed`, `edge_handler`, - `Language`, - `Country`, + ]) + + const NETLIFY_CONDITIONS_ALLOWLIST = new Set([ + `language`, + `country`, ]) // Map redirect data to the format Netlify expects + // eslint-disable-next-line max-statements redirects = redirects.map((redirect: any) => { const { fromPath, isPermanent, redirectInBrowser, force, toPath, statusCode, ...rest } = redirect @@ -39,6 +43,19 @@ export default async function writeRedirectsFile(pluginData: any, redirects: any if (typeof value === `string` && value.includes(` `)) { console.warn(`Invalid redirect value "${value}" specified for key "${key}". Values should not contain spaces.`) + } else if (key === 'conditions') { + // "conditions" key from Gatsby contains only "language" and "country" + // which need special transformation to match Netlify _redirects + // https://www.gatsbyjs.com/docs/reference/config-files/actions/#createRedirect + + for (const conditionKey in value) { + if (NETLIFY_CONDITIONS_ALLOWLIST.has(conditionKey)) { + const conditionValue = Array.isArray(value[conditionKey]) ? value[conditionKey].join(',') : value[conditionKey] + // Gatsby gives us "country", we want "Country" + const conditionName = conditionKey.charAt(0).toUpperCase() + conditionKey.slice(1) + pieces.push(`${conditionName}=${conditionValue}`) + } + } } else if (NETLIFY_REDIRECT_KEYWORDS_ALLOWLIST.has(key)) { pieces.push(`${key}=${value}`) } From 7ab4d938687a94b8ea2acfc3a4bd35c2c13202cc Mon Sep 17 00:00:00 2001 From: Sarah Etter Date: Wed, 20 Apr 2022 08:44:30 -0400 Subject: [PATCH 2/4] feat: add conversion of wildcard and splat redirects to _redirects file --- src/create-redirects.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/create-redirects.ts b/src/create-redirects.ts index 2e49fd8c..6cc17396 100644 --- a/src/create-redirects.ts +++ b/src/create-redirects.ts @@ -2,6 +2,22 @@ import { existsSync, readFile, writeFile } from 'fs-extra' import { HEADER_COMMENT } from './constants' +const toNetlifyPath = (fromPath: string, toPath: string): Array => { + let netlifyFromPath = fromPath + let netlifyToPath = toPath + + // Wildcard & splat redirects + if (fromPath.includes('*')) { + netlifyFromPath = fromPath + netlifyToPath = toPath.replace(/\*/, ':splat') + } + + return [ + netlifyFromPath, + netlifyToPath, + ] +} + // eslint-disable-next-line max-statements export default async function writeRedirectsFile(pluginData: any, redirects: any, rewrites: any) { const { publicFolder } = pluginData @@ -34,9 +50,11 @@ export default async function writeRedirectsFile(pluginData: any, redirects: any if (force) status = `${status}!` + const [netlifyFromPath, netlifyToPath] = toNetlifyPath(fromPath, toPath) + // The order of the first 3 parameters is significant. // The order for rest params (key-value pairs) is arbitrary. - const pieces = [fromPath, toPath, status] + const pieces = [netlifyFromPath, netlifyToPath, status] for (const key in rest) { const value = rest[key] From 630bf73ff05b382bdb333005175d031bad070c06 Mon Sep 17 00:00:00 2001 From: Sarah Etter Date: Wed, 20 Apr 2022 08:45:43 -0400 Subject: [PATCH 3/4] feat: add conversion of query param redirects to _redirects file --- src/create-redirects.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/create-redirects.ts b/src/create-redirects.ts index 6cc17396..fda18c49 100644 --- a/src/create-redirects.ts +++ b/src/create-redirects.ts @@ -3,14 +3,10 @@ import { existsSync, readFile, writeFile } from 'fs-extra' import { HEADER_COMMENT } from './constants' const toNetlifyPath = (fromPath: string, toPath: string): Array => { - let netlifyFromPath = fromPath - let netlifyToPath = toPath - - // Wildcard & splat redirects - if (fromPath.includes('*')) { - netlifyFromPath = fromPath - netlifyToPath = toPath.replace(/\*/, ':splat') - } + // Modifies query parameter redirects, having no effect on other fromPath strings + const netlifyFromPath = fromPath.replace(/[&?]/, ' ') + // Modifies wildcard & splat redirects, having no effect on other toPath strings + const netlifyToPath = toPath.replace(/\*/, ':splat') return [ netlifyFromPath, From 18438f4f394e493edab9f0b6cad5a0254079b00b Mon Sep 17 00:00:00 2001 From: Sarah Etter Date: Wed, 20 Apr 2022 08:46:47 -0400 Subject: [PATCH 4/4] test: Add test for create-redirects --- .../__snapshots__/create-redirects.ts.snap | 13 ++ src/__tests__/build-headers-program.ts | 175 +---------------- src/__tests__/create-redirects.ts | 89 +++++++++ src/__tests__/helpers.ts | 177 ++++++++++++++++++ 4 files changed, 281 insertions(+), 173 deletions(-) create mode 100644 src/__tests__/__snapshots__/create-redirects.ts.snap create mode 100644 src/__tests__/create-redirects.ts create mode 100644 src/__tests__/helpers.ts diff --git a/src/__tests__/__snapshots__/create-redirects.ts.snap b/src/__tests__/__snapshots__/create-redirects.ts.snap new file mode 100644 index 00000000..08fdf1c0 --- /dev/null +++ b/src/__tests__/__snapshots__/create-redirects.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`create-redirects writes file 1`] = ` +" +## Created with gatsby-plugin-netlify +/foo /bar 302 +/blog /canada/blog 302 Country=ca +/speaker /english/speaker 302 Language=en +/speaker /french/speaker 302 Language=fr +/param id=7 /test 302 +/dogs/* /animals/:splat 302 +/cats id=:id /animals/:id 302" +`; diff --git a/src/__tests__/build-headers-program.ts b/src/__tests__/build-headers-program.ts index 7cd5385b..5b680bcb 100644 --- a/src/__tests__/build-headers-program.ts +++ b/src/__tests__/build-headers-program.ts @@ -1,179 +1,9 @@ -/* eslint-disable max-lines, max-nested-callbacks */ -import { tmpdir } from 'os' -import { join } from 'path' - -import { existsSync, mkdtemp, readFile } from 'fs-extra' +import { existsSync, readFile } from 'fs-extra' import buildHeadersProgram from '../build-headers-program' import { DEFAULT_OPTIONS } from '../constants' -// eslint-disable-next-line max-lines-per-function -const createPluginData = async () => { - const tmpDir = await mkdtemp(join(tmpdir(), `gatsby-plugin-netlify-`)) - - return { - components: new Map([ - [ - 1, - { - componentChunkName: `component---node-modules-gatsby-plugin-offline-app-shell-js`, - }, - ], - [ - 2, - { - componentChunkName: `component---src-templates-blog-post-js`, - }, - ], - [ - 3, - { - componentChunkName: `component---src-pages-404-js`, - }, - ], - [ - 4, - { - componentChunkName: `component---src-pages-index-js`, - }, - ], - ]), - pages: new Map([ - [ - `/offline-plugin-app-shell-fallback/`, - { - jsonName: `offline-plugin-app-shell-fallback-a30`, - internalComponentName: `ComponentOfflinePluginAppShellFallback`, - path: `/offline-plugin-app-shell-fallback/`, - matchPath: undefined, - componentChunkName: `component---node-modules-gatsby-plugin-offline-app-shell-js`, - isCreatedByStatefulCreatePages: false, - context: {}, - updatedAt: 1_557_740_602_268, - pluginCreator___NODE: `63e5f7ff-e5f1-58f7-8e2c-55872ac42281`, - pluginCreatorId: `63e5f7ff-e5f1-58f7-8e2c-55872ac42281`, - }, - ], - [ - `/hi-folks/`, - { - jsonName: `hi-folks-a2b`, - internalComponentName: `ComponentHiFolks`, - path: `/hi-folks/`, - matchPath: undefined, - componentChunkName: `component---src-templates-blog-post-js`, - isCreatedByStatefulCreatePages: false, - context: {}, - updatedAt: 1_557_740_602_330, - pluginCreator___NODE: `7374ebf2-d961-52ee-92a2-c25e7cb387a9`, - pluginCreatorId: `7374ebf2-d961-52ee-92a2-c25e7cb387a9`, - }, - ], - [ - `/my-second-post/`, - { - jsonName: `my-second-post-2aa`, - internalComponentName: `ComponentMySecondPost`, - path: `/my-second-post/`, - matchPath: undefined, - componentChunkName: `component---src-templates-blog-post-js`, - isCreatedByStatefulCreatePages: false, - context: {}, - updatedAt: 1_557_740_602_333, - pluginCreator___NODE: `7374ebf2-d961-52ee-92a2-c25e7cb387a9`, - pluginCreatorId: `7374ebf2-d961-52ee-92a2-c25e7cb387a9`, - }, - ], - [ - `/hello-world/`, - { - jsonName: `hello-world-8bc`, - internalComponentName: `ComponentHelloWorld`, - path: `/hello-world/`, - matchPath: undefined, - componentChunkName: `component---src-templates-blog-post-js`, - isCreatedByStatefulCreatePages: false, - context: {}, - updatedAt: 1_557_740_602_335, - pluginCreator___NODE: `7374ebf2-d961-52ee-92a2-c25e7cb387a9`, - pluginCreatorId: `7374ebf2-d961-52ee-92a2-c25e7cb387a9`, - }, - ], - [ - `/404/`, - { - jsonName: `404-22d`, - internalComponentName: `Component404`, - path: `/404/`, - matchPath: undefined, - componentChunkName: `component---src-pages-404-js`, - isCreatedByStatefulCreatePages: true, - context: {}, - updatedAt: 1_557_740_602_358, - pluginCreator___NODE: `049c1cfd-95f7-5555-a4ac-9b396d098b26`, - pluginCreatorId: `049c1cfd-95f7-5555-a4ac-9b396d098b26`, - }, - ], - [ - `/`, - { - jsonName: `index`, - internalComponentName: `ComponentIndex`, - path: `/`, - matchPath: undefined, - componentChunkName: `component---src-pages-index-js`, - isCreatedByStatefulCreatePages: true, - context: {}, - updatedAt: 1_557_740_602_361, - pluginCreator___NODE: `049c1cfd-95f7-5555-a4ac-9b396d098b26`, - pluginCreatorId: `049c1cfd-95f7-5555-a4ac-9b396d098b26`, - }, - ], - [ - `/404.html`, - { - jsonName: `404-html-516`, - internalComponentName: `Component404Html`, - path: `/404.html`, - matchPath: undefined, - componentChunkName: `component---src-pages-404-js`, - isCreatedByStatefulCreatePages: true, - context: {}, - updatedAt: 1_557_740_602_382, - pluginCreator___NODE: `f795702c-a3b8-5a88-88ee-5d06019d44fa`, - pluginCreatorId: `f795702c-a3b8-5a88-88ee-5d06019d44fa`, - }, - ], - ]), - manifest: { - 'main.js': `render-page.js`, - 'main.js.map': `render-page.js.map`, - app: [ - `webpack-runtime-acaa8994f1f704475e21.js`, - `styles.1025963f4f2ec7abbad4.css`, - `styles-565f081c8374bbda155f.js`, - `app-f33c13590352da20930f.js`, - ], - 'component---node-modules-gatsby-plugin-offline-app-shell-js': [ - `component---node-modules-gatsby-plugin-offline-app-shell-js-78f9e4dea04737fa062d.js`, - ], - 'component---src-templates-blog-post-js': [ - `0-0180cd94ef2497ac7db8.js`, - `component---src-templates-blog-post-js-517987eae96e75cddbe7.js`, - ], - 'component---src-pages-404-js': [ - `0-0180cd94ef2497ac7db8.js`, - `component---src-pages-404-js-53e6c51a5a7e73090f50.js`, - ], - 'component---src-pages-index-js': [ - `0-0180cd94ef2497ac7db8.js`, - `component---src-pages-index-js-0bdd01c77ee09ef0224c.js`, - ], - }, - pathPrefix: ``, - publicFolder: (...files: any[]) => join(tmpDir, ...files), - } -} +import { createPluginData } from './helpers' jest.mock(`fs-extra`, () => { const actualFsExtra = jest.requireActual(`fs-extra`) @@ -308,4 +138,3 @@ describe(`build-headers-program`, () => { expect(reporter.panic).toHaveBeenCalled() }) }) -/* eslint-enable max-lines, max-nested-callbacks */ diff --git a/src/__tests__/create-redirects.ts b/src/__tests__/create-redirects.ts new file mode 100644 index 00000000..31cdf6fe --- /dev/null +++ b/src/__tests__/create-redirects.ts @@ -0,0 +1,89 @@ +import { readFile } from 'fs-extra' + +import createRedirects from '../create-redirects' + +import { createPluginData } from './helpers' + +jest.mock(`fs-extra`, () => { + const actualFsExtra = jest.requireActual(`fs-extra`) + return { + ...actualFsExtra, + } +}) + +const redirects = [ + { + "fromPath": "/foo", + "isPermanent": false, + "ignoreCase": true, + "redirectInBrowser": false, + "toPath": "/bar" + }, + { + "fromPath": "/blog", + "isPermanent": false, + "ignoreCase": true, + "redirectInBrowser": false, + "toPath": "/canada/blog", + "conditions": { + "country": "ca" + } + }, + { + "fromPath": "/speaker", + "isPermanent": false, + "ignoreCase": true, + "redirectInBrowser": false, + "toPath": "/english/speaker", + "conditions": { + "language": [ + "en" + ] + } + }, + { + "fromPath": "/speaker", + "isPermanent": false, + "ignoreCase": true, + "redirectInBrowser": false, + "toPath": "/french/speaker", + "conditions": { + "language": [ + "fr" + ] + } + }, + { + "fromPath": "/param?id=7", + "isPermanent": false, + "ignoreCase": true, + "redirectInBrowser": false, + "toPath": "/test" + }, + { + "fromPath": "/dogs/*", + "isPermanent": false, + "ignoreCase": true, + "redirectInBrowser": false, + "toPath": "/animals/*" + }, + { + "fromPath": "/cats?id=:id", + "isPermanent": false, + "ignoreCase": true, + "redirectInBrowser": false, + "toPath": "/animals/:id" + } +] + +describe(`create-redirects`, () => { + + it(`writes file`, async () => { + const pluginData = await createPluginData() + + await createRedirects(pluginData, redirects, []) + + const output = await readFile(pluginData.publicFolder(`_redirects`), `utf8`) + expect(output).toMatchSnapshot() + }) +}) diff --git a/src/__tests__/helpers.ts b/src/__tests__/helpers.ts new file mode 100644 index 00000000..aa9c33cb --- /dev/null +++ b/src/__tests__/helpers.ts @@ -0,0 +1,177 @@ +/* eslint-disable max-lines */ +import { tmpdir } from 'os' +import { join } from 'path' + +import { mkdtemp } from 'fs-extra' + +// eslint-disable-next-line max-lines-per-function +export const createPluginData = async () => { + const tmpDir = await mkdtemp(join(tmpdir(), `gatsby-plugin-netlify-`)) + + return { + components: new Map([ + [ + 1, + { + componentChunkName: `component---node-modules-gatsby-plugin-offline-app-shell-js`, + }, + ], + [ + 2, + { + componentChunkName: `component---src-templates-blog-post-js`, + }, + ], + [ + 3, + { + componentChunkName: `component---src-pages-404-js`, + }, + ], + [ + 4, + { + componentChunkName: `component---src-pages-index-js`, + }, + ], + ]), + pages: new Map([ + [ + `/offline-plugin-app-shell-fallback/`, + { + jsonName: `offline-plugin-app-shell-fallback-a30`, + internalComponentName: `ComponentOfflinePluginAppShellFallback`, + path: `/offline-plugin-app-shell-fallback/`, + matchPath: undefined, + componentChunkName: `component---node-modules-gatsby-plugin-offline-app-shell-js`, + isCreatedByStatefulCreatePages: false, + context: {}, + updatedAt: 1_557_740_602_268, + pluginCreator___NODE: `63e5f7ff-e5f1-58f7-8e2c-55872ac42281`, + pluginCreatorId: `63e5f7ff-e5f1-58f7-8e2c-55872ac42281`, + }, + ], + [ + `/hi-folks/`, + { + jsonName: `hi-folks-a2b`, + internalComponentName: `ComponentHiFolks`, + path: `/hi-folks/`, + matchPath: undefined, + componentChunkName: `component---src-templates-blog-post-js`, + isCreatedByStatefulCreatePages: false, + context: {}, + updatedAt: 1_557_740_602_330, + pluginCreator___NODE: `7374ebf2-d961-52ee-92a2-c25e7cb387a9`, + pluginCreatorId: `7374ebf2-d961-52ee-92a2-c25e7cb387a9`, + }, + ], + [ + `/my-second-post/`, + { + jsonName: `my-second-post-2aa`, + internalComponentName: `ComponentMySecondPost`, + path: `/my-second-post/`, + matchPath: undefined, + componentChunkName: `component---src-templates-blog-post-js`, + isCreatedByStatefulCreatePages: false, + context: {}, + updatedAt: 1_557_740_602_333, + pluginCreator___NODE: `7374ebf2-d961-52ee-92a2-c25e7cb387a9`, + pluginCreatorId: `7374ebf2-d961-52ee-92a2-c25e7cb387a9`, + }, + ], + [ + `/hello-world/`, + { + jsonName: `hello-world-8bc`, + internalComponentName: `ComponentHelloWorld`, + path: `/hello-world/`, + matchPath: undefined, + componentChunkName: `component---src-templates-blog-post-js`, + isCreatedByStatefulCreatePages: false, + context: {}, + updatedAt: 1_557_740_602_335, + pluginCreator___NODE: `7374ebf2-d961-52ee-92a2-c25e7cb387a9`, + pluginCreatorId: `7374ebf2-d961-52ee-92a2-c25e7cb387a9`, + }, + ], + [ + `/404/`, + { + jsonName: `404-22d`, + internalComponentName: `Component404`, + path: `/404/`, + matchPath: undefined, + componentChunkName: `component---src-pages-404-js`, + isCreatedByStatefulCreatePages: true, + context: {}, + updatedAt: 1_557_740_602_358, + pluginCreator___NODE: `049c1cfd-95f7-5555-a4ac-9b396d098b26`, + pluginCreatorId: `049c1cfd-95f7-5555-a4ac-9b396d098b26`, + }, + ], + [ + `/`, + { + jsonName: `index`, + internalComponentName: `ComponentIndex`, + path: `/`, + matchPath: undefined, + componentChunkName: `component---src-pages-index-js`, + isCreatedByStatefulCreatePages: true, + context: {}, + updatedAt: 1_557_740_602_361, + pluginCreator___NODE: `049c1cfd-95f7-5555-a4ac-9b396d098b26`, + pluginCreatorId: `049c1cfd-95f7-5555-a4ac-9b396d098b26`, + }, + ], + [ + `/404.html`, + { + jsonName: `404-html-516`, + internalComponentName: `Component404Html`, + path: `/404.html`, + matchPath: undefined, + componentChunkName: `component---src-pages-404-js`, + isCreatedByStatefulCreatePages: true, + context: {}, + updatedAt: 1_557_740_602_382, + pluginCreator___NODE: `f795702c-a3b8-5a88-88ee-5d06019d44fa`, + pluginCreatorId: `f795702c-a3b8-5a88-88ee-5d06019d44fa`, + }, + ], + ]), + manifest: { + 'main.js': `render-page.js`, + 'main.js.map': `render-page.js.map`, + app: [ + `webpack-runtime-acaa8994f1f704475e21.js`, + `styles.1025963f4f2ec7abbad4.css`, + `styles-565f081c8374bbda155f.js`, + `app-f33c13590352da20930f.js`, + ], + 'component---node-modules-gatsby-plugin-offline-app-shell-js': [ + `component---node-modules-gatsby-plugin-offline-app-shell-js-78f9e4dea04737fa062d.js`, + ], + 'component---src-templates-blog-post-js': [ + `0-0180cd94ef2497ac7db8.js`, + `component---src-templates-blog-post-js-517987eae96e75cddbe7.js`, + ], + 'component---src-pages-404-js': [ + `0-0180cd94ef2497ac7db8.js`, + `component---src-pages-404-js-53e6c51a5a7e73090f50.js`, + ], + 'component---src-pages-index-js': [ + `0-0180cd94ef2497ac7db8.js`, + `component---src-pages-index-js-0bdd01c77ee09ef0224c.js`, + ], + }, + pathPrefix: ``, + publicFolder: (...files: any[]) => join(tmpDir, ...files), + } +} + +test.skip('skip', () => 1) + +/* eslint-enable max-lines */