electricity
Advanced tools
Comparing version 2.9.0 to 3.0.0
@@ -9,2 +9,11 @@ # Changelog | ||
## [3.0.0] - 2021-11-17 | ||
### Changed | ||
- Version 3.0.0 is a complete rewrite designed to improve application startup times by lazily processing files as they are requested (instead of eagerly processing all files at application start). | ||
- Because files are processed lazily as they are requested, many users should experience reduced memory footprints for their apps. | ||
- We dropped support for the `If-Modified-Since` HTTP header due to the complexities in calculating this accurately for Snockets and SASS dependency graphs. The ETag HTTP header is still supported. | ||
- In v2.9.0 we replaced `react-tools` with `@babel/preset-react`. You can now pass along options to babel. | ||
- When using a CDN hostname we always return absolute URLs with the https:// origin (instead of protocol-relative URLs since this is now an anti-pattern: https://www.paulirish.com/2010/the-protocol-relative-url/). | ||
- We no longer GZip content that is less than 1500 bytes (the size of a single TCP packet). | ||
## [2.9.0] - 2021-11-02 | ||
@@ -11,0 +20,0 @@ ### Changed |
@@ -11,2 +11,2 @@ module.exports = [ | ||
'text/plain' | ||
]; | ||
]; |
584
lib/index.js
@@ -16,8 +16,9 @@ const crypto = require('crypto'); | ||
const availableEncodings = ['gzip', 'identity']; | ||
const gzipContentTypes = require('./gzipContentTypes.js'); | ||
exports.static = function static(directory, options) { | ||
directory = path.resolve(directory || 'public'); | ||
exports.static = function(directory, options) { | ||
// Default to 'public' if the directory is not specified | ||
directory = directory || 'public'; | ||
// Options are optional | ||
if (!options) { | ||
@@ -27,3 +28,8 @@ options = {}; | ||
if (!('hashify' in options)) { | ||
if (!options.babel) { | ||
options.babel = {}; | ||
} | ||
// Hashify by default | ||
if (!Object.prototype.hasOwnProperty.call(options, 'hashify')) { | ||
options.hashify = true; | ||
@@ -40,8 +46,6 @@ } | ||
if (!options.jsx) { | ||
options.jsx = {}; | ||
} | ||
// Snockets must be processed syncronously to produce consistent output | ||
options.snockets.async = false; | ||
// UglifyCSS by default | ||
if (!options.uglifycss) { | ||
@@ -53,2 +57,3 @@ options.uglifycss = { | ||
// UglifyJS by default | ||
if (!options.uglifyjs) { | ||
@@ -60,306 +65,306 @@ options.uglifyjs = { | ||
// Don't watch for changes by default | ||
if (!options.watch) { | ||
options.watch = { | ||
enabled: true | ||
enabled: false | ||
}; | ||
} | ||
if (options.hostname) { | ||
if (typeof options.hostname !== 'string') { | ||
throw Error('hostname must be a string'); | ||
} else if (options.hostname.slice(-1) === '/') { | ||
options.hostname = options.hostname.slice(0, options.hostname.length - 1); | ||
} | ||
} | ||
// Create a local cache to hold the files | ||
const files = {}; | ||
if (options.sass.imagePath) { | ||
if (typeof options.sass.imagePath !== 'string') { | ||
throw Error('sass.imagePath must be a string'); | ||
} | ||
} | ||
const snockets = new Snockets(); | ||
if (!options.sass.functions) { | ||
options.sass.functions = {}; | ||
} | ||
let watcher; | ||
options.sass.functions['image-url($img)'] = function(img) { | ||
return new sass.types.String('url("' + | ||
(options.sass.imagePath ? | ||
path.join('/', options.sass.imagePath, img.getValue()) : | ||
path.join('/', img.getValue()) | ||
) + | ||
'")'); | ||
}; | ||
if (options.watch.enabled) { | ||
// Setup the watcher | ||
watcher = chokidar.watch(directory, { ignoreInitial: true }); | ||
var files = {}; | ||
var snockets = new Snockets(); | ||
var stylesheets = []; | ||
var watcher; | ||
watcher.on('all', function(eventName, filePath) { | ||
removeFile(filePath); | ||
}); | ||
} | ||
function cacheFile(filePath, stat) { | ||
var data; | ||
var ext = path.extname(filePath); | ||
var relativeUrl = toRelativeUrl(filePath); | ||
/** | ||
* Tries to read a file from local cache. | ||
* Reads the file from disk if it's not present in the local cache. | ||
* @param {string} urlPath | ||
*/ | ||
function fetchFile(urlPath) { | ||
// Try to get the file from local cache | ||
let file = files[urlPath]; | ||
// Compilation steps | ||
if (ext === '.scss' && !shouldIgnore(relativeUrl, options.sass.ignore)) { | ||
// Sass | ||
options.sass.file = filePath; | ||
data = sass.renderSync(options.sass).css.toString(); | ||
// Future steps should treat this as plain CSS | ||
relativeUrl = relativeUrl.replace(/.scss$/, '.css'); | ||
ext = '.css'; | ||
} else if (ext === '.js' && !shouldIgnore(relativeUrl, options.snockets.ignore)) { | ||
// Snockets | ||
try { | ||
data = snockets.getConcatenation(filePath, options.snockets); | ||
} catch (e) { | ||
// Snockets can't parse, so just pass the js file along | ||
console.warn(`Snockets skipping ${filePath}:\n ${e}`); | ||
data = fs.readFileSync(filePath).toString(); | ||
} | ||
// Return the file from cache if found | ||
if (file) { | ||
return file; | ||
} | ||
if (ext === '.js' && !shouldIgnore(relativeUrl, options.jsx.ignore)) { | ||
// React | ||
data = data || fs.readFileSync(filePath).toString(); | ||
// Read the file from disk | ||
file = readFile(urlPath); | ||
try { | ||
let result = babel.transformSync(data, { | ||
presets: ['@babel/preset-react'] | ||
}); | ||
// Put the file in local cache | ||
files[urlPath] = file; | ||
data = result.code; | ||
} catch(e) { | ||
// React can't transform, so just pass the js file along | ||
console.warn(`JSX compiler skipping ${filePath}:\n ${e}`); | ||
data = fs.readFileSync(filePath).toString(); | ||
} | ||
} | ||
return file; | ||
} | ||
// Postprocessing steps | ||
if (ext === '.css') { | ||
if (options.uglifycss.enabled) { | ||
// Uglifycss | ||
data = data ? UglifyCss.processString(data, options.uglifycss) : UglifyCss.processFiles([filePath], options.uglifycss); | ||
} | ||
} else if (ext === '.js' && options.uglifyjs.enabled && !shouldIgnore(relativeUrl, options.uglifyjs.ignore)) { | ||
var uglifyjsOptions = JSON.parse(JSON.stringify(options.uglifyjs)); | ||
delete uglifyjsOptions.enabled; | ||
delete uglifyjsOptions.ignore; | ||
if (uglifyjsOptions.sourceMap === true) { | ||
uglifyjsOptions.sourceMap.filename = path.basename(relativeUrl); | ||
uglifyjsOptions.sourceMap.out = `${uglifyjsOptions.sourceMap.filename}.map`; | ||
} | ||
var result = UglifyJS.minify(data, uglifyjsOptions); | ||
if (result.error) { | ||
console.warn(`UglifyJS skipping ${filePath}:\n ${JSON.stringify(result.error)}`); | ||
} else { | ||
data = result.code; | ||
} | ||
/** | ||
* Converts a URL (/robots.txt) to a URL that includes the file's hash (/robots-3f54004ef6fc21b24a9e6069fc114fd9070b77a1.txt) | ||
* @param {string} url | ||
* @param {string} hash | ||
*/ | ||
function hashifyUrl(url, hash) { | ||
if (!url.includes('.')) { | ||
return url.replace(/([?#].*)?$/, `-${hash}$1`); | ||
} | ||
// Have data? | ||
data = data || fs.readFileSync(filePath); | ||
return url.replace(/\.([^.]*)([?#].*)?$/, `-${hash}.$1$2`); | ||
} | ||
// Cache file and hash, generate hash later if it's CSS because we're going to modify the contents | ||
files[relativeUrl] = { | ||
content: data, | ||
contentLength: typeof data === 'string' ? Buffer.byteLength(data) : data.length, | ||
contentType: mime.getType(relativeUrl), | ||
hash: ext !== '.css' ? crypto.createHash('md5').update(data).digest('hex') : '', | ||
modified: stat.mtime | ||
}; | ||
/** | ||
* Parses a URL path potentially containing a hash (/robots-3f54004ef6fc21b24a9e6069fc114fd9070b77a1.txt) | ||
* into an object with a hash and path properties ({ hash: '3f54004ef6fc21b24a9e6069fc114fd9070b77a1', path: '/robots.txt' }) | ||
* @param {object} req | ||
*/ | ||
function parseUrlPath(urlPath) { | ||
// https://regex101.com/r/j5hvRj/1 | ||
const regex = /\/.+(-([0-9a-f]{40}))/; | ||
const matches = urlPath.match(regex); | ||
// If-Modified-Since doesn't support millisecond precision | ||
files[relativeUrl].modified.setMilliseconds(0); | ||
if (ext === '.css') { | ||
stylesheets.push(relativeUrl); | ||
} | ||
return relativeUrl; | ||
} | ||
function dehashifyPath(filePath) { | ||
if (!options.hashify) { | ||
if (!matches) { | ||
return { | ||
path: filePath, | ||
hash: undefined | ||
path: urlPath | ||
}; | ||
} | ||
var hashRegex = /-[0-9a-f]+(\.[^.]*$)/; | ||
var hashMatch = filePath.match(hashRegex); | ||
var hash = hashMatch ? hashMatch[0].slice(1).replace(/\.([^.]*$)/, '') : ''; | ||
return { | ||
path: hash.length == 32 ? filePath.replace(hashRegex, '$1') : filePath, | ||
hash: hash.length == 32 ? hash : null | ||
hash: matches[2], | ||
path: urlPath.replace(matches[1], '') | ||
}; | ||
} | ||
function gzip(file, callback) { | ||
if (file.gzippedContent && file.gzippedContentLength) { | ||
return callback(null, file); | ||
} | ||
function readCascadingStyleSheetsFile(filePath) { | ||
let data; | ||
zlib.gzip(file.content, function(err, gzippedContent) { | ||
if (err) { | ||
return callback(err); | ||
// CSS | ||
try { | ||
data = fs.readFileSync(filePath).toString(); | ||
} catch(err) { | ||
// Handle ENOENT (No such file or directory): https://nodejs.org/api/errors.html#common-system-errors | ||
if (err.code !== 'ENOENT') { | ||
throw err; | ||
} | ||
file.gzippedContent = gzippedContent; | ||
file.gzippedContentLength = gzippedContent.length; | ||
// SASS | ||
const basename = path.basename(filePath, path.extname(filePath)); | ||
options.sass.file = path.join(path.dirname(filePath), `${basename}.scss`); | ||
const result = sass.renderSync(options.sass); | ||
data = result.css.toString(); | ||
callback(null, file); | ||
// SASS (watcher) | ||
if (watcher) { | ||
result.stats.includedFiles.forEach(file => { | ||
watcher.add(file); | ||
}); | ||
} | ||
} | ||
// Update URLs in CSS: https://regex101.com/r/FxrppP/4 | ||
data = data.replace(/url\(['"]?(.*?)['"]?\)/g, function(match, p1) { | ||
return `url(${urlBuilder(p1)})`; | ||
}); | ||
// UglifyCSS | ||
if (options.uglifycss.enabled) { | ||
data = UglifyCss.processString(data, options.uglifycss); | ||
} | ||
return data; | ||
} | ||
function hashifyCss(cssPath) { | ||
return files[cssPath].content.toString().replace(/url\(['"]?(.*?)['"]?\)/g, function(match, filename) { | ||
var file; | ||
function readFile(urlPath) { | ||
let filePath = toFilePath(urlPath); | ||
let extension = path.extname(filePath); | ||
let data; | ||
if (filename[0] === '/') { | ||
file = files[filename]; | ||
} else { | ||
var cssDir = path.dirname(cssPath); | ||
filename = path.normalize(path.join(cssDir, filename)).replace(/\\/g, '/'); | ||
file = files[filename]; | ||
} | ||
if (extension === '.css') { | ||
data = readCascadingStyleSheetsFile(filePath); | ||
} else if (extension === '.js') { | ||
data = readJavaScriptFile(filePath); | ||
} else { | ||
data = fs.readFileSync(filePath); | ||
} | ||
if (file) { | ||
return 'url(' + urlBuilder(filename) + ')'; | ||
} | ||
const file = { | ||
content: data, | ||
contentLength: data.length, | ||
contentType: mime.getType(urlPath), | ||
hash: crypto.createHash('sha1').update(data).digest('hex') | ||
}; | ||
return match; | ||
}); | ||
} | ||
// Don't gzip any content less that 1500 bytes (the size of a TCP packet). Only gzip specific content types. | ||
if (file.contentLength > 1500 && gzipContentTypes.includes(file.contentType)) { | ||
const gzipContent = zlib.gzipSync(file.content); | ||
function hashifyPath(filePath, hash) { | ||
if (!filePath.includes('.')) { | ||
return filePath.replace(/([?#].*)?$/, `-${hash}$1`); | ||
file.gzip = { | ||
content: gzipContent, | ||
contentLength: gzipContent.length | ||
}; | ||
} | ||
return filePath.replace(/\.([^.]*)([?#].*)?$/, `-${hash}.$1$2`); | ||
return file; | ||
} | ||
function prefixSlash(path) { | ||
return path[0] === '/' ? path : '/' + path; | ||
} | ||
function readJavaScriptFile(filePath) { | ||
let data = fs.readFileSync(filePath).toString(); | ||
function reloadFile(filePath, stat) { | ||
stat = typeof stat === 'object' ? stat : fs.statSync(filePath); | ||
// Snockets | ||
try { | ||
data = snockets.getConcatenation(filePath, options.snockets); | ||
} catch(err) { | ||
// Snockets can't parse, so just pass the js file along | ||
console.warn(`Snockets skipping ${filePath}:\n ${err}`); | ||
} | ||
var ext = path.extname(filePath); | ||
// Snockets (watcher) | ||
if (watcher) { | ||
try { | ||
// Get all files in the snockets chain | ||
const compiledChain = snockets.getCompiledChain(filePath, options.snockets); | ||
if (ext === '.scss') { | ||
sassGraph.parseDir(directory).index[filePath].importedBy.forEach(reloadFile); | ||
} else if (ext === '.js') { | ||
// Clear snockets cache | ||
snockets.cache = {}; | ||
snockets.concatCache = {}; | ||
snockets.scan(filePath); | ||
snockets.depGraph.parentsOf(filePath).forEach(reloadFile); | ||
// Add each file of the snockets chain to the watcher | ||
compiledChain.forEach(c => { | ||
watcher.add(c.filename); | ||
}); | ||
} catch(err) { | ||
// Snockets can't parse, so skip watch | ||
console.warn(`Snockets skipping watch for ${filePath}:\n ${err}`); | ||
} | ||
} | ||
var relativeUrl = cacheFile(filePath, stat); | ||
// Babel | ||
try { | ||
let result = babel.transformSync(data, { | ||
...options.babel, | ||
presets: [require('@babel/preset-react')] | ||
}); | ||
if (stylesheets.includes(relativeUrl)) { | ||
files[relativeUrl].content = hashifyCss(relativeUrl); | ||
files[relativeUrl].hash = crypto.createHash('md5').update(files[relativeUrl].content).digest('hex'); | ||
data = result.code; | ||
} catch(err) { | ||
// Babel can't transform, so just pass the file along | ||
console.warn(`Babel skipping ${filePath}:\n ${err}`); | ||
} | ||
} | ||
function toRelativeUrl(filePath) { | ||
var relativeUrl = path.relative(directory, path.resolve(filePath)); | ||
// UglifyJS | ||
if (options.uglifyjs.enabled) { | ||
const uglifyjsOptions = JSON.parse(JSON.stringify(options.uglifyjs)); | ||
delete uglifyjsOptions.enabled; | ||
// Make URI-friendly and prepend a / | ||
return prefixSlash(relativeUrl).replace(/\\/g, '/'); | ||
} | ||
const result = UglifyJS.minify(data, uglifyjsOptions); | ||
function shouldIgnore(filePath, ignore) { | ||
if (!ignore) { | ||
// Not ignoring anything | ||
return false; | ||
if (result.error) { | ||
console.warn(`UglifyJS skipping ${filePath}:\n ${JSON.stringify(result.error)}`); | ||
} else { | ||
data = result.code; | ||
} | ||
} | ||
if (Array.isArray(ignore)) { | ||
// Multiple things to ignore | ||
return ignore.some(function ignoreElementMatch(ignored) { | ||
return shouldIgnore(filePath, ignored); | ||
}); | ||
return data; | ||
} | ||
/** | ||
* Removes a file from the local cache. | ||
* @param {*} filePath | ||
*/ | ||
function removeFile(filePath) { | ||
let extension = path.extname(filePath); | ||
if (extension === '.js') { | ||
return removeJavaScriptFile(filePath); | ||
} else if (extension === '.scss') { | ||
return removeSassFile(filePath); | ||
} | ||
return filePath.match(ignore) !== null; | ||
// Remove the changed file from the local cache | ||
delete files[toUrlPath(filePath)]; | ||
} | ||
function stripQueryAndTarget(filePath) { | ||
return filePath.replace(/[?#].*$/, ''); | ||
/** | ||
* Removes a JavaScript file from the local cache. | ||
* @param {string} filePath | ||
*/ | ||
function removeJavaScriptFile(filePath) { | ||
// Remove the changed file from the local cache | ||
delete files[toUrlPath(filePath)]; | ||
// Resolve the absolute file path for the changed file | ||
const absoluteFilePath = path.resolve(filePath); | ||
// Find any parents that have a dependency on this file and remove them too | ||
snockets.depGraph.parentsOf(absoluteFilePath).forEach(removeJavaScriptFile); | ||
} | ||
function urlBuilder(filePath) { | ||
var file = files[stripQueryAndTarget(prefixSlash(filePath))]; | ||
/** | ||
* Removes a SASS file from the local cache. | ||
* @param {string} filePath | ||
*/ | ||
function removeSassFile(filePath) { | ||
const basename = path.basename(filePath, path.extname(filePath)); | ||
const cssFilePath = path.join(path.dirname(filePath), `${basename}.css`); | ||
const urlPath = toUrlPath(cssFilePath); | ||
if (file) { | ||
var uri = prefixSlash(filePath); | ||
// Remove the changed file from the local cache | ||
delete files[urlPath]; | ||
if (options.hashify) { | ||
uri = hashifyPath(uri, file.hash); | ||
} | ||
// Resolve the absolute file path for the changed file | ||
const absoluteFilePath = path.resolve(filePath); | ||
if (options && options.hostname) { | ||
uri = '//' + options.hostname + uri; | ||
} | ||
const graph = sassGraph.parseDir(directory); | ||
const sassFile = graph.index[absoluteFilePath]; | ||
return uri; | ||
} else { | ||
return filePath; | ||
if (sassFile) { | ||
sassFile.importedBy.forEach(removeSassFile); | ||
} | ||
} | ||
// Load all files synchronously | ||
(function loadFiles(workingDir) { | ||
var contents = fs.readdirSync(workingDir); | ||
function urlBuilder(urlPath) { | ||
let url = urlPath; | ||
contents.forEach(function loadDirectory(file) { | ||
var filePath = path.join(workingDir, file); | ||
var stat = fs.statSync(filePath); | ||
if (options.hashify) { | ||
try { | ||
const request = parseUrlPath(urlPath); | ||
const file = fetchFile(request.path); | ||
if (stat.isDirectory()) { | ||
return loadFiles(filePath); | ||
url = hashifyUrl(request.path, file.hash); | ||
} catch(err) { | ||
// If we don't have a file that matches the specified URL path simply return the original URL path | ||
} | ||
} | ||
cacheFile(filePath, stat); | ||
}); | ||
})(directory); | ||
if (options.hostname) { | ||
url = `https://${options.hostname}${url}`; | ||
} | ||
// Hashify URLs in stylesheets | ||
stylesheets.forEach(function(stylesheet) { | ||
files[stylesheet].content = hashifyCss(stylesheet); | ||
files[stylesheet].hash = crypto.createHash('md5').update(files[stylesheet].content).digest('hex'); | ||
}); | ||
return url; | ||
} | ||
// Watch for changes | ||
if (options.watch.enabled) { | ||
const jsDirectories = Array.from(new Set(Object.keys(snockets.depGraph.map).map(d => path.dirname(d)))); | ||
const sassDirectories = Array.from(new Set(Object.keys(sassGraph.parseDir(directory).index).map(d => path.dirname(d)))); | ||
/** | ||
* Converts a URL path (/robots.txt) to a file path (/Users/username/site/public/robots.txt). | ||
* @param {string} urlPath | ||
*/ | ||
function toFilePath(urlPath) { | ||
const myURL = new URL(urlPath, 'https://example.org/'); | ||
const pathname = myURL.pathname.replace(/^\//, ''); | ||
return path.resolve(directory, pathname); | ||
} | ||
watcher = chokidar.watch([directory, ...jsDirectories, ...sassDirectories], { | ||
ignoreInitial: true | ||
}); | ||
/** | ||
* Converts a file path (/Users/username/site/public/robots.txt) to a URL path (/robots.txt). | ||
* @param {string} urlPath | ||
*/ | ||
function toUrlPath(filePath) { | ||
const urlPath = path.posix.relative(directory, path.resolve(filePath)); | ||
watcher.on('add', reloadFile); | ||
watcher.on('change', reloadFile); | ||
watcher.on('unlink', function(path) { | ||
var compiledPath = path.replace(/\.scss$/, '.css'); | ||
delete files[toRelativeUrl(compiledPath)]; | ||
}); | ||
return `/${urlPath}`; | ||
} | ||
@@ -369,8 +374,8 @@ | ||
// Ignore anything that's not a GET or HEAD request | ||
if (req.method !== 'GET' && req.method !== 'HEAD') { | ||
if (!['GET', 'HEAD'].includes(req.method)) { | ||
return next(); | ||
} | ||
// Register view helper if we haven't already | ||
if (!req.app.locals.electricity) { | ||
// Register function in app.locals to help views build URLs: https://expressjs.com/en/api.html#app.locals | ||
if (req.app && !req.app.locals.electricity) { | ||
req.app.locals.electricity = { | ||
@@ -381,31 +386,46 @@ url: urlBuilder | ||
var reqInfo = dehashifyPath(req.path); | ||
var file = files[reqInfo.path]; | ||
let file; | ||
const request = parseUrlPath(req.path); | ||
if (!file) { | ||
return next(); | ||
try { | ||
file = fetchFile(request.path); | ||
} catch(err) { | ||
// Handle EISDIR (Is a directory): https://nodejs.org/api/errors.html#common-system-errors | ||
if (err.code === 'EISDIR') { | ||
return next(); | ||
} | ||
// Handle ENOENT (No such file or directory): https://nodejs.org/api/errors.html#common-system-errors | ||
if (err.code === 'ENOENT') { | ||
return next(); | ||
} | ||
return next(err); | ||
} | ||
// Verify file matches the requested hash, otherwise 302 | ||
if (options.hashify && reqInfo.hash !== file.hash) { | ||
if (options.hashify && request.hash !== file.hash) { | ||
res.set({ | ||
'Cache-Control': 'no-cache', | ||
'Expires': '0', | ||
'Pragma': 'no-cache' | ||
'cache-control': 'no-cache', | ||
'expires': '0', | ||
'pragma': 'no-cache' | ||
}); | ||
return res.redirect(hashifyPath(reqInfo.path, file.hash)); | ||
const url = hashifyUrl(request.path, file.hash); | ||
return res.redirect(url); | ||
} | ||
var expires = new Date(); | ||
expires.setYear(expires.getFullYear() + 1); | ||
// Set a far-future expiration date | ||
const expires = new Date(); | ||
expires.setFullYear(expires.getFullYear() + 1); | ||
res.set({ | ||
'Cache-Control': 'public, max-age=31536000', | ||
'Content-Type': file.contentType, | ||
'ETag': file.hash, | ||
'Expires': expires.toUTCString(), | ||
'Last-Modified': file.modified.toUTCString() | ||
'cache-control': 'public, max-age=31536000', | ||
'content-Type': file.contentType, | ||
etag: file.hash, | ||
expires: expires.toUTCString() | ||
}); | ||
// Set any other headers specified in options | ||
if (options.headers) { | ||
@@ -415,36 +435,36 @@ res.set(options.headers); | ||
if (req.get('If-None-Match') === file.hash) { | ||
res.status(304); | ||
res.end(); | ||
} else if (new Date(req.get('If-Modified-Since')) >= file.modified) { | ||
res.status(304); | ||
res.end(); | ||
} else { | ||
res.set({ 'Content-Length': file.contentLength }); | ||
res.status(200); | ||
const ifNoneMatch = req.get('if-none-match'); | ||
if (req.method === 'HEAD') { | ||
return res.end(); | ||
} | ||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match | ||
if (ifNoneMatch && ifNoneMatch.includes(file.hash)) { | ||
return res.sendStatus(304); | ||
} | ||
var negotiator = new Negotiator(req); | ||
// By default, send the file's content (without gzip) | ||
let content = file.content; | ||
let contentLength = file.contentLength; | ||
if (negotiator.encodings(availableEncodings).indexOf('gzip') === 0 && gzipContentTypes.indexOf(file.contentType) !== -1) { | ||
gzip(file, function(err, gzippedFile) { | ||
if (err) { | ||
return next(err); | ||
} | ||
// Check to see if the file could be gzipped | ||
if (file.gzip && file.gzip.content) { | ||
const negotiator = new Negotiator(req); | ||
res.set({ | ||
'Content-Encoding': 'gzip', | ||
'Content-Length': gzippedFile.gzippedContentLength | ||
}); | ||
// Ensure the request supports gzip | ||
if (negotiator.encodings().includes('gzip')) { | ||
content = file.gzip.content; | ||
contentLength = file.gzip.contentLength; | ||
res.send(file.gzippedContent); | ||
}); | ||
} else { | ||
res.send(file.content); | ||
res.set('content-encoding', 'gzip'); | ||
} | ||
} | ||
// Set the content-length header | ||
res.set('content-length', contentLength); | ||
// Return early without sending content for HEAD requests | ||
if (req.method === 'HEAD') { | ||
return res.sendStatus(200); | ||
} | ||
res.send(content); | ||
}; | ||
}; | ||
}; |
@@ -8,3 +8,3 @@ { | ||
"negotiator": "~0.6.2", | ||
"sass": "~1.43.2", | ||
"sass": "~1.32.13", | ||
"sass-graph": "~3.0.4", | ||
@@ -16,3 +16,2 @@ "snockets": "~1.3.8", | ||
"devDependencies": { | ||
"buffer-compare": "*", | ||
"coveralls": "*", | ||
@@ -37,5 +36,5 @@ "fs-extra": "*", | ||
"coveralls": "nyc npm test && nyc report --reporter=text-lcov | coveralls", | ||
"test": "mocha --exit -R spec -t 21000" | ||
"test": "mocha --exit -R spec" | ||
}, | ||
"version": "2.9.0" | ||
"version": "3.0.0" | ||
} |
@@ -17,5 +17,5 @@ # Electricity | ||
```javascript | ||
var express = require('express'); | ||
const express = require('express'); | ||
app.use(express.static(__dirname + '/public')); | ||
app.use(express.static('public')); | ||
``` | ||
@@ -26,6 +26,6 @@ | ||
```javascript | ||
var express = require('express'); | ||
var electricity = require('electricity'); | ||
const express = require('express'); | ||
const electricity = require('electricity'); | ||
app.use(electricity.static(__dirname + '/public')); | ||
app.use(electricity.static('public')); | ||
``` | ||
@@ -43,3 +43,3 @@ | ||
```ejs | ||
<img src="<%= electricity.url('/apple-touch-icon-precomposed.png') %>" /> | ||
<img src="<%= electricity.url('/images/image.png') %>" /> | ||
<link href="<%= electricity.url('/styles/style.css') %>" rel="stylesheet" /> | ||
@@ -52,5 +52,5 @@ <script src="<%= electricity.url('/scripts/script.js') %>"></script> | ||
```html | ||
<img src="/apple-touch-icon-precomposed-d131dd02c5e6eec4.png" /> | ||
<link href="/styles/style-693d9a0698aff95c.css" rel="stylesheet" /> | ||
<script src="/scripts/script-2fcab58712467eab.js"></script> | ||
<img src="/images/image-423251d722a53966eb9368c65bfd14b39649105d.png" /> | ||
<link href="/styles/style-22a53914b39649105d66eb9368c65b423251d7fd.css" rel="stylesheet" /> | ||
<script src="/scripts/script-5d66eb9368c22a53914b39d7fd6491065b423251.js"></script> | ||
``` | ||
@@ -62,3 +62,3 @@ | ||
- **HTTP Headers:** Electricity sets proper `Cache-Control`, `ETag`, `Expires`, and `Last-Modified` headers to help avoid unnecessary HTTP requests on subsequent page views. | ||
- **HTTP Headers:** Electricity sets proper `Cache-Control`, `ETag`, and `Expires`, headers to help avoid unnecessary HTTP requests on subsequent page views. | ||
- **Minification of JavaScript and CSS:** Electricity minifies JavaScript and CSS files in order to improve response time by reducing file sizes. | ||
@@ -77,3 +77,4 @@ - **Gzip:** Electricity gzips many content types (CSS, HTML, JavaScript, JSON, plaintext, XML) to reduce response sizes. | ||
```javascript | ||
var options = { | ||
const options = { | ||
babel: {}, | ||
hashify: true, | ||
@@ -85,6 +86,3 @@ headers: {}, | ||
uglifyjs: { | ||
enabled: true, | ||
compress: { | ||
sequences: false | ||
} | ||
enabled: true | ||
}, | ||
@@ -101,20 +99,26 @@ uglifycss: { | ||
var options = { | ||
babel: { // Object passed straight to @babel/core options: https://babeljs.io/docs/en/options | ||
generatorOpts: { | ||
compact: false | ||
}, | ||
parserOpts: { | ||
errorRecovery: true | ||
} | ||
}, | ||
hashify: false, // Do not generate hashes for URLs | ||
headers: { 'Access-Control-Allow-Origin': 'http://foo.example' }, | ||
headers: { // Any additional headers you want a specify | ||
'Access-Control-Allow-Origin': 'https://example.com' | ||
}, | ||
hostname: 'cdn.example.com', // CDN hostname | ||
jsx: { // Object passed straight to react-tools options | ||
ignore: ['raw', /donotcompile/] // Files to skip compilation on, can be a single argument to String.prototype.match or an array | ||
} | ||
sass: { // Object passed straight to node-sass options | ||
imagePath: '/images', // Image path for sass image-url helper | ||
ignore: ['raw', /donotcompile/] // Files to skip compilation on, can be a single argument to String.prototype.match or an array | ||
outputStyle: 'compressed', | ||
quietDeps: true | ||
}, | ||
snockets: { // Object passed straight to snockets options | ||
ignore: ['raw', /donotcompile/] // Files to skip compilation on, can be a single argument to String.prototype.match or an array | ||
snockets: { // Object passed straight to snockets options: https://www.npmjs.com/package/snockets | ||
}, | ||
uglifyjs: { // Object passed straight to uglify-js options | ||
enabled: true // Minify Javascript | ||
uglifyjs: { // Object passed straight to uglify-js options: https://github.com/mishoo/UglifyJS#minify-options | ||
enabled: false // Do not minify Javascript | ||
}, | ||
uglifycss: { // Object passed straight to uglifycss options | ||
enabled: true // Minify CSS | ||
uglifycss: { // Object passed straight to uglifycss options: https://github.com/fmarcia/uglifycss | ||
enabled: false // Do not minify CSS | ||
} | ||
@@ -127,3 +131,3 @@ }; | ||
```javascript | ||
app.use(electricity.static(__dirname + '/public', options)); | ||
app.use(electricity.static('public', options)); | ||
``` | ||
@@ -133,6 +137,6 @@ | ||
Electricity sets proper `Cache-Control`, `ETag`, `Expires`, and `Last-Modified` headers to help avoid unnecessary HTTP requests on subsequent page views. If you'd like to specify literal values for specific HTTP headers you can set them in the `headers` option. This is useful if you need to specify a `Access-Control-Allow-Origin` header when loading fonts or JSON data off a CDN. | ||
Electricity sets proper `Cache-Control`, `ETag`, and `Expires` headers to help avoid unnecessary HTTP requests on subsequent page views. If you'd like to specify literal values for specific HTTP headers you can set them in the `headers` option. This is useful if you need to specify a `Access-Control-Allow-Origin` header when loading fonts or JSON data off a CDN. | ||
``` | ||
app.use(electricity.static(__dirname + '/public', { | ||
app.use(electricity.static('public', { | ||
headers: { 'Access-Control-Allow-Origin': '*' } | ||
@@ -144,6 +148,6 @@ })); | ||
Electricity will automatically rewrite URIs in CSS to use MD5 hashes (if a matching file is found). For example: | ||
Electricity will automatically rewrite URIs in CSS to use SHA1 hashes (if a matching file is found). For example: | ||
```css | ||
background-image: url(/apple-touch-icon-precomposed.png); | ||
background-image: url(/background.png); | ||
``` | ||
@@ -154,3 +158,3 @@ | ||
```css | ||
background-image: url(/apple-touch-icon-precomposed-d131dd02c5e6eec4.png); | ||
background-image: url(/background-423251d722a53966eb9368c65bfd14b39649105d.png); | ||
``` | ||
@@ -162,10 +166,10 @@ | ||
```javascript | ||
var express = require('express'); | ||
var electricity = require('electricity'); | ||
const express = require('express'); | ||
const electricity = require('electricity'); | ||
var options = { | ||
const options = { | ||
hostname: 'cdn.example.com' | ||
}; | ||
app.use(electricity.static(__dirname + '/public'), options); | ||
app.use(electricity.static('public'), options); | ||
``` | ||
@@ -175,3 +179,3 @@ | ||
```ejs | ||
<img src="<%= electricity.url('/apple-touch-icon-precomposed.png') %>" /> | ||
<img src="<%= electricity.url('/images/image.png') %>" /> | ||
<link href="<%= electricity.url('/styles/style.css') %>" rel="stylesheet" /> | ||
@@ -181,7 +185,7 @@ <script src="<%= electricity.url('/scripts/script.js') %>"></script> | ||
Your HTML will ultimately get rendered using protocol-relative URLs like this: | ||
Your HTML will ultimately get rendered using absolute URLs like this: | ||
```html | ||
<img src="//cdn.example.com/apple-touch-icon-precomposed-d131dd02c5e6eec4.png" /> | ||
<link href="//cdn.example.com/styles/style-693d9a0698aff95c.css" rel="stylesheet" /> | ||
<script src="//cdn.example.com/scripts/script-2fcab58712467eab.js"></script> | ||
``` | ||
<img src="https://cdn.example.com/images/image-423251d722a53966eb9368c65bfd14b39649105d.png" /> | ||
<link href="https://cdn.example.com/styles/style-22a53914b39649105d66eb9368c65b423251d7fd.css" rel="stylesheet" /> | ||
<script src="http://cdn.example.com/scripts/script-5d66eb9368c22a53914b39d7fd6491065b423251.js"></script> | ||
``` |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
36734
4
9
381
176
1
+ Addedsass@1.32.13(transitive)
- Removedsass@1.43.5(transitive)
Updatedsass@~1.32.13