diff --git a/packages/gatsby/index.d.ts b/packages/gatsby/index.d.ts index 00bb8ed93ef5f..3adbc7584892e 100644 --- a/packages/gatsby/index.d.ts +++ b/packages/gatsby/index.d.ts @@ -42,6 +42,7 @@ export { IRedirectRoute, IFunctionDefinition, RoutesManifest, + HeaderRoutes, FunctionsManifest, IAdapterConfig, } from "./dist/utils/adapter/types" diff --git a/packages/gatsby/src/utils/adapter/__tests__/__snapshots__/manager.ts.snap b/packages/gatsby/src/utils/adapter/__tests__/__snapshots__/manager.ts.snap index 2243c0842050e..b622b18d0df61 100644 --- a/packages/gatsby/src/utils/adapter/__tests__/__snapshots__/manager.ts.snap +++ b/packages/gatsby/src/utils/adapter/__tests__/__snapshots__/manager.ts.snap @@ -322,3 +322,110 @@ Array [ }, ] `; + +exports[`getRoutesManifest should return routes manifest 2`] = ` +Array [ + Object { + "headers": Array [ + Object { + "key": "x-xss-protection", + "value": "1; mode=block", + }, + Object { + "key": "x-content-type-options", + "value": "nosniff", + }, + Object { + "key": "referrer-policy", + "value": "same-origin", + }, + Object { + "key": "x-frame-options", + "value": "DENY", + }, + ], + "path": "/*", + }, + Object { + "headers": Array [ + Object { + "key": "cache-control", + "value": "public, max-age=31536000, immutable", + }, + ], + "path": "/static/*", + }, + Object { + "headers": Array [ + Object { + "key": "cache-control", + "value": "public, max-age=0, must-revalidate", + }, + ], + "path": "/", + }, + Object { + "headers": Array [ + Object { + "key": "cache-control", + "value": "public, max-age=0, must-revalidate", + }, + ], + "path": "/page-data/index/page-data.json", + }, + Object { + "headers": Array [ + Object { + "key": "cache-control", + "value": "public, max-age=0, must-revalidate", + }, + ], + "path": "/page-data/sq/d/1.json", + }, + Object { + "headers": Array [ + Object { + "key": "cache-control", + "value": "public, max-age=0, must-revalidate", + }, + ], + "path": "/page-data/app-data.json", + }, + Object { + "headers": Array [ + Object { + "key": "cache-control", + "value": "public, max-age=31536000, immutable", + }, + ], + "path": "/app-123.js", + }, + Object { + "headers": Array [ + Object { + "key": "cache-control", + "value": "public, max-age=0, must-revalidate", + }, + ], + "path": "/chunk-map.json", + }, + Object { + "headers": Array [ + Object { + "key": "cache-control", + "value": "public, max-age=0, must-revalidate", + }, + ], + "path": "/webpack.stats.json", + }, + Object { + "headers": Array [ + Object { + "key": "cache-control", + "value": "public, max-age=0, must-revalidate", + }, + ], + "path": "/_gatsby/slices/_gatsby-scripts-1.html", + }, +] +`; diff --git a/packages/gatsby/src/utils/adapter/__tests__/manager.ts b/packages/gatsby/src/utils/adapter/__tests__/manager.ts index a1b518057e0d5..601b18f88bd35 100644 --- a/packages/gatsby/src/utils/adapter/__tests__/manager.ts +++ b/packages/gatsby/src/utils/adapter/__tests__/manager.ts @@ -50,9 +50,11 @@ describe(`getRoutesManifest`, () => { process.chdir(fixturesDir) setWebpackAssets(new Set([`app-123.js`])) - const routesManifest = getRoutesManifest() + const { routes: routesManifest, headers: headerRoutes } = + getRoutesManifest() expect(routesManifest).toMatchSnapshot() + expect(headerRoutes).toMatchSnapshot() }) it(`should respect "never" trailingSlash config option`, () => { @@ -62,7 +64,7 @@ describe(`getRoutesManifest`, () => { process.chdir(fixturesDir) setWebpackAssets(new Set([`app-123.js`])) - const routesManifest = getRoutesManifest() + const { routes: routesManifest } = getRoutesManifest() expect(routesManifest).toEqual( expect.arrayContaining([ @@ -81,7 +83,7 @@ describe(`getRoutesManifest`, () => { process.chdir(fixturesDir) setWebpackAssets(new Set([`app-123.js`])) - const routesManifest = getRoutesManifest() + const { routes: routesManifest } = getRoutesManifest() expect(routesManifest).toEqual( expect.arrayContaining([ @@ -98,14 +100,87 @@ describe(`getRoutesManifest`, () => { process.chdir(fixturesDir) setWebpackAssets(new Set([`app-123.js`])) - const routesManifest = getRoutesManifest() - expect(routesManifest).toEqual( + const { routes } = getRoutesManifest() + expect(routes).toEqual( expect.arrayContaining([ expect.objectContaining({ path: `https://old-url` }), expect.objectContaining({ path: `http://old-url` }), ]) ) }) + + it(`should return header rules`, () => { + mockStoreState(stateDefault, { + config: { + ...stateDefault.config, + headers: [ + { + source: `/ssr/*`, + headers: [ + { + key: `x-ssr-header`, + value: `my custom header value from config`, + }, + ], + }, + ], + }, + }) + process.chdir(fixturesDir) + setWebpackAssets(new Set([`app-123.js`, `static/app-456.js`])) + + const { headers } = getRoutesManifest() + + expect(headers).toContainEqual({ + headers: [ + { key: `x-xss-protection`, value: `1; mode=block` }, + { key: `x-content-type-options`, value: `nosniff` }, + { key: `referrer-policy`, value: `same-origin` }, + { key: `x-frame-options`, value: `DENY` }, + ], + path: `/*`, + }) + expect(headers).toContainEqual({ + headers: [ + { + key: `cache-control`, + value: `public, max-age=31536000, immutable`, + }, + ], + path: `/static/*`, + }) + expect(headers).toContainEqual({ + headers: [ + { + key: `cache-control`, + value: `public, max-age=0, must-revalidate`, + }, + ], + path: `/page-data/index/page-data.json`, + }) + expect(headers).toContainEqual({ + headers: [ + { + key: `cache-control`, + value: `public, max-age=31536000, immutable`, + }, + ], + path: `/app-123.js`, + }) + expect(headers).not.toContainEqual({ + headers: [ + { key: `x-xss-protection`, value: `1; mode=block` }, + { key: `x-content-type-options`, value: `nosniff` }, + { key: `referrer-policy`, value: `same-origin` }, + { key: `x-frame-options`, value: `DENY` }, + ], + path: `/ssr/*`, + }) + + expect(headers).not.toContain( + expect.objectContaining({ path: `/static/app-456.js` }) + ) + }) }) describe(`getFunctionsManifest`, () => { diff --git a/packages/gatsby/src/utils/adapter/constants.ts b/packages/gatsby/src/utils/adapter/constants.ts index ff396fcebb415..88098a419fd41 100644 --- a/packages/gatsby/src/utils/adapter/constants.ts +++ b/packages/gatsby/src/utils/adapter/constants.ts @@ -27,10 +27,14 @@ export const MUST_REVALIDATE_HEADERS: IHeader["headers"] = [ ...BASE_HEADERS, ] -export const PERMAMENT_CACHING_HEADERS: IHeader["headers"] = [ +export const PERMANENT_CACHE_CONTROL_HEADER: IHeader["headers"] = [ { key: `cache-control`, value: `public, max-age=31536000, immutable`, }, +] + +export const PERMAMENT_CACHING_HEADERS: IHeader["headers"] = [ + ...PERMANENT_CACHE_CONTROL_HEADER, ...BASE_HEADERS, ] diff --git a/packages/gatsby/src/utils/adapter/manager.ts b/packages/gatsby/src/utils/adapter/manager.ts index 3ea04fdf2e757..459f0d9a4da7e 100644 --- a/packages/gatsby/src/utils/adapter/manager.ts +++ b/packages/gatsby/src/utils/adapter/manager.ts @@ -17,6 +17,7 @@ import type { IAdapter, IAdapterFinalConfig, IAdapterConfig, + HeaderRoutes, } from "./types" import { store, readState } from "../../redux" import { getPageMode } from "../page-mode" @@ -31,6 +32,7 @@ import { BASE_HEADERS, MUST_REVALIDATE_HEADERS, PERMAMENT_CACHING_HEADERS, + PERMANENT_CACHE_CONTROL_HEADER, } from "./constants" import { createHeadersMatcher } from "./create-headers" import { HTTP_STATUS_CODE } from "../../redux/types" @@ -201,10 +203,13 @@ export async function initAdapterManager(): Promise { let _routesManifest: RoutesManifest | undefined = undefined let _functionsManifest: FunctionsManifest | undefined = undefined + let _headerRoutes: HeaderRoutes | undefined = undefined const adaptContext: IAdaptContext = { get routesManifest(): RoutesManifest { if (!_routesManifest) { - _routesManifest = getRoutesManifest() + const { routes, headers } = getRoutesManifest() + _routesManifest = routes + _headerRoutes = headers } return _routesManifest @@ -216,6 +221,15 @@ export async function initAdapterManager(): Promise { return _functionsManifest }, + get headerRoutes(): HeaderRoutes { + if (!_headerRoutes) { + const { routes, headers } = getRoutesManifest() + _routesManifest = routes + _headerRoutes = headers + } + + return _headerRoutes + }, reporter, // Our internal Gatsby config allows this to be undefined but for the adapter we should always pass through the default values and correctly show this in the TypeScript types trailingSlash: trailingSlash as TrailingSlash, @@ -261,11 +275,48 @@ export function setWebpackAssets(assets: Set): void { type RouteWithScore = { score: number } & Route -function getRoutesManifest(): RoutesManifest { +const headersAreEqual = (a, b): boolean => + a.key === b.key && a.value === b.value + +const defaultHeaderRoutes: HeaderRoutes = [ + { + path: `/*`, + headers: BASE_HEADERS, + }, + { + path: `/static/*`, + headers: PERMANENT_CACHE_CONTROL_HEADER, + }, +] + +const customHeaderFilter = + (route: Route) => + (h: IHeader["headers"][0]): boolean => { + for (const baseHeader of BASE_HEADERS) { + if (headersAreEqual(baseHeader, h)) { + return false + } + } + if (route.path.startsWith(`/static/`)) { + for (const cachingHeader of PERMAMENT_CACHING_HEADERS) { + if (headersAreEqual(cachingHeader, h)) { + return false + } + } + } + return true + } + +function getRoutesManifest(): { + routes: RoutesManifest + headers: HeaderRoutes +} { const routes: Array = [] const state = store.getState() const createHeaders = createHeadersMatcher(state.config.headers) + const headerRoutes: HeaderRoutes = [...defaultHeaderRoutes] + const fileAssets = new Set( globSync(`**/**`, { cwd: posix.join(process.cwd(), `public`), @@ -293,11 +344,18 @@ function getRoutesManifest(): RoutesManifest { if (route.type !== `function`) { route.headers = createHeaders(route.path, route.headers) + const customHeaders = route.headers.filter(customHeaderFilter(route)) + if (customHeaders.length > 0) { + headerRoutes.push({ path: route.path, headers: customHeaders }) + } } - ;(route as RouteWithScore).score = rankRoute(route.path) + const routeWithScore: RouteWithScore = { + ...route, + score: rankRoute(route.path), + } - routes.push(route as RouteWithScore) + routes.push(routeWithScore) } function addStaticRoute({ @@ -509,25 +567,27 @@ function getRoutesManifest(): RoutesManifest { }) } - return ( - routes - .sort((a, b) => { - // The higher the score, the higher the specificity of our path - const order = b.score - a.score - if (order !== 0) { - return order - } + const sortedRoutes = routes + .sort((a, b) => { + // The higher the score, the higher the specificity of our path + const order = b.score - a.score + if (order !== 0) { + return order + } - // if specificity is the same we do lexigraphic comparison of path to ensure - // deterministic order regardless of order pages where created - return a.path.localeCompare(b.path) - }) - // The score should be internal only, so we remove it from the final manifest - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .map(({ score, ...rest }): Route => { - return { ...rest } - }) - ) + // if specificity is the same we do lexigraphic comparison of path to ensure + // deterministic order regardless of order pages where created + return a.path.localeCompare(b.path) + }) + // The score should be internal only, so we remove it from the final manifest + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .map(({ score, ...rest }): Route => { + return { ...rest } + }) + return { + routes: sortedRoutes, + headers: headerRoutes, + } } function getFunctionsManifest(): FunctionsManifest { diff --git a/packages/gatsby/src/utils/adapter/types.ts b/packages/gatsby/src/utils/adapter/types.ts index b9915427228eb..8e004b93d16b1 100644 --- a/packages/gatsby/src/utils/adapter/types.ts +++ b/packages/gatsby/src/utils/adapter/types.ts @@ -67,6 +67,11 @@ export type Route = IStaticRoute | IFunctionRoute | IRedirectRoute export type RoutesManifest = Array +export interface IHeaderRoute extends IBaseRoute { + headers: IHeader["headers"] +} + +export type HeaderRoutes = Array export interface IFunctionDefinition { /** * Unique identifier of this function. Corresponds to the `functionId` inside the `routesManifest`. @@ -99,6 +104,7 @@ interface IDefaultContext { export interface IAdaptContext extends IDefaultContext { routesManifest: RoutesManifest functionsManifest: FunctionsManifest + headerRoutes: HeaderRoutes /** * @see https://www.gatsbyjs.com/docs/reference/config-files/gatsby-config/#pathprefix */