Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(gatsby-adapter-netlify): adapter use headerRoutes (#38652) #38674

Merged
merged 1 commit into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions e2e-tests/adapters/cypress/e2e/basics.cy.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import { title } from "../../constants"
import { WorkaroundCachedResponse } from "../utils/dont-cache-responses-in-browser"

describe("Basics", () => {
beforeEach(() => {
cy.intercept("/gatsby-icon.png").as("static-folder-image")
cy.intercept("/static/astro-**.png", req => {
req.on("before:response", res => {
// this generally should be permamently cached, but that cause problems with intercepting
// see https://docs.cypress.io/api/commands/intercept#cyintercept-and-request-caching
// so we disable caching for this response
// tests for cache-control headers should be done elsewhere

res.headers["cache-control"] = "no-store"
})
}).as("img-import")
cy.intercept("/static/astro-**.png", WorkaroundCachedResponse).as(
"img-import"
)

cy.visit("/").waitForRouteChange()
})
Expand Down
147 changes: 147 additions & 0 deletions e2e-tests/adapters/cypress/e2e/headers.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { WorkaroundCachedResponse } from "../utils/dont-cache-responses-in-browser"

describe("Headers", () => {
const defaultHeaders = {
"x-xss-protection": "1; mode=block",
"x-content-type-options": "nosniff",
"referrer-policy": "same-origin",
"x-frame-options": "DENY",
}

// DRY for repeated assertions in multple tests
const expectedHeadersByRouteAlias = {
"@app-data": {
...defaultHeaders,
"cache-control": "public,max-age=0,must-revalidate",
},
"@page-data": {
...defaultHeaders,
"cache-control": "public,max-age=0,must-revalidate",
},
"@slice-data": {
...defaultHeaders,
"cache-control": "public,max-age=0,must-revalidate",
},
"@static-query-result": {
...defaultHeaders,
"cache-control": "public,max-age=0,must-revalidate",
},
"@img-webpack-import": {
...defaultHeaders,
"cache-control": "public,max-age=31536000,immutable",
},
"@js": {
...defaultHeaders,
"cache-control": "public,max-age=31536000,immutable",
},
}

// `ntl serve` and actual deploy seem to have possible slight differences around header value formatting
// so this just remove spaces around commas to make it easier to compare
function normalizeHeaderValue(value: string | undefined): string | undefined {
if (typeof value === "undefined") {
return value
}
// Remove spaces around commas
return value.replace(/\s*,\s*/gm, `,`)
}
function checkHeaders(
routeAlias: string,
expectedHeaders?: Record<string, string>
) {
if (!expectedHeaders) {
expectedHeaders = expectedHeadersByRouteAlias[routeAlias]
}

if (!expectedHeaders) {
throw new Error(`No expected headers provided for "${routeAlias}`)
}

cy.wait(routeAlias).then(interception => {
Object.keys(expectedHeaders).forEach(headerKey => {
const headers = interception.response.headers[headerKey]

const firstHeader: string = Array.isArray(headers)
? headers[0]
: headers

expect(normalizeHeaderValue(firstHeader)).to.eq(
normalizeHeaderValue(expectedHeaders[headerKey])
)
})
})
}

beforeEach(() => {
cy.intercept("/", WorkaroundCachedResponse).as("index")
cy.intercept("routes/ssr/static", WorkaroundCachedResponse).as("ssr")
cy.intercept("routes/dsg/static", WorkaroundCachedResponse).as("dsg")

cy.intercept("**/page-data.json", WorkaroundCachedResponse).as("page-data")
cy.intercept("**/app-data.json", WorkaroundCachedResponse).as("app-data")
cy.intercept("**/slice-data/*.json", WorkaroundCachedResponse).as(
"slice-data"
)
cy.intercept("**/page-data/sq/d/*.json", WorkaroundCachedResponse).as(
"static-query-result"
)

cy.intercept("/static/astro-**.png", WorkaroundCachedResponse).as(
"img-webpack-import"
)
cy.intercept("*.js", WorkaroundCachedResponse).as("js")
})

it("should contain correct headers for index page", () => {
cy.visit("/").waitForRouteChange()

checkHeaders("@index", {
...defaultHeaders,
"x-custom-header": "my custom header value",
"cache-control": "public,max-age=0,must-revalidate",
})

checkHeaders("@app-data")
checkHeaders("@page-data")
checkHeaders("@slice-data")
checkHeaders("@static-query-result")

// index page is only one showing webpack imported image
checkHeaders("@img-webpack-import")
checkHeaders("@js")
})

it("should contain correct headers for ssr page", () => {
cy.visit("routes/ssr/static").waitForRouteChange()

checkHeaders("@ssr", {
...defaultHeaders,
"x-custom-header": "my custom header value",
"x-ssr-header": "my custom header value from config",
"x-ssr-header-getserverdata": "my custom header value from getServerData",
"x-ssr-header-overwrite": "getServerData wins",
})

checkHeaders("@app-data")
// page-data is baked into SSR page so it's not fetched and we don't assert it
checkHeaders("@slice-data")
checkHeaders("@static-query-result")
checkHeaders("@js")
})

it("should contain correct headers for dsg page", () => {
cy.visit("routes/dsg/static").waitForRouteChange()

checkHeaders("@dsg", {
...defaultHeaders,
"x-custom-header": "my custom header value",
"x-dsg-header": "my custom header value",
})

checkHeaders("@app-data")
checkHeaders("@page-data")
checkHeaders("@slice-data")
checkHeaders("@static-query-result")
checkHeaders("@js")
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { CyHttpMessages } from "cypress/types/net-stubbing"

/**
* https://docs.cypress.io/api/commands/intercept#cyintercept-and-request-caching
*
* For responses that are to be cached we need to use a trick so browser doesn't cache them
* So this enforces `no-store` cache-control header before response hits the browser
* and then restore original cache-control value for assertions.
*/
export const WorkaroundCachedResponse = (
req: CyHttpMessages.IncomingHttpRequest
): void | Promise<void> => {
req.on("before:response", res => {
res.headers["x-original-cache-control"] = res.headers["cache-control"]
res.headers["cache-control"] = "no-store"
})
req.on("after:response", res => {
res.headers["cache-control"] = res.headers["x-original-cache-control"]
delete res.headers["x-original-cache-control"]
})
}
34 changes: 21 additions & 13 deletions e2e-tests/adapters/debug-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { inspect } from "util"
import type { AdapterInit } from "gatsby"

const createTestingAdapter: AdapterInit = (adapterOptions) => {
const createTestingAdapter: AdapterInit = adapterOptions => {
return {
name: `gatsby-adapter-debug`,
cache: {
Expand All @@ -10,28 +10,36 @@ const createTestingAdapter: AdapterInit = (adapterOptions) => {
},
store({ directories, reporter }) {
reporter.info(`[gatsby-adapter-debug] cache.store() ${directories}`)
}
},
},
adapt({
routesManifest,
headerRoutes,
functionsManifest,
pathPrefix,
trailingSlash,
reporter,
}) {
reporter.info(`[gatsby-adapter-debug] adapt()`)

console.log(`[gatsby-adapter-debug] adapt()`, inspect({
routesManifest,
functionsManifest,
pathPrefix,
trailingSlash,
}, {
depth: Infinity,
colors: true
}))
}
console.log(
`[gatsby-adapter-debug] adapt()`,
inspect(
{
routesManifest,
headerRoutes,
functionsManifest,
pathPrefix,
trailingSlash,
},
{
depth: Infinity,
colors: true,
}
)
)
},
}
}

export default createTestingAdapter
export default createTestingAdapter
36 changes: 35 additions & 1 deletion e2e-tests/adapters/gatsby-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import debugAdapter from "./debug-adapter"
import { siteDescription, title } from "./constants"

const shouldUseDebugAdapter = process.env.USE_DEBUG_ADAPTER ?? false
const trailingSlash = (process.env.TRAILING_SLASH || `never`) as GatsbyConfig["trailingSlash"]
const trailingSlash = (process.env.TRAILING_SLASH ||
`never`) as GatsbyConfig["trailingSlash"]

let configOverrides: GatsbyConfig = {}

Expand All @@ -21,6 +22,39 @@ const config: GatsbyConfig = {
},
trailingSlash,
plugins: [],
headers: [
{
source: `/*`,
headers: [
{
key: "x-custom-header",
value: "my custom header value",
},
],
},
{
source: `routes/ssr/*`,
headers: [
{
key: "x-ssr-header",
value: "my custom header value from config",
},
{
key: "x-ssr-header-overwrite",
value: "config wins",
},
],
},
{
source: `routes/dsg/*`,
headers: [
{
key: "x-dsg-header",
value: "my custom header value",
},
],
},
],
...configOverrides,
}

Expand Down
37 changes: 17 additions & 20 deletions e2e-tests/adapters/scripts/deploy-and-run/netlify.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,24 +37,21 @@ console.log(`Deployed to ${deployInfo.deploy_url}`)
try {
await execa(`npm`, [`run`, npmScriptToRun], { stdio: `inherit` })
} finally {
if (!process.env.GATSBY_TEST_SKIP_CLEANUP) {
console.log(`Deleting project with deploy_id ${deployInfo.deploy_id}`)

const deleteResponse = await execa("ntl", [
"api",
"deleteDeploy",
"--data",
`{ "deploy_id": "${deployInfo.deploy_id}" }`,
])

if (deleteResponse.exitCode !== 0) {
throw new Error(
`Failed to delete project ${deleteResponse.stdout} ${deleteResponse.stderr} (${deleteResponse.exitCode})`
)
}

console.log(
`Successfully deleted project with deploy_id ${deployInfo.deploy_id}`
)
}
// if (!process.env.GATSBY_TEST_SKIP_CLEANUP) {
// console.log(`Deleting project with deploy_id ${deployInfo.deploy_id}`)
// const deleteResponse = await execa("ntl", [
// "api",
// "deleteDeploy",
// "--data",
// `{ "deploy_id": "${deployInfo.deploy_id}" }`,
// ])
// if (deleteResponse.exitCode !== 0) {
// throw new Error(
// `Failed to delete project ${deleteResponse.stdout} ${deleteResponse.stderr} (${deleteResponse.exitCode})`
// )
// }
// console.log(
// `Successfully deleted project with deploy_id ${deployInfo.deploy_id}`
// )
// }
}
18 changes: 12 additions & 6 deletions e2e-tests/adapters/src/pages/routes/ssr/static.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ const SSR = ({ serverData }) => {
<h1>SSR</h1>
<div>
<code>
<pre>
{JSON.stringify({ serverData }, null, 2)}
</pre>
<pre>{JSON.stringify({ serverData }, null, 2)}</pre>
</code>
</div>
<div>
<code>
<pre data-testid="query">{JSON.stringify(serverData?.arg?.query)}</pre>
<pre data-testid="params">{JSON.stringify(serverData?.arg?.params)}</pre>
<pre data-testid="query">
{JSON.stringify(serverData?.arg?.query)}
</pre>
<pre data-testid="params">
{JSON.stringify(serverData?.arg?.params)}
</pre>
</code>
</div>
</Layout>
Expand All @@ -32,5 +34,9 @@ export function getServerData(arg) {
ssr: true,
arg,
},
headers: {
"x-ssr-header-getserverdata": "my custom header value from getServerData",
"x-ssr-header-overwrite": "getServerData wins",
},
}
}
}
9 changes: 7 additions & 2 deletions packages/gatsby-adapter-netlify/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,14 @@ const createNetlifyAdapter: AdapterInit<INetlifyAdapterOptions> = options => {
}
},
},
async adapt({ routesManifest, functionsManifest }): Promise<void> {
async adapt({
routesManifest,
functionsManifest,
headerRoutes,
}): Promise<void> {
const { lambdasThatUseCaching } = await handleRoutesManifest(
routesManifest
routesManifest,
headerRoutes
)

// functions handling
Expand Down
Loading