cache-headers
Advanced tools
Comparing version 0.0.5 to 1.0.0-hella.1
@@ -0,26 +1,293 @@ | ||
'use strict'; | ||
Object.defineProperty(exports, '__esModule', { value: true }); | ||
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } | ||
var url = _interopDefault(require('fast-url-parser')); | ||
var globject = _interopDefault(require('globject')); | ||
var slasher = _interopDefault(require('glob-slasher')); | ||
var isEmpty = _interopDefault(require('lodash.isempty')); | ||
var moment_src_lib_moment_moment = require('moment/src/lib/moment/moment'); | ||
var moment_src_lib_locale_locale = require('moment/src/lib/locale/locale'); | ||
var moment_src_lib_format_format = require('moment/src/lib/format/format'); | ||
var regular = _interopDefault(require('regular')); | ||
/** | ||
* Maps to keys in the different cache methods | ||
* @type {Object} | ||
*/ | ||
var KEY_SURROGATE_CONTROL = 'maxAge'; | ||
/** | ||
* @ignore | ||
* User: daletan | ||
* Date: 12/19/15 | ||
* Time: 8:49 PM | ||
* Time: 10:25 PM | ||
* Copyright 1stdibs.com, Inc. 2015. All Rights Reserved. | ||
*/ | ||
'use strict'; | ||
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; | ||
var url = require('fast-url-parser'); | ||
var globject = require('globject'); | ||
var slasher = require('glob-slasher'); | ||
var isEmpty = require('lodash.isempty'); | ||
// Mon, 01, Jan 2016, 00:00:00 UTC | ||
var defaultDateFormat = 'ddd, DD MMM YYYY HH:mm:ss z'; | ||
var _require = require('./cacheControl'); | ||
/** | ||
* @param {*} val The value to check if it is an actual object. Arrays are not considered objects in this case | ||
* @return {boolean} | ||
*/ | ||
function isNonEmptyObject(val) { | ||
return !Array.isArray(val) && (typeof val === 'undefined' ? 'undefined' : _typeof(val)) === 'object' && !isEmpty(val); | ||
} | ||
var headerTypes = _require.headerTypes; | ||
var generate = _require.generate; | ||
/** | ||
* | ||
* @param {*} val The value to check if it is like a number ie. 100 and "100" would return true | ||
* @return {boolean} | ||
*/ | ||
function isNumberLike(val) { | ||
return regular.number.test(val); | ||
} | ||
var additionalHeaders = require('./additionalHeaders'); | ||
var utils = require('./utils'); | ||
var timeValues = require('./timeValues'); | ||
/** | ||
* @param {object} [time] Date object | ||
* @return {object} moment object in UTC format | ||
*/ | ||
function getUtcTime() { | ||
var time = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : new Date(); | ||
return moment_src_lib_moment_moment.createUTC(time); | ||
} | ||
/** | ||
* Format a UTC Date value | ||
* @param {object} options | ||
* @param {number} [options.date=now()] UTC time format. A JavaScript date must be passed in, not a moment date object | ||
* @param {string} [options.dateFormat=defaultDateFormat] Primarily used for testing | ||
* @return {string} header date string in GMT format | ||
*/ | ||
function formatDate() { | ||
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
var _options$date = options.date, | ||
date = _options$date === undefined ? moment_src_lib_moment_moment.now() : _options$date, | ||
_options$dateFormat = options.dateFormat, | ||
dateFormat = _options$dateFormat === undefined ? defaultDateFormat : _options$dateFormat; | ||
// keeping this here if we want to | ||
// support setting locales in the future | ||
var locale = { key: undefined, config: undefined }; | ||
// need to set locale before formatting | ||
moment_src_lib_locale_locale.updateLocale(locale.key, locale.config); | ||
var formatted = moment_src_lib_format_format.formatMoment(getUtcTime(date), dateFormat); | ||
// do browsers require using GMT instead of UTC? | ||
return formatted.replace('UTC', 'GMT'); | ||
} | ||
/** | ||
* @ignore | ||
* User: daletan | ||
* Date: 12/24/15 | ||
* Time: 9:51 AM | ||
* Copyright 1stdibs.com, Inc. 2015. All Rights Reserved. | ||
*/ | ||
// time set in seconds | ||
var ONE_MINUTE = 60; | ||
var TEN_MINUTES = 600; | ||
var HALF_HOUR = 1800; | ||
var ONE_HOUR = 3600; | ||
var ONE_DAY = 86400; | ||
var ONE_WEEK = 604800; | ||
var ONE_MONTH = 2592000; // 30 days | ||
var ONE_YEAR = 31536000; | ||
var timeValues = Object.freeze({ | ||
ONE_MINUTE: ONE_MINUTE, | ||
TEN_MINUTES: TEN_MINUTES, | ||
HALF_HOUR: HALF_HOUR, | ||
ONE_HOUR: ONE_HOUR, | ||
ONE_DAY: ONE_DAY, | ||
ONE_WEEK: ONE_WEEK, | ||
ONE_MONTH: ONE_MONTH, | ||
ONE_YEAR: ONE_YEAR | ||
}); | ||
var PRIVATE_VALUE = 'private'; | ||
var NO_CACHE_NO_STORE = 'no-cache, no-store, must-revalidate'; | ||
// TODO: add parser that can understand string input for header values | ||
// ie: 'private, max-age=300 | ||
/** | ||
* If a number or number-like, return the value as a number | ||
* If a string, and it is in a the `timeValues` map, return that time value | ||
* @param {number|string|*} value | ||
* @return {number|string} | ||
*/ | ||
function getTimeValue(value) { | ||
if (isNumberLike(value)) { | ||
return Number(value); | ||
} else if (typeof value === 'string') { | ||
// checks for values listed in ./timeValues | ||
value = value.toUpperCase(); | ||
if (timeValues[value]) { | ||
return timeValues[value]; | ||
} | ||
} | ||
// if no valid value, always return a number | ||
console.warn('no cached value found. ' + value + ' was passed in. returning 0'); | ||
return 0; | ||
} | ||
/** | ||
* | ||
* @param {boolean} setPrivate Used for user-specific pages | ||
* @returns {*} | ||
*/ | ||
function generateBrowserCacheHeader() { | ||
var setPrivate = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; | ||
if (setPrivate) { | ||
return PRIVATE_VALUE + ', ' + NO_CACHE_NO_STORE; | ||
} | ||
return NO_CACHE_NO_STORE; | ||
} | ||
/** | ||
* @param {string|number} maxAge | ||
* @returns {string} | ||
*/ | ||
function generateMaxAgeHeader(maxAge) { | ||
return 'max-age=' + getTimeValue(maxAge); | ||
} | ||
/** | ||
* @param {string|number} maxAge | ||
* @returns {string} | ||
*/ | ||
function generateStaleRevalidateCacheHeader(maxAge) { | ||
return 'stale-while-revalidate=' + getTimeValue(maxAge); | ||
} | ||
/** | ||
* @param {string|number} maxAge | ||
* @returns {string} | ||
*/ | ||
function generateStaleError(maxAge) { | ||
return 'stale-if-error=' + getTimeValue(maxAge); | ||
} | ||
/** | ||
* All options can use a string value. See {@link module:timeValues} for all available values | ||
* Returns the cache header name as the key for res.set() | ||
* @memberof module:cacheControl | ||
* @alias generate | ||
* @param {object} [options] Caching options | ||
* @param {number|string} [options.staleRevalidate=false] Time when to refresh the content in the background | ||
* @param {number|string} [options.staleIfError=false] Time to allow for serving cache when there is an error from a back-end service | ||
* @param {boolean} [options.setPrivate=false] use the `private` cache header value for user-specific pages | ||
* @returns {{Cache-Control: string}} | ||
*/ | ||
function generateCacheControl() { | ||
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
var _options$staleRevalid = options.staleRevalidate, | ||
staleRevalidate = _options$staleRevalid === undefined ? false : _options$staleRevalid, | ||
_options$staleError = options.staleError, | ||
staleError = _options$staleError === undefined ? false : _options$staleError, | ||
_options$setPrivate = options.setPrivate, | ||
setPrivate = _options$setPrivate === undefined ? false : _options$setPrivate; | ||
var cacheHeaders = [generateBrowserCacheHeader(setPrivate)]; | ||
if (typeof staleRevalidate !== 'boolean') { | ||
cacheHeaders.push(generateStaleRevalidateCacheHeader(staleRevalidate)); | ||
} | ||
if (typeof staleError !== 'boolean') { | ||
cacheHeaders.push(generateStaleError(staleError)); | ||
} | ||
return { | ||
'Cache-Control': '' + cacheHeaders.join(', ') | ||
}; | ||
} | ||
/** | ||
* Returns the cache header name as the key for res.set() | ||
* @param {object} options | ||
* @param {number|string} [options.maxAge=timeValues.TEN_MINUTES] The browser cache length | ||
* @param {boolean} [options.setPrivate=false] Set the max-age value to 0 for user-specific pages | ||
* @returns {{Surrogate-Control: string}} | ||
*/ | ||
function generateSurrogateControl() { | ||
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
var _options$setPrivate2 = options.setPrivate, | ||
setPrivate = _options$setPrivate2 === undefined ? false : _options$setPrivate2; | ||
var maxAge = options.maxAge; | ||
if (maxAge === undefined) { | ||
// only default this if maxAge is undefined | ||
// since 0 can be passed in | ||
maxAge = TEN_MINUTES; | ||
} | ||
// always force time to 0 if setPrivate - usually user-specific content | ||
var time = setPrivate ? 0 : maxAge; | ||
return { | ||
'Surrogate-Control': generateMaxAgeHeader(time) | ||
}; | ||
} | ||
function generatePragmaHeader() { | ||
return { | ||
'Pragma': 'no-cache' | ||
}; | ||
} | ||
function generateExpiresHeader() { | ||
return { | ||
'Expires': 0 | ||
}; | ||
} | ||
function generateLastModifiedHeader() { | ||
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
var _options$lastModified = options.lastModified, | ||
lastModified = _options$lastModified === undefined ? false : _options$lastModified; | ||
if (!lastModified) { | ||
lastModified = formatDate(); | ||
} | ||
return { | ||
'Last-Modified': lastModified | ||
}; | ||
} | ||
/** | ||
* @see `generateCacheControl`, `generateSurrogateControl`, and `generateLastModifiedHeader` for options | ||
* @param options | ||
* @returns {*[]} | ||
*/ | ||
function generateAllCacheHeaders() { | ||
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
return Object.assign({}, generateCacheControl(options), generateSurrogateControl(options), generatePragmaHeader(), generateExpiresHeader(), generateLastModifiedHeader(options)); | ||
} | ||
/** | ||
* @ignore | ||
* User: daletan | ||
* Date: 12/19/15 | ||
* Time: 8:49 PM | ||
* Copyright 1stdibs.com, Inc. 2015. All Rights Reserved. | ||
*/ | ||
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } | ||
/** | ||
* This will either set a specific header or defer to using express' res.set() functionality | ||
@@ -35,4 +302,4 @@ * {{@link http://expressjs.com/en/api.html#res.set}} | ||
if (headerData.name && headerData.value) { | ||
res.setHeader(headerData.name, headerData.value); | ||
} else if (utils.isTrueObject(headerData)) { | ||
res.set(headerData.name, headerData.value); | ||
} else if (isNonEmptyObject(headerData)) { | ||
res.set(headerData); | ||
@@ -42,13 +309,17 @@ } | ||
/** | ||
* @param {object<array>} headers | ||
* @param {string} headers[].name The header name | ||
* @param {string} headers[].value The header value | ||
* @returns {function(*, *=, *)} | ||
*/ | ||
function setAdditionalHeaders() { | ||
var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; | ||
var headers = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; | ||
return function (req, res, next) { | ||
Object.keys(options).forEach(function (key) { | ||
if (typeof additionalHeaders[key] === 'function') { | ||
var option = options[key]; | ||
var headerData = additionalHeaders[key](option); | ||
setHeader(res, headerData); | ||
} | ||
}); | ||
if (Array.isArray(headers) && headers.length) { | ||
headers.map(function (headerData) { | ||
return setHeader(res, headerData); | ||
}); | ||
} | ||
next(); | ||
@@ -67,7 +338,6 @@ }; | ||
function middleware(config) { | ||
var _ref = config || {}; | ||
var _ref = config || {}, | ||
cacheSettings = _ref.cacheSettings, | ||
paths = _ref.paths; | ||
var cacheSettings = _ref.cacheSettings; | ||
var paths = _ref.paths; | ||
return function (req, res, next) { | ||
@@ -77,21 +347,23 @@ | ||
var cacheValues = globject(slasher(paths || {}, { value: false })); | ||
var cacheValue = cacheValues(slasher(pathname)); | ||
var values = cacheValues(slasher(pathname)); | ||
if (utils.isTrueObject(cacheSettings)) { | ||
if (isNonEmptyObject(cacheSettings)) { | ||
// override default cacheValue settings | ||
cacheValue = generate(cacheSettings).value; | ||
} else if (utils.isTrueObject(cacheValue)) { | ||
cacheValue = generate(cacheValue).value; | ||
} else if (cacheValue === false) { | ||
cacheValue = generate({ maxAge: 0, sMaxAge: 0, setNoCache: true }).value; | ||
} else if (utils.isNumberLike(cacheValue)) { | ||
values = generateAllCacheHeaders(cacheSettings); | ||
} else if (isNonEmptyObject(values)) { | ||
values = generateAllCacheHeaders(values); | ||
} else if (values === false) { | ||
var _generateAllCacheHead; | ||
values = generateAllCacheHeaders((_generateAllCacheHead = {}, _defineProperty(_generateAllCacheHead, KEY_SURROGATE_CONTROL, 0), _defineProperty(_generateAllCacheHead, 'setPrivate', true), _generateAllCacheHead)); | ||
} else if (isNumberLike(values)) { | ||
// catch `0` before !cacheValue check | ||
// make sure to convert value to actual number | ||
cacheValue = Number(cacheValue); | ||
cacheValue = generate({ maxAge: cacheValue, sMaxAge: cacheValue }).value; | ||
} else if (!cacheValue || isEmpty(cacheValue)) { | ||
cacheValue = generate().value; | ||
values = generateAllCacheHeaders(_defineProperty({}, KEY_SURROGATE_CONTROL, Number(values))); | ||
} else if (!values || isEmpty(values)) { | ||
values = generateAllCacheHeaders(); | ||
} | ||
setHeader(res, { name: 'Cache-Control', value: cacheValue }); | ||
setHeader(res, values); | ||
next(); | ||
@@ -101,10 +373,11 @@ }; | ||
/** | ||
* @module index | ||
* @type {object} | ||
*/ | ||
module.exports = Object.assign({ | ||
headerTypes: headerTypes, | ||
setAdditionalHeaders: setAdditionalHeaders, | ||
middleware: middleware | ||
}, timeValues); | ||
exports.setAdditionalHeaders = setAdditionalHeaders; | ||
exports.middleware = middleware; | ||
exports.ONE_MINUTE = ONE_MINUTE; | ||
exports.TEN_MINUTES = TEN_MINUTES; | ||
exports.HALF_HOUR = HALF_HOUR; | ||
exports.ONE_HOUR = ONE_HOUR; | ||
exports.ONE_DAY = ONE_DAY; | ||
exports.ONE_WEEK = ONE_WEEK; | ||
exports.ONE_MONTH = ONE_MONTH; | ||
exports.ONE_YEAR = ONE_YEAR; |
{ | ||
"name": "cache-headers", | ||
"version": "0.0.5", | ||
"version": "1.0.0-hella.1", | ||
"description": "Generate browser and cdn cache header values", | ||
"main": "dist/index.js", | ||
"scripts": { | ||
"test": "./node_modules/.bin/mocha --reporter spec", | ||
"compile": "./node_modules/.bin/babel src -d dist", | ||
"gitlint": "node_modules/.bin/dibslint --git --warnings -e", | ||
"docs": "./node_modules/.bin/mr-doc -s src/ -o docs -n 'Cache Headers'", | ||
"updatedocs": "rm -rf ./docs && npm run docs && git add ./docs && git commit -n -m 'updated docs v${npm show . version}'", | ||
"test": "NODE_ENV=test jest", | ||
"compile": "rm -rf ./dist && NODE_ENV=build rollup -c", | ||
"docs": "rm -rf ./docs && esdoc -c ./esdoc.json", | ||
"lint": "dibslint --root=./src -e --git --warnings", | ||
"precommit": "dibslint --git --warnings -e", | ||
"prepublish": "yarn run compile", | ||
"updatedocs": "rm -rf ./docs && yarn run docs && git add ./docs && git commit -n -m 'updated docs v${npm show . version}'", | ||
"generatedist": "./generate.sh compile dist", | ||
"generatedocs": "./generate.sh docs docs", | ||
"preversion": "npm run gitlint && npm test && npm run generatedocs && npm run generatedist" | ||
"preversion": "yarn run lint && yarn test && yarn run generatedist" | ||
}, | ||
"pre-commit": { | ||
"run": [ | ||
"gitlint" | ||
"jest": { | ||
"collectCoverage": true, | ||
"coverageDirectory": "<rootDir>/__coverage__", | ||
"coverageThreshold": { | ||
"global": { | ||
"branches": 83, | ||
"functions": 100, | ||
"lines": 100, | ||
"statements": 100 | ||
} | ||
}, | ||
"setupFiles": [ | ||
"<rootDir>/__tests__/testHelper.js" | ||
], | ||
"testPathIgnorePatterns": [ | ||
"/node_modules/", | ||
"<rootDir>/__tests__/registerBabel.js" | ||
], | ||
"transformIgnorePatterns": [ | ||
"!<rootDir>/node_modules/moment/src" | ||
] | ||
@@ -30,3 +49,6 @@ }, | ||
], | ||
"author": "dale tan <dale@1stdibs.com> (https://1stdibs.com)", | ||
"author": "1stdibs, Inc. (https://1stdibs.com)", | ||
"contributors": [ | ||
"dale tan <dale@1stdibs.com> (https://1stdibs.com)" | ||
], | ||
"repository": { | ||
@@ -41,11 +63,23 @@ "type": "git", | ||
"babel-core": "^6.1.2", | ||
"babel-plugin-istanbul": "^2.0.3", | ||
"babel-preset-es2015": "^6.1.2", | ||
"core-js": "^1.2.6", | ||
"del": "^2.2.0", | ||
"dibslint": "^1.4.2", | ||
"express": "^4.13.3", | ||
"fast-url-parser": "^1.1.3", | ||
"glob-slasher": "^1.0.1", | ||
"globject": "^1.0.1", | ||
"gulp": "^3.9.0", | ||
"gulp-babel": "^6.1.1", | ||
"husky": "^0.11.9", | ||
"jest": "^19.0.2", | ||
"lodash.isempty": "^3.0.4", | ||
"mocha": "^2.3.3", | ||
"moment": "^2.15.2", | ||
"mr-doc": "^3.0.7", | ||
"pre-commit": "^1.1.2", | ||
"regular": "^0.1.6", | ||
"rollup": "^0.36.3", | ||
"rollup-plugin-babel": "^2.6.1", | ||
"rollup-plugin-commonjs": "^5.0.5", | ||
"supertest": "^1.1.0" | ||
@@ -58,8 +92,6 @@ }, | ||
"globject": "^1.0.1", | ||
"lodash.clone": "^3.0.3", | ||
"lodash.isempty": "^3.0.4", | ||
"lodash.isnumber": "^3.0.1", | ||
"moment": "^2.10.6", | ||
"moment": "^2.15.2", | ||
"regular": "^0.1.6" | ||
} | ||
} |
@@ -1,35 +0,9 @@ | ||
/** | ||
* @ignore | ||
* User: daletan | ||
* Date: 12/19/15 | ||
* Time: 10:23 PM | ||
* Copyright 1stdibs.com, Inc. 2015. All Rights Reserved. | ||
*/ | ||
import {isNumberLike, formatDate} from './utils'; | ||
import * as timeValues from './timeValues'; | ||
'use strict'; | ||
export const PRIVATE_VALUE = 'private'; | ||
export const NO_CACHE_NO_STORE = 'no-cache, no-store, must-revalidate'; | ||
const utils = require('./utils'); | ||
const timeValues = require('./timeValues'); | ||
/** | ||
* @memberof module:cacheControl | ||
* @type {Object} | ||
*/ | ||
const headerTypes = Object.freeze({ | ||
browser: { | ||
varName: 'maxAge', | ||
value: 'max-age' | ||
}, | ||
cdn: { | ||
varName: 'sMaxAge', | ||
value: 's-maxage' | ||
}, | ||
staleRevalidate: { | ||
varName: 'staleRevalidate', | ||
value: 'stale-while-revalidate' | ||
}, | ||
staleError: { | ||
varName: 'staleError', | ||
value: 'stale-if-error' | ||
} | ||
}); | ||
// TODO: add parser that can understand string input for header values | ||
// ie: 'private, max-age=300 | ||
@@ -39,34 +13,54 @@ /** | ||
* If a string, and it is in a the `timeValues` map, return that time value | ||
* @private | ||
* @param {number|string} value | ||
* @param {number|string|*} value | ||
* @return {number|string} | ||
*/ | ||
function getTimeValue(value) { | ||
if (utils.isNumberLike(value)) { | ||
value = Number(value); | ||
if (isNumberLike(value)) { | ||
return Number(value); | ||
} else if (typeof value === 'string') { | ||
// checks for values listed in ./timeValues | ||
value = value.toUpperCase(); | ||
if (!timeValues[value]) { | ||
console.warn(`An invalid time value was passed in, received '${value}'. Returning a value of 10`); | ||
return 10; | ||
if (timeValues[value]) { | ||
return timeValues[value]; | ||
} | ||
return timeValues[value]; | ||
} | ||
return value; | ||
// if no valid value, always return a number | ||
console.warn(`no cached value found. ${value} was passed in. returning 0`); | ||
return 0; | ||
} | ||
function generateBrowserCacheHeader(maxAge) { | ||
return `${headerTypes.browser.value}=${getTimeValue(maxAge)}`; | ||
/** | ||
* | ||
* @param {boolean} setPrivate Used for user-specific pages | ||
* @returns {*} | ||
*/ | ||
function generateBrowserCacheHeader(setPrivate = false) { | ||
if (setPrivate) { | ||
return `${PRIVATE_VALUE}, ${NO_CACHE_NO_STORE}`; | ||
} | ||
return NO_CACHE_NO_STORE; | ||
} | ||
function generateCdnCacheHeader(maxAge) { | ||
return `${headerTypes.cdn.value}=${getTimeValue(maxAge)}`; | ||
/** | ||
* @param {string|number} maxAge | ||
* @returns {string} | ||
*/ | ||
function generateMaxAgeHeader(maxAge) { | ||
return `max-age=${getTimeValue(maxAge)}`; | ||
} | ||
/** | ||
* @param {string|number} maxAge | ||
* @returns {string} | ||
*/ | ||
function generateStaleRevalidateCacheHeader(maxAge) { | ||
return `${headerTypes.staleRevalidate.value}=${getTimeValue(maxAge)}`; | ||
return `stale-while-revalidate=${getTimeValue(maxAge)}`; | ||
} | ||
/** | ||
* @param {string|number} maxAge | ||
* @returns {string} | ||
*/ | ||
function generateStaleError(maxAge) { | ||
return `${headerTypes.staleError.value}=${getTimeValue(maxAge)}`; | ||
return `stale-if-error=${getTimeValue(maxAge)}`; | ||
} | ||
@@ -76,58 +70,95 @@ | ||
* All options can use a string value. See {@link module:timeValues} for all available values | ||
* Returns the cache header name as the key for res.set() | ||
* @memberof module:cacheControl | ||
* @alias generate | ||
* @param {object} [options] Caching options | ||
* @param {number|string} [options.maxAge=timeValues.TEN_MINUTES] The browser cache length | ||
* @param {number|string} [options.sMaxAge=false] The cdn cache length | ||
* @param {number|string} [options.staleRevalidate=false] Time when to refresh the content in the background | ||
* @param {number|string} [options.staleError=false] Time to allow for serving cache when there is an error from a back-end service | ||
* @return {{name: string, value: string}} | ||
* @param {number|string} [options.staleIfError=false] Time to allow for serving cache when there is an error from a back-end service | ||
* @param {boolean} [options.setPrivate=false] use the `private` cache header value for user-specific pages | ||
* @returns {{Cache-Control: string}} | ||
*/ | ||
function generateCacheControl(options) { | ||
const { maxAge = timeValues.TEN_MINUTES, | ||
sMaxAge = false, | ||
function generateCacheControl(options = {}) { | ||
const { | ||
staleRevalidate = false, | ||
staleError = false, | ||
setNoCache = false, | ||
// private should only be used for user-specific pages. ie account pages | ||
// This will not be set if the sMaxAge is set | ||
setPrivate = false } = options || {}; | ||
let cacheHeaders; | ||
setPrivate = false } = options; | ||
const cacheHeaders = [generateBrowserCacheHeader(setPrivate)]; | ||
if (setNoCache) { | ||
cacheHeaders = ['no-cache', generateBrowserCacheHeader(0)]; | ||
} else { | ||
cacheHeaders = [generateBrowserCacheHeader(maxAge)]; | ||
if (typeof staleRevalidate !== 'boolean') { | ||
cacheHeaders.push(generateStaleRevalidateCacheHeader(staleRevalidate)); | ||
} | ||
if (sMaxAge) { | ||
cacheHeaders.push(generateCdnCacheHeader(sMaxAge)); | ||
} | ||
if (typeof staleError !== 'boolean') { | ||
cacheHeaders.push(generateStaleError(staleError)); | ||
} | ||
if (staleRevalidate) { | ||
cacheHeaders.push(generateStaleRevalidateCacheHeader(staleRevalidate)); | ||
} | ||
return { | ||
'Cache-Control': `${cacheHeaders.join(', ')}` | ||
}; | ||
} | ||
if (staleError) { | ||
cacheHeaders.push(generateStaleError(staleError)); | ||
} | ||
/** | ||
* Returns the cache header name as the key for res.set() | ||
* @param {object} options | ||
* @param {number|string} [options.maxAge=timeValues.TEN_MINUTES] The browser cache length | ||
* @param {boolean} [options.setPrivate=false] Set the max-age value to 0 for user-specific pages | ||
* @returns {{Surrogate-Control: string}} | ||
*/ | ||
function generateSurrogateControl(options = {}) { | ||
const { | ||
// private should only be used for user-specific pages. ie account pages | ||
setPrivate = false | ||
} = options; | ||
let { maxAge } = options; | ||
if (maxAge === undefined) { | ||
// only default this if maxAge is undefined | ||
// since 0 can be passed in | ||
maxAge = timeValues.TEN_MINUTES; | ||
} | ||
// always force time to 0 if setPrivate - usually user-specific content | ||
const time = setPrivate ? 0 : maxAge; | ||
if (setPrivate && !sMaxAge) { | ||
cacheHeaders.unshift('private'); | ||
} | ||
return { | ||
'Surrogate-Control': generateMaxAgeHeader(time) | ||
}; | ||
} | ||
function generatePragmaHeader() { | ||
return { | ||
name: 'Cache-Control', | ||
value: `${cacheHeaders.join(', ')}` | ||
'Pragma': 'no-cache' | ||
}; | ||
} | ||
function generateExpiresHeader() { | ||
return { | ||
'Expires': 0 | ||
}; | ||
} | ||
function generateLastModifiedHeader(options = {}) { | ||
let {lastModified = false} = options; | ||
if (!lastModified) { | ||
lastModified = formatDate(); | ||
} | ||
return { | ||
'Last-Modified': lastModified | ||
}; | ||
} | ||
/** | ||
* @module cacheControl | ||
* @type {{generate: generateCacheControl, headerTypes: Object}} | ||
* @see `generateCacheControl`, `generateSurrogateControl`, and `generateLastModifiedHeader` for options | ||
* @param options | ||
* @returns {*[]} | ||
*/ | ||
module.exports = { | ||
generate: generateCacheControl, | ||
headerTypes | ||
}; | ||
export function generateAllCacheHeaders(options = {}) { | ||
return Object.assign( | ||
{}, | ||
generateCacheControl(options), | ||
generateSurrogateControl(options), | ||
generatePragmaHeader(), | ||
generateExpiresHeader(), | ||
generateLastModifiedHeader(options) | ||
); | ||
} |
@@ -11,11 +11,14 @@ /** | ||
const url = require('fast-url-parser'); | ||
const globject = require('globject'); | ||
const slasher = require('glob-slasher'); | ||
const isEmpty = require('lodash.isempty'); | ||
const { headerTypes, generate } = require('./cacheControl'); | ||
const additionalHeaders = require('./additionalHeaders'); | ||
const utils = require('./utils'); | ||
const timeValues = require('./timeValues'); | ||
import url from 'fast-url-parser'; | ||
import globject from 'globject'; | ||
import slasher from 'glob-slasher'; | ||
import isEmpty from 'lodash.isempty'; | ||
import { | ||
KEY_SURROGATE_CONTROL | ||
} from './headerTypes'; | ||
import { generateAllCacheHeaders } from './cacheControl'; | ||
import { isNumberLike, isNonEmptyObject } from './utils'; | ||
export * from './timeValues'; | ||
/** | ||
@@ -31,4 +34,4 @@ * This will either set a specific header or defer to using express' res.set() functionality | ||
if (headerData.name && headerData.value) { | ||
res.setHeader(headerData.name, headerData.value); | ||
} else if (utils.isTrueObject(headerData)) { | ||
res.set(headerData.name, headerData.value); | ||
} else if (isNonEmptyObject(headerData)) { | ||
res.set(headerData); | ||
@@ -38,11 +41,13 @@ } | ||
function setAdditionalHeaders(options = {}) { | ||
/** | ||
* @param {object<array>} headers | ||
* @param {string} headers[].name The header name | ||
* @param {string} headers[].value The header value | ||
* @returns {function(*, *=, *)} | ||
*/ | ||
export function setAdditionalHeaders(headers = []) { | ||
return (req, res, next) => { | ||
Object.keys(options).forEach(key => { | ||
if (typeof additionalHeaders[key] === 'function') { | ||
const option = options[key]; | ||
const headerData = additionalHeaders[key](option); | ||
setHeader(res, headerData); | ||
} | ||
}); | ||
if (Array.isArray(headers) && headers.length) { | ||
headers.map(headerData => setHeader(res, headerData)); | ||
} | ||
next(); | ||
@@ -60,3 +65,3 @@ }; | ||
*/ | ||
function middleware(config) { | ||
export function middleware(config) { | ||
@@ -69,33 +74,26 @@ const { cacheSettings, paths } = config || {}; | ||
const cacheValues = globject(slasher(paths || {}, {value: false})); | ||
let cacheValue = cacheValues(slasher(pathname)); | ||
let values = cacheValues(slasher(pathname)); | ||
if (utils.isTrueObject(cacheSettings)) { | ||
if (isNonEmptyObject(cacheSettings)) { | ||
// override default cacheValue settings | ||
cacheValue = generate(cacheSettings).value; | ||
} else if (utils.isTrueObject(cacheValue)) { | ||
cacheValue = generate(cacheValue).value; | ||
} else if (cacheValue === false) { | ||
cacheValue = generate({ maxAge: 0, sMaxAge: 0, setNoCache: true }).value; | ||
} else if (utils.isNumberLike(cacheValue)) { | ||
values = generateAllCacheHeaders(cacheSettings); | ||
} else if (isNonEmptyObject(values)) { | ||
values = generateAllCacheHeaders(values); | ||
} else if (values === false) { | ||
values = generateAllCacheHeaders({ | ||
[KEY_SURROGATE_CONTROL]: 0, | ||
setPrivate: true | ||
}); | ||
} else if (isNumberLike(values)) { | ||
// catch `0` before !cacheValue check | ||
// make sure to convert value to actual number | ||
cacheValue = Number(cacheValue); | ||
cacheValue = generate({ maxAge: cacheValue, sMaxAge: cacheValue }).value; | ||
} else if (!cacheValue || isEmpty(cacheValue)) { | ||
cacheValue = generate().value; | ||
values = generateAllCacheHeaders({ [KEY_SURROGATE_CONTROL]: Number(values) }); | ||
} else if (!values || isEmpty(values)) { | ||
values = generateAllCacheHeaders(); | ||
} | ||
setHeader(res, { name: 'Cache-Control', value: cacheValue }); | ||
setHeader(res, values); | ||
next(); | ||
}; | ||
} | ||
/** | ||
* @module index | ||
* @type {object} | ||
*/ | ||
module.exports = Object.assign({ | ||
headerTypes, | ||
setAdditionalHeaders, | ||
middleware | ||
}, timeValues); |
@@ -14,2 +14,3 @@ /** | ||
const TEN_MINUTES = 600; | ||
const HALF_HOUR = 1800; | ||
const ONE_HOUR = 3600; | ||
@@ -25,5 +26,6 @@ const ONE_DAY = 86400; | ||
*/ | ||
module.exports = Object.freeze({ | ||
export { | ||
ONE_MINUTE, | ||
TEN_MINUTES, | ||
HALF_HOUR, | ||
ONE_HOUR, | ||
@@ -34,2 +36,2 @@ ONE_DAY, | ||
ONE_YEAR | ||
}); | ||
}; |
300
src/utils.js
@@ -11,20 +11,10 @@ /** | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const moment = require('moment'); | ||
const regular = require('regular'); | ||
const isNumber = require('lodash.isnumber'); | ||
const isEmpty = require('lodash.isempty'); | ||
const timeValues = require('./timeValues'); | ||
import {now, createUTC as utc} from 'moment/src/lib/moment/moment'; | ||
import {updateLocale} from 'moment/src/lib/locale/locale'; | ||
import {formatMoment} from 'moment/src/lib/format/format'; | ||
import regular from 'regular'; | ||
import isEmpty from 'lodash.isempty'; | ||
/** | ||
* Possible date format output | ||
* @type {Object} | ||
*/ | ||
const dateFormats = Object.freeze({ | ||
// all numbers have leading zero | ||
normal: 'ddd, DD MMM YYYY HH:mm:ss', | ||
// disregard seconds during tests | ||
test: 'ddd, DD MMM YYYY HH:mm' | ||
}); | ||
// Mon, 01, Jan 2016, 00:00:00 UTC | ||
const defaultDateFormat = 'ddd, DD MMM YYYY HH:mm:ss z'; | ||
@@ -35,4 +25,4 @@ /** | ||
*/ | ||
function isTrueObject(val) { | ||
return !Array.isArray(val) && typeof val === 'object' && !isEmpty(val) ; | ||
export function isNonEmptyObject(val) { | ||
return !Array.isArray(val) && typeof val === 'object' && !isEmpty(val); | ||
} | ||
@@ -45,43 +35,7 @@ | ||
*/ | ||
function isNumberLike(val) { | ||
return isNumber(val) || regular.number.test(val); | ||
export function isNumberLike(val) { | ||
return regular.number.test(val); | ||
} | ||
/** | ||
* @param {*} val Any JS object | ||
* @private | ||
* @return {string} | ||
*/ | ||
function getType(val) { | ||
return typeof val; | ||
} | ||
/** | ||
* @param {object} [time] A Date object | ||
* @return {number} | ||
*/ | ||
function createUnixTime(time) { | ||
if (!time || (time === (void 0))) { | ||
// void 0 => undefined | ||
time = new Date(); | ||
} | ||
// convert milliseconds to seconds | ||
return Math.round(moment.unix(time) / 1000); | ||
} | ||
/** | ||
* @param {object[]} [timestamps] An array of Dates | ||
* @private | ||
* @return {object} A Date object | ||
*/ | ||
function getLatestTimestamp(timestamps = []) { | ||
timestamps.sort((a, b) => { | ||
const unixTimeA = createUnixTime(a); | ||
const unixTimeB = createUnixTime(b); | ||
return unixTimeA > unixTimeB; | ||
}); | ||
return timestamps.reverse()[0]; | ||
} | ||
/** | ||
* @param {object} [time] Date object | ||
@@ -91,223 +45,25 @@ * @return {object} moment object in UTC format | ||
function getUtcTime(time = new Date()) { | ||
return moment.utc(time); | ||
return utc(time); | ||
} | ||
/** | ||
* Format a UTC Date value | ||
* @param {object} options | ||
* @param {object} [options.date=new Date()] Date object | ||
* @param {number} [options.timeToAdd=timeValues.TEN_MINUTES] A number of time to add, defaults in seconds | ||
* @param {string} [options.timeFormat='s'] The time format based on momentjs {{@link http://momentjs.com/docs/#/manipulating/add/}} | ||
* @return {object} moment object in UTC format with additional time added | ||
* @param {number} [options.date=now()] UTC time format. A JavaScript date must be passed in, not a moment date object | ||
* @param {string} [options.dateFormat=defaultDateFormat] Primarily used for testing | ||
* @return {string} header date string in GMT format | ||
*/ | ||
function addTime(options = {}) { | ||
export function formatDate(options = {}) { | ||
const { | ||
date = new Date(), | ||
timeToAdd = timeValues.TEN_MINUTES, | ||
timeFormat = 's' } = options; | ||
const utcTime = getUtcTime(date); | ||
return utcTime.add(timeToAdd, timeFormat); | ||
date = now(), | ||
dateFormat = defaultDateFormat | ||
} = options; | ||
// keeping this here if we want to | ||
// support setting locales in the future | ||
const locale = {key: undefined, config: undefined}; | ||
// need to set locale before formatting | ||
updateLocale(locale.key, locale.config); | ||
const formatted = formatMoment(getUtcTime(date), dateFormat); | ||
// do browsers require using GMT instead of UTC? | ||
return formatted.replace('UTC', 'GMT'); | ||
} | ||
/** | ||
* Format a UTC Date value | ||
* @param {number} time UTC time format | ||
* @param {string} [formatType='normal'] Primarily used for testing | ||
* @return {string} header date string in GMT format | ||
*/ | ||
function formatDate(time, formatType = 'normal') { | ||
if (moment.isMoment(time)) { | ||
time = time.toISOString(); | ||
} | ||
const format = dateFormats[formatType] || dateFormats.normal; | ||
return (getUtcTime(time).format(format)).toString() + ' GMT'; | ||
} | ||
/** | ||
* Promise returns a number or a moment timestamp object | ||
* @param {number|string|object} time if an object, a Date object | ||
* @return {Promise} | ||
*/ | ||
function getTimestamp(time) { | ||
return new Promise((resolve) => { | ||
if (getType(time) === 'number' || (getType(time) === 'string' && +time === +time)) { | ||
return resolve(+time); | ||
} | ||
const timestamp = moment.utc(new Date(time)); | ||
if (timestamp.isValid()) { | ||
return resolve(timestamp); | ||
} | ||
time = moment.utc(new Date()); | ||
return resolve(+time); | ||
}); | ||
} | ||
/** | ||
* Creates a wrapping promise of promises and only resolves | ||
* when all promises have been resolved | ||
* @param {object[]} values | ||
* @private | ||
* @return {Promise} | ||
*/ | ||
function arrayOfTimestamps(values) { | ||
const promises = []; | ||
values.forEach(value => { | ||
promises.push( | ||
getTimestamp(value) | ||
); | ||
}); | ||
return Promise.all(promises); | ||
} | ||
/** | ||
* Gets the last modified time of a list of files | ||
* Creates a wrapping promise of promises and only resolves | ||
* when all promises have been resolved | ||
* @param {object[]} files An array of file path strings | ||
* @private | ||
* @return {Promise} | ||
*/ | ||
function arrayOfTimestampsFiles(files) { | ||
const promises = []; | ||
files.forEach(file => { | ||
promises.push( | ||
function () { | ||
return new Promise((resolve, reject) => { | ||
fs.lstat(file, (err, fileStats) => { | ||
if (err) { | ||
return reject(err); | ||
} | ||
if (fileStats.isFile()) { | ||
return resolve(getTimestamp(fileStats.mtime)); | ||
} | ||
return resolve(0); | ||
}); | ||
}); | ||
}() | ||
); | ||
}); | ||
return Promise.all(promises); | ||
} | ||
/** | ||
* @param {string} dirPath The directory to look into | ||
* @private | ||
* @return {Promise} | ||
*/ | ||
function getTimestampFromDirectory(dirPath) { | ||
return new Promise((resolve, reject) => { | ||
fs.readdir(dirPath, (err, files) => { | ||
if (err) { | ||
return reject(err); | ||
} | ||
files = files.map(file => { | ||
// prepend the `dirPath` to each file | ||
return path.resolve(dirPath, file); | ||
}); | ||
return arrayOfTimestampsFiles(files) | ||
.then(timestamps => { | ||
const latestTimestamp = getLatestTimestamp(timestamps); | ||
const time = createUnixTime(latestTimestamp ? latestTimestamp : null); | ||
return resolve(time); | ||
}) | ||
.catch(() => { | ||
reject(createUnixTime()); | ||
}); | ||
}); | ||
}); | ||
} | ||
/** | ||
* Gets the stats of the file. This checks whether it is an actual | ||
* file or a directory and delegates to other methods accordingly | ||
* @param {string} filePath | ||
* @private | ||
* @return {Promise} | ||
*/ | ||
function checkTimestampFileType(filePath) { | ||
return new Promise((resolve, reject) => { | ||
fs.realpath(filePath, (err, realFilePath) => { | ||
fs.lstat(realFilePath, (statErr, fileStats) => { | ||
if (fileStats.isFile()) { | ||
return resolve(createUnixTime(fileStats.mtime)); | ||
} | ||
if (fileStats.isDirectory()) { | ||
return resolve(getTimestampFromDirectory(realFilePath)); | ||
} | ||
return reject(false); | ||
}); | ||
}); | ||
}); | ||
} | ||
/** | ||
* @param {string} filePath Path to the file | ||
* @private | ||
* @return {Promise} | ||
*/ | ||
function getFileTimestamp(filePath) { | ||
return new Promise((resolve) => { | ||
return checkTimestampFileType(filePath) | ||
.then(resolvedTime => { | ||
resolve(resolvedTime); | ||
}) | ||
.catch(() => { | ||
resolve(createUnixTime()); | ||
}); | ||
}); | ||
} | ||
/** | ||
* All promises return a formatted date string to be used for response headers | ||
* in the format of `Mon, 21 Dec 2015 19:45:29 GMT` | ||
* @param {object[]|string|null|boolean} compare Array of timestamps or a single path to check the last modified time | ||
* @param {string} [formatType=normal] Typically used for testing. Values of `test` and `normal` are accepted | ||
* @return {Promise} | ||
*/ | ||
function getLastModified(compare = null, formatType = 'normal') { | ||
return new Promise((resolve, reject) => { | ||
if (Array.isArray(compare) && compare.length > 0) { | ||
return arrayOfTimestamps(compare) | ||
.then(timestamps => { | ||
const latestTimestamp = getLatestTimestamp(timestamps); | ||
return resolve(formatDate(latestTimestamp, formatType)); | ||
}) | ||
.catch(err => reject(err)); | ||
} else if (getType(compare) === 'string' && compare !== '') { | ||
return getTimestamp(compare) | ||
.then(timestamp => { | ||
resolve(formatDate(timestamp, formatType)); | ||
}) | ||
.catch(() => { | ||
getFileTimestamp(compare) | ||
.then(timestamp => { | ||
resolve(formatDate(timestamp, formatType)); | ||
}) | ||
.catch(err => reject(err)); | ||
}); | ||
} | ||
return resolve(formatDate(createUnixTime(), formatType)); | ||
}); | ||
} | ||
/** | ||
* @module utils | ||
* @type {{dateFormats: dateFormats, isTrueObject: isTrueObject, isNumberLike: isNumberLike, formatDate: formatDate, getUtcTime: getUtcTime, getTimestamp: getTimestamp, createUnixTime: createUnixTime, getLastModified: getLastModified, addTime: addTime}} | ||
*/ | ||
module.exports = { | ||
dateFormats, | ||
isTrueObject, | ||
isNumberLike, | ||
formatDate, | ||
getUtcTime, | ||
getTimestamp, | ||
createUnixTime, | ||
getLastModified, | ||
addTime | ||
}; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
822125
7
61
2
25
2249
2
- Removedlodash.clone@^3.0.3
- Removedlodash.isnumber@^3.0.1
- Removedlodash._arraycopy@3.0.0(transitive)
- Removedlodash._arrayeach@3.0.0(transitive)
- Removedlodash._baseassign@3.2.0(transitive)
- Removedlodash._baseclone@3.3.0(transitive)
- Removedlodash._basecopy@3.0.1(transitive)
- Removedlodash._basefor@3.0.3(transitive)
- Removedlodash._bindcallback@3.0.1(transitive)
- Removedlodash._isiterateecall@3.0.9(transitive)
- Removedlodash.clone@3.0.3(transitive)
- Removedlodash.isnumber@3.0.3(transitive)
Updatedmoment@^2.15.2