Skip to content

feat: added better custom header support #1358

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

Merged
merged 24 commits into from
Jun 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
209a0c6
feat: added better custom header support
nickytonline May 24, 2022
9761de1
test: added some tests for custom headers
nickytonline May 24, 2022
8dcf91a
fix: fixed issue if there is no routes manifest
nickytonline May 24, 2022
3202075
test: added custom header E2E test
nickytonline May 24, 2022
8788060
chore: updated type for the route manifest
nickytonline May 25, 2022
33f135a
chore: updated loading of routes manifest
nickytonline May 25, 2022
0d49d3e
test: some tests are no longer async
nickytonline May 25, 2022
ebcb105
test: some more test for custom headers
nickytonline May 25, 2022
4491a1d
test: added some tests for custom headers
nickytonline May 27, 2022
deb269a
test: removed redundant test
nickytonline May 27, 2022
0be2f28
feat: added support for basePath in custom headers
nickytonline May 30, 2022
26c5b1b
test: tests for basePath in custom headers
nickytonline May 30, 2022
1cc6506
feat: implemented locale paths for custom headers in the Netlify conf…
nickytonline May 31, 2022
b6d01b8
test: removed tests that aren't actually testing the feature
nickytonline Jun 1, 2022
610fb45
test: move back to toEqual from toMatchInlineSnapshot
nickytonline Jun 1, 2022
373779c
feat: updated custom headers logic for headers generated in the Netli…
nickytonline Jun 2, 2022
8f28878
chore: cleaned up some dead code
nickytonline Jun 2, 2022
375cca7
fix: now generated headers in Netlify configuration take default loca…
nickytonline Jun 3, 2022
0d35e36
Update plugin/src/helpers/config.ts
nickytonline Jun 3, 2022
b50d92c
test: added onPostBuild test to check for header generation
nickytonline Jun 3, 2022
9781416
test: added more onPostBuild test to check for header generation
nickytonline Jun 3, 2022
0ca1f5e
chore: undoing a copy pasta error
nickytonline Jun 3, 2022
e3c8ed1
chore: package-lock.json was out of date for nx-next-monorepo-demo
nickytonline Jun 3, 2022
d175a92
chore: package lock updated again
nickytonline Jun 6, 2022
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
9 changes: 0 additions & 9 deletions cypress/integration/default/headers.spec.ts

This file was deleted.

18 changes: 18 additions & 0 deletions demos/default/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ module.exports = {
},
],
},
{
source: '/api/:path*',
headers: [
{
key: 'x-custom-api-header',
value: 'my custom api header value',
},
],
},
{
source: '/:path*',
headers: [
{
key: 'x-custom-header-for-everything',
value: 'my custom header for everything value',
},
],
},
]
},
trailingSlash: true,
Expand Down
98 changes: 96 additions & 2 deletions plugin/src/helpers/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import type { NetlifyConfig } from '@netlify/build'
import { readJSON, writeJSON } from 'fs-extra'
import type { Header } from 'next/dist/lib/load-custom-routes'
import type { NextConfigComplete } from 'next/dist/server/config-shared'
import { join, dirname, relative } from 'pathe'
import slash from 'slash'

import { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME } from '../constants'

import type { RoutesManifest } from './types'

const ROUTES_MANIFEST_FILE = 'routes-manifest.json'

type NetlifyHeaders = NetlifyConfig['headers']

export interface RequiredServerFiles {
version?: number
config?: NextConfigComplete
Expand All @@ -13,7 +21,10 @@ export interface RequiredServerFiles {
ignore?: string[]
}

export type NextConfig = Pick<RequiredServerFiles, 'appDir' | 'ignore'> & NextConfigComplete
export type NextConfig = Pick<RequiredServerFiles, 'appDir' | 'ignore'> &
NextConfigComplete & {
routesManifest?: RoutesManifest
}

const defaultFailBuild = (message: string, { error }): never => {
throw new Error(`${message}\n${error && error.stack}`)
Expand All @@ -30,7 +41,11 @@ export const getNextConfig = async function getNextConfig({
// @ts-ignore
return failBuild('Error loading your Next config')
}
return { ...config, appDir, ignore }

const routesManifest: RoutesManifest = await readJSON(join(publish, ROUTES_MANIFEST_FILE))

// If you need access to other manifest files, you can add them here as well
return { ...config, appDir, ignore, routesManifest }
} catch (error: unknown) {
return failBuild('Error loading your Next config', { error })
}
Expand Down Expand Up @@ -108,3 +123,82 @@ export const configureHandlerFunctions = ({ netlifyConfig, publish, ignore = []
})
})
}

interface BuildHeaderParams {
path: string
headers: Header['headers']
locale?: string
}

const buildHeader = (buildHeaderParams: BuildHeaderParams) => {
const { path, headers } = buildHeaderParams

return {
for: path,
values: headers.reduce((builtHeaders, { key, value }) => {
builtHeaders[key] = value

return builtHeaders
}, {}),
}
}

// Replace the pattern :path* at the end of a path with * since it's a named splat which the Netlify
// configuration does not support.
const sanitizePath = (path: string) => path.replace(/:[^*/]+\*$/, '*')

/**
* Persist NEXT.js custom headers to the Netlify configuration so the headers work with static files
* See {@link https://nextjs.org/docs/api-reference/next.config.js/headers} for more information on custom
* headers in Next.js
*
* @param nextConfig - The NextJS configuration
* @param netlifyHeaders - Existing headers that are already configured in the Netlify configuration
*/
export const generateCustomHeaders = (nextConfig: NextConfig, netlifyHeaders: NetlifyHeaders = []) => {
// The routesManifest is the contents of the routes-manifest.json file which will already contain the generated
// header paths which take locales and base path into account since this runs after the build. The routes-manifest.json
// file is located at demos/default/.next/routes-manifest.json once you've build the demo site.
const {
routesManifest: { headers: customHeaders = [] },
i18n,
} = nextConfig

// Skip `has` based custom headers as they have more complex dynamic conditional header logic
// that currently isn't supported by the Netlify configuration.
// Also, this type of dynamic header logic is most likely not for SSG pages.
for (const { source, headers, locale: localeEnabled } of customHeaders.filter((customHeader) => !customHeader.has)) {
// Explicitly checking false to make the check simpler.
// Locale specific paths are excluded only if localeEnabled is false. There is no true value for localeEnabled. It's either
// false or undefined, where undefined means it's true.
//
// Again, the routesManifest has already been generated taking locales into account, but the check is required
// so the paths can be properly set in the Netlify configuration.
const useLocale = i18n?.locales?.length > 0 && localeEnabled !== false

if (useLocale) {
const { locales } = i18n
const joinedLocales = locales.join('|')

/**
* converts e.g.
* /:nextInternalLocale(en|fr)/some-path
* to a path for each locale
* /en/some-path and /fr/some-path as well as /some-path (default locale)
*/
const defaultLocalePath = sanitizePath(source).replace(`/:nextInternalLocale(${joinedLocales})`, '')

netlifyHeaders.push(buildHeader({ path: defaultLocalePath, headers }))

for (const locale of locales) {
const path = sanitizePath(source).replace(`:nextInternalLocale(${joinedLocales})`, locale)

netlifyHeaders.push(buildHeader({ path, headers }))
}
} else {
const path = sanitizePath(source)

netlifyHeaders.push(buildHeader({ path, headers }))
}
}
}
9 changes: 8 additions & 1 deletion plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getRequiredServerFiles,
updateRequiredServerFiles,
configureHandlerFunctions,
generateCustomHeaders,
} from './helpers/config'
import { updateConfig, writeMiddleware } from './helpers/edge'
import { moveStaticPages, movePublicFiles, patchNextFiles, unpatchNextFiles } from './helpers/files'
Expand Down Expand Up @@ -136,6 +137,7 @@ const plugin: NetlifyPlugin = {
netlifyConfig: {
build: { publish },
redirects,
headers,
},
utils: {
status,
Expand All @@ -161,7 +163,12 @@ const plugin: NetlifyPlugin = {

await checkForOldFunctions({ functions })
await checkZipSize(join(FUNCTIONS_DIST, `${ODB_FUNCTION_NAME}.zip`))
const { basePath, appDir } = await getNextConfig({ publish, failBuild })
const nextConfig = await getNextConfig({ publish, failBuild })

const { basePath, appDir } = nextConfig

generateCustomHeaders(nextConfig, headers)

warnForProblematicUserRewrites({ basePath, redirects })
warnForRootRedirects({ appDir })
await unpatchNextFiles(basePath)
Expand Down
Loading