UNPKG

hastily

Version:

express middleware to simulate fastly cdn

164 lines 14.1 kB
"use strict"; /** * Simulate the [Fastly ImageOpto](https://docs.fastly.com/api/imageopto/) API. * Returns a middleware which attempts to run image responses through `sharp`. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.imageopto = exports.hasSupportedExtension = exports.HASTILY_STREAMABLE_PATH_REGEXP = exports.HASTILY_STREAMABLE_FILETYPES = exports.HASTILY_HEADER = void 0; const logging_1 = require("./logging"); const mime_types_1 = __importDefault(require("mime-types")); const sharp_1 = __importDefault(require("sharp")); const url_1 = require("url"); const vary_1 = __importDefault(require("vary")); const errors_1 = __importDefault(require("./errors")); const fastly_params_1 = __importDefault(require("./fastly-params")); const map_options_1 = __importDefault(require("./map-options")); const splice_response_1 = __importDefault(require("./splice-response")); exports.HASTILY_HEADER = { NAME: 'X-Optimized', VALUE: 'hastily', }; exports.HASTILY_STREAMABLE_FILETYPES = new Set(Object.keys(sharp_1.default.format).filter((ext) => sharp_1.default.format[ext].input.stream)); // plus the one sharp doesn't mention outright, the alias to jpeg... exports.HASTILY_STREAMABLE_FILETYPES.add('jpg'); // minus SVGs, which are widely supported in 2019 and we should not rasterize exports.HASTILY_STREAMABLE_FILETYPES.delete('svg'); exports.HASTILY_STREAMABLE_PATH_REGEXP = new RegExp(`/.+\\.(${[...exports.HASTILY_STREAMABLE_FILETYPES].join('|')})(?:[?#].*)?`); /** * Use the `sharp.format` manifest to determine if the current request's file * extension matches a format that sharp can stream in to optimize. * @param req {Request} */ exports.hasSupportedExtension = (req) => exports.HASTILY_STREAMABLE_PATH_REGEXP.test(req.originalUrl); /** * Returns a new imageopto middleware for use in Express `app.use()`. * Won't do anything if the Express app isn't already serving images! * */ function imageopto(filterOrOpts) { const options = { filter: exports.hasSupportedExtension, force: false, }; if (typeof filterOrOpts === 'function') { options.filter = filterOrOpts; } else if (typeof filterOrOpts === 'object') { options.filter = filterOrOpts.filter || options.filter; options.force = filterOrOpts.force; options.errorLog = options.quiet ? (_) => void 0 : filterOrOpts.errorLog; } const constructorLog = logging_1.createLogger('middleware'); constructorLog.debug('creating new middleware'); if (options.filter === exports.hasSupportedExtension) { constructorLog.debug('middleware filtering req.path for %s', exports.HASTILY_STREAMABLE_PATH_REGEXP.source); } const requestLog = logging_1.createLogger('request'); return function hastily(req, res, next) { const reqLog = requestLog.child({ req }); if (!options.filter(req)) { reqLog.debug('did not pass supplied filter function'); return next(); } splice_response_1.default(req, res, next, () => { // determine if the entity should be transformed if (!shouldTransform(req, res, reqLog, options)) { return false; } reqLog.debug('hastily will handle this image by transforming the response through sharp'); vary_1.default(res, 'Accept'); res.setHeader(exports.HASTILY_HEADER.NAME, exports.HASTILY_HEADER.VALUE); // image opto stream const params = new fastly_params_1.default(new Map(new url_1.URLSearchParams(req.query).entries()), req, res); const sharpStream = sharp_1.default(); const emitSharpError = (error) => { if (options.errorLog) { options.errorLog(error); } else { reqLog.error('Image processing failed: %s', error.toString()); } }; sharpStream.on('error', emitSharpError); try { const transformStream = map_options_1.default(params, sharpStream); const warnings = params.getWarnings(); if (warnings.length > 0) { const requestErrors = new errors_1.default(req.url, warnings); if (options.errorLog) { options.errorLog(requestErrors); } else { reqLog.warn(requestErrors.toString()); } } reqLog.debug('mapped options and created sharp stream'); return transformStream; } catch (e) { emitSharpError(e); return false; } }); }; } exports.imageopto = imageopto; const cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/; /** * Determine if the entity should be transformed. * @private */ function shouldTransform(req, res, reqLog, options) { if (req.method !== 'GET') { reqLog.debug('no transform: request method must be GET but is %s', req.method); return false; } if (res.statusCode > 299 || res.statusCode < 200) { reqLog.debug('no transform: res.statusCode must be 2xx but is %s', res.statusCode); return false; } const cacheControl = res.getHeader('Cache-Control'); // Don't optimize for Cache-Control: no-transform // https://tools.ietf.org/html/rfc7234#section-5.2.2.4 if (cacheControl && cacheControlNoTransformRegExp.test(cacheControl.toString())) { reqLog.debug('no transform: cache control header: "%s"', cacheControl); return false; } if (!options.force) { // Don't optimize if we've already done it somewhere const hastilyHeader = res.getHeader(exports.HASTILY_HEADER.NAME); if (hastilyHeader === exports.HASTILY_HEADER.VALUE) { reqLog.warn('no transform: header %o, hastily alrady transformed this earlier', exports.HASTILY_HEADER); return false; } // Don't optimize if Fastly has already done it for us const fastlyHeader = res.getHeader('fastly-io-info'); if (fastlyHeader) { reqLog.debug('no transform: fastly already transformed according to fastly-io-info header: "%s"', fastlyHeader); return false; } } const contentType = res.getHeader('content-type'); const extension = mime_types_1.default.extension(contentType); if (!extension) { reqLog.error('no transform: no valid content-type could not be detected'); return false; } const sharpFormatCapabilities = sharp_1.default.format[extension]; if (!sharpFormatCapabilities || !sharpFormatCapabilities.input.stream) { reqLog.error('no transform: sharp does not support input of type "%s"', contentType); return false; } const contentEncoding = res.getHeader('content-encoding'); if (contentEncoding && typeof contentEncoding === 'string' && contentEncoding.trim()) { reqLog.error('no transform: image is compressed with content-encoding "%s"; hastily does not support decompressing images. The server *should not* be compressing images! HTTP content encoding is only recommended for text documents. Binary files will not get smaller, and may evem get larger!'); } return true; } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW1hZ2VvcHRvLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2xpYi9pbWFnZW9wdG8udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7R0FHRzs7Ozs7O0FBRUgsdUNBQXlDO0FBRXpDLDREQUE4QjtBQUM5QixrREFBMEM7QUFDMUMsNkJBQXNDO0FBQ3RDLGdEQUF3QjtBQUN4QixzREFBcUM7QUFDckMsb0VBQTJDO0FBUzNDLGdFQUF1QztBQUN2Qyx3RUFBdUM7QUFvQjFCLFFBQUEsY0FBYyxHQUFHO0lBQzVCLElBQUksRUFBRSxhQUFhO0lBQ25CLEtBQUssRUFBRSxTQUFTO0NBQ2pCLENBQUM7QUFFVyxRQUFBLDRCQUE0QixHQUFHLElBQUksR0FBRyxDQUNqRCxNQUFNLENBQUMsSUFBSSxDQUFDLGVBQUssQ0FBQyxNQUFNLENBQUMsQ0FBQyxNQUFNLENBQzlCLENBQUMsR0FBRyxFQUFFLEVBQUUsQ0FBQyxlQUFLLENBQUMsTUFBTSxDQUFDLEdBQXVCLENBQUMsQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUM1RCxDQUNGLENBQUM7QUFDRixvRUFBb0U7QUFDcEUsb0NBQTRCLENBQUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxDQUFDO0FBQ3hDLDZFQUE2RTtBQUM3RSxvQ0FBNEIsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUM7QUFDOUIsUUFBQSw4QkFBOEIsR0FBRyxJQUFJLE1BQU0sQ0FDdEQsVUFBVSxDQUFDLEdBQUcsb0NBQTRCLENBQUMsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FDcEUsQ0FBQztBQUVGOzs7O0dBSUc7QUFDVSxRQUFBLHFCQUFxQixHQUFrQixDQUFDLEdBQUcsRUFBRSxFQUFFLENBQzFELHNDQUE4QixDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsV0FBVyxDQUFDLENBQUM7QUFFdkQ7Ozs7R0FJRztBQUNILFNBQWdCLFNBQVMsQ0FDdkIsWUFBOEM7SUFFOUMsTUFBTSxPQUFPLEdB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