From 98d42cb4efd0b1551be2beb38702639c6897dc18 Mon Sep 17 00:00:00 2001 From: Mohamed Elkholy Date: Mon, 10 May 2021 00:14:19 +0200 Subject: [PATCH] fix(task:http-headers): add support for `asset` paths --- README.md | 2 +- src/index.ts | 2 +- src/tasks/http-headers/README.md | 13 ++--- src/tasks/http-headers/index.ts | 10 +--- src/tasks/http-headers/lib/builder.ts | 70 ++++++++++++++++----------- src/tasks/http-headers/options.ts | 5 -- 6 files changed, 54 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 453b462..9a49b01 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ The plugin comes with these tasks out of the box: - [Purgecss](https://github.com/mohatt/gatsby-plugin-postbuild/blob/master/src/tasks/purgecss) Optimizes HTML/CSS files by removing unused CSS selectors. - [HTTP Headers](https://github.com/mohatt/gatsby-plugin-postbuild/blob/master/src/tasks/http-headers) - Transforms HTML link tags with resource hints into HTTP Link headers using configuration file formats for different providers. + Automatically generates headers configuration file for different hosting providers - [Minify](https://github.com/mohatt/gatsby-plugin-postbuild/blob/master/src/tasks/minify) Minifies HTML inline scripts and styles using [terser](https://github.com/terser/terser) and [cssnano](https://github.com/cssnano/cssnano). diff --git a/src/index.ts b/src/index.ts index 905883a..40bf9f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ /** * Main public interfaces */ -export type { IPostbuildArg } from './postbuild' +export type { IPostbuildArg, IAssetsManifest } from './postbuild' export type { ITaskApi, ITaskApiOptions, ITaskApiEvents, ITaskOptions, IEvents } from './tasks' export type { Filesystem, IFilesystemReportMeta, IGlobOptions } from './filesystem' export type { IOptions, IExtensionOptions, IOptionProcessing, IOptionProcessingStrategy } from './options' diff --git a/src/tasks/http-headers/README.md b/src/tasks/http-headers/README.md index 229b88a..966f30d 100644 --- a/src/tasks/http-headers/README.md +++ b/src/tasks/http-headers/README.md @@ -1,11 +1,10 @@ # HTTP Headers -A Postbuild task that transforms HTML link tags with resource hints eg. `` into HTTP Link headers using configuration -file formats for different providers. Available providers are: +Automatically generates headers configuration file for different hosting providers. Available providers are: - [Netlify](https://docs.netlify.com/routing/headers/) - [Vercel](https://vercel.com/docs/configuration#project/headers) - [Firebase](https://firebase.google.com/docs/hosting/full-config#headers) -The task currently supports link tags with these rel types: `preconnect` `dns-prefetch` `preload` `prerender` `prefetch` +The task also transforms HTML link tags with resource hints eg. `` into HTTP Link headers. Currently supports link tags with these rel types: `preconnect` `dns-prefetch` `preload` `prerender` `prefetch` ## Usage ```typescript @@ -24,14 +23,16 @@ plugins: [ '[pages]': { // Add headers to all pages generated by Gatsby 'X-Robots-Tag': 'noindex', 'Link': [ - '; rel=preload; as=script', - '; rel=preload; as=image' + '<[asset:/static/logo.png]>; rel=preload; as=image', // Will be translated to ; rel=preload; as=image + '; rel=preload; as=script', ], }, '[assets]': {}, // Add headers to all immutable assets generated by Gatsby '[page-data]': {}, // Add headers to page-data/* files '[static]': {}, // Add headers to static/* files - '/custom-path/*': {} // Add custom path headers + '/path/to/unkown': {}, // Add custom path headers + '/about': {}, // Overwrite headers for specific page + '/static/logo.png': {}, // Overwrite headers for specific asset. Will be translated to `/static/logo-[hash].png` }, security: true, // Adds some basic security headers caching: true, // Adds essensial caching headers diff --git a/src/tasks/http-headers/index.ts b/src/tasks/http-headers/index.ts index aa0c6c6..a1662b0 100644 --- a/src/tasks/http-headers/index.ts +++ b/src/tasks/http-headers/index.ts @@ -11,8 +11,8 @@ let builder: Builder // @ts-expect-error export const events: ITaskApiEvents = { on: { - postbuild: ({ options, filesystem, gatsby }) => { - builder = new Builder(options, filesystem, gatsby.pathPrefix) + postbuild: ({ options, filesystem, gatsby, assets }) => { + builder = new Builder(options, assets, filesystem, gatsby.pathPrefix) }, shutdown: () => { return builder.build() @@ -45,12 +45,6 @@ export const events: ITaskApiEvents = { // Add the created link to the current page path builder.addPageLink(path, link) - - // If the link's href refers to a local path send it to the builder - // to decide whether it should be listed as an immutable cached asset - if (!/^\w+:\/\//.test(link.href)) { - builder.addCachedAsset(link) - } } // Remove the link node if specified in task options diff --git a/src/tasks/http-headers/lib/builder.ts b/src/tasks/http-headers/lib/builder.ts index c262801..fbea81c 100644 --- a/src/tasks/http-headers/lib/builder.ts +++ b/src/tasks/http-headers/lib/builder.ts @@ -1,7 +1,7 @@ import _ from 'lodash' import Link from './link' import Provider from './providers' -import type { Filesystem } from '@postbuild' +import type { Filesystem, IAssetsManifest } from '@postbuild' import type { IOptions, IHeadersMap, IPathHeadersMap } from '../options' export enum PathPlaceholder { @@ -53,18 +53,19 @@ const HEADERS_CACHING: IPathHeadersMap = { */ export default class Builder { headers: IPathHeadersMap - cachedAssets: string[] = [] pages: { [path: string]: Link[] } = {} readonly provider: Provider readonly options: IOptions + readonly assets: IAssetsManifest readonly fs: Filesystem readonly pathPrefix: string - constructor (options: IOptions, fs: Filesystem, pathPrefix: string) { + constructor (options: IOptions, assets: IAssetsManifest, fs: Filesystem, pathPrefix: string) { this.provider = Provider.factory(options, fs) this.options = options + this.assets = assets this.fs = fs this.pathPrefix = pathPrefix this.headers = { @@ -73,23 +74,6 @@ export default class Builder { } } - /** - * Adds a Link's href path as an immutable cached asset if - * it matches certain critiria - */ - addCachedAsset (link: Link): void { - if ( - !this.options.caching || - this.options.cachingAssetTypes.length === 0 || - link.type !== 'preload' || - link.href.indexOf('/page-data/') === 0 || - link.href.indexOf('/static/') === 0 - ) return - if (this.options.cachingAssetTypes.includes(link.attrs.as)) { - this.cachedAssets.push(link.href) - } - } - /** * Adds a Link to a page path */ @@ -106,6 +90,32 @@ export default class Builder { : href } + processHeaderValue (value: string[]): string[] + processHeaderValue (value: string): string + processHeaderValue (value: string|string[]): string|string[] { + if (Array.isArray(value)) { + return value.map(this.processHeaderValue.bind(this)) + } + + const matches = value.match(/\[(\w+):([^\]]+)]/) + if (matches?.[1] != null && matches[2]) { + const placeholder = matches[1].toLowerCase() + const placeholderValue = matches[2] + + if (placeholder === 'asset') { + const asset = placeholderValue.slice(1) + if (this.assets.has(asset)) { + return value.replace(matches[0], '/' + (this.assets.get(asset) as string)) + } + throw new Error(`Unable to find asset "${placeholderValue}" in value "${value}"`) + } + + throw new Error(`Invalid dynamic placeholder "${placeholder}" in value "${value}"`) + } + + return value + } + protected isPathPlaceholder (path: string): path is PathPlaceholder { return /^\[[^\]]+]$/.test(path) } @@ -137,7 +147,7 @@ export default class Builder { `Headers with multi-value support are: ${multiValueHeaders.join(', ')}` ) } - dest[path][name] = value + dest[path][name] = this.processHeaderValue(value as any) } } @@ -169,13 +179,19 @@ export default class Builder { this.headers[path] = userHeaders[path] } - if (this.options.caching && this.cachedAssets.length) { - for (const asset of this.cachedAssets) { - this.headers[asset] = this.mergeHeaders({ - ...this.headers[PathPlaceholder.Assets] - }, this.headers[asset]) + this.assets.forEach((hashed, original) => { + hashed = `/${hashed}` + original = `/${original}` + if (hashed.startsWith('/static/') || original === '/styles.css') { + return } - } + this.headers[hashed] = this.mergeHeaders({ + ...this.headers[PathPlaceholder.Assets] + }, this.headers[original]) + if (original in this.headers) { + delete this.headers[original] + } + }) for (const path in this.pages) { // @todo: Validate the return value of a user-defined callback diff --git a/src/tasks/http-headers/options.ts b/src/tasks/http-headers/options.ts index 3c8501d..a200226 100644 --- a/src/tasks/http-headers/options.ts +++ b/src/tasks/http-headers/options.ts @@ -38,7 +38,6 @@ export type IOptions = ITaskOptions & { headers: IPathHeadersMap security: boolean caching: boolean - cachingAssetTypes: string[] transformPathLinks: (links: Link[], path: string) => Link[] removeLinkTags: boolean } @@ -54,7 +53,6 @@ export const options: ITaskApiOptions = { headers: {}, security: true, caching: true, - cachingAssetTypes: ['image', 'script', 'style', 'font'], transformPathLinks: links => links, removeLinkTags: true }, @@ -79,9 +77,6 @@ export const options: ITaskApiOptions = { .description('Adds useful security headers.'), caching: joi.boolean() .description('Adds useful caching headers to immutable asset paths.'), - cachingAssetTypes: joi.array() - .items(nonEmptyString) - .description('Specifies the types of assets that are considered immutable.'), transformPathLinks: joi.function().maxArity(2) .description('Callback for manipulating links under each path.'), removeLinkTags: joi.boolean()