Skip to content

Commit

Permalink
feat: Add support for transform caching (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
coreyfarrell authored Aug 12, 2019
1 parent 1189183 commit 89c711f
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 24 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ matching header will be processed by babel. Default `/(java|ecma)script/`.
Setting this to `false` will allow the full error message to be displayed. By
default errors are masked to prevent disclosure of server details.

### `cache`

A Map-like object for caching transform results. This object must have support
for both `get` and `set` methods.

### `cacheHashSalt`

A string used to salt the hash of source content.

## Running tests

Tests are provided by xo and ava.
Expand Down
46 changes: 33 additions & 13 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const path = require('path');
const fp = require('fastify-plugin');
const babel = require('@babel/core');
const hasha = require('hasha');

function shouldBabel(reply, opts) {
return opts.babelTypes.test(reply.getHeader('Content-Type') || '');
Expand All @@ -13,18 +14,25 @@ function babelPlugin(fastify, opts, next) {
opts.babelTypes = /(java|ecma)script/;
}

const cacheSalt = opts.cacheHashSalt ? hasha(opts.cacheHashSalt, {algorithm: 'sha256'}) : '';

fastify.addHook('onSend', babelOnSend);

next();

function actualSend(payload, next, filename) {
function actualSend(payload, next, hash, filename) {
const babelOpts = {
...opts.babelrc,
filename: filename || path.join(process.cwd(), 'index.js')
};

try {
next(null, babel.transform(payload, babelOpts).code);
const {code} = babel.transform(payload, babelOpts);
if (hash) {
opts.cache.set(hash, code);
}

next(null, code);
} catch (error) {
if (opts.maskError !== false) {
error.message = 'Babel Internal Error';
Expand Down Expand Up @@ -52,29 +60,41 @@ function babelPlugin(fastify, opts, next) {
return next();
}

reply.removeHeader('content-length');
if (payload === '') {
/* Skip babel if we have empty payload (304's for example). */
return next(null, '');
}

let hash;
if (opts.cache) {
const cacheTag = reply.getHeader('etag') || reply.getHeader('last-modified');
/* If we don't have etag or last-modified assume this is dynamic and not worth caching */
if (cacheTag) {
/* Prefer payload.filename, then payload it is a string */
const filename = typeof payload === 'string' ? payload : payload.filename;
hash = hasha([cacheTag, filename, cacheSalt], {algorithm: 'sha256'});
const result = opts.cache.get(hash);

if (typeof result !== 'undefined') {
next(null, result);
return;
}
}
}

if (typeof payload === 'string') {
actualSend(payload, next);
actualSend(payload, next, hash);
return;
}

let code = '';
payload.on('data', chunk => {
code += chunk;
});
payload.on('end', () => {
reply.removeHeader('content-length');

actualSend(code, next, payload.filename);
});
const code = [];
payload.on('data', chunk => code.push(chunk));
payload.on('end', () => actualSend(code.join(''), next, hash, payload.filename));
}
}

module.exports = fp(babelPlugin, {
fastify: '>=2.4.1',
fastify: '>=2.7.1',
name: 'fastify-babel'
});
16 changes: 9 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,22 @@
},
"homepage": "https://github.com/cfware/fastify-babel#readme",
"dependencies": {
"fastify-plugin": "^1.6.0"
"fastify-plugin": "^1.6.0",
"hasha": "^5.0.0"
},
"peerDependencies": {
"@babel/core": "^7.4.5"
"@babel/core": "^7.5.5"
},
"devDependencies": {
"@babel/core": "^7.4.5",
"@babel/core": "^7.5.5",
"@cfware/nyc": "^0.5.0",
"ava": "^2.0.0",
"babel-plugin-bare-import-rewrite": "^1.5.0",
"fastify": "^2.4.1",
"fastify-static": "^2.4.0",
"ava": "^2.2.0",
"babel-plugin-bare-import-rewrite": "^1.5.1",
"fastify": "^2.7.1",
"fastify-static": "^2.5.0",
"node-fetch": "^2.6.0",
"nyc": "^14.1.1",
"quick-lru": "^4.0.1",
"standard-version": "^7.0.0",
"string-to-stream": "^2.0.0",
"xo": "^0.24.0"
Expand Down
101 changes: 97 additions & 4 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import path from 'path';
import test from 'ava';
import fetch from 'node-fetch';
import sts from 'string-to-stream';
import QuickLRU from 'quick-lru';
import fastifyPackage from 'fastify/package';
import fastifyModule from 'fastify';
import fastifyStatic from 'fastify-static';
Expand All @@ -13,6 +14,11 @@ const babelResult = `import fastify from "${fastifyMain}";`;
const fromModuleSource = 'node_modules/fake-module/fake-module.js';
const fromModuleResult = `import fastify from "../fastify/${fastifyPackage.main}";`;

const appOpts = {
root: path.join(__dirname, '..', 'fixtures'),
prefix: '/'
};

const errorMessage = {
statusCode: 500,
code: 'BABEL_PARSE_ERROR',
Expand All @@ -35,10 +41,6 @@ const babelrcError = {
};

async function createServer(t, babelTypes, maskError, babelrc = {plugins: ['bare-import-rewrite']}) {
const appOpts = {
root: path.join(__dirname, '..', 'fixtures'),
prefix: '/'
};
/* Use of babel-plugin-bare-import-rewrite ensures fastify-babel does the
* right thing with payload.filename. */
const babelOpts = {babelrc, babelTypes, maskError};
Expand Down Expand Up @@ -106,3 +108,94 @@ test('static app js caching', async t => {

t.is(res2.status, 304);
});

async function testCache(t, cacheHashSalt) {
let hits = 0;
const hitCounter = () => ({
visitor: {
Program() {
hits++;
}
}
});

const fastify = fastifyModule();
const cache = new QuickLRU({maxSize: 50});
fastify
.get('/nofile.js', (req, reply) => {
reply.header('content-type', 'text/ecmascript');
reply.header('last-modified', 'Mon, 12 Aug 2019 12:00:00 GMT');
reply.send(staticContent);
})
.get('/uncachable.js', (req, reply) => {
reply.header('content-type', 'text/ecmascript');
reply.send(staticContent);
})
.register(fastifyStatic, appOpts)
.register(fastifyBabel, {
babelrc: {
plugins: [
'bare-import-rewrite',
hitCounter
]
},
cache,
cacheHashSalt
});
await fastify.listen(0);
const host = `http://127.0.0.1:${fastify.server.address().port}`;
const doFetch = async (path, step, prevKeys) => {
const res = await fetch(host + path);
const body = await res.text();
t.is(body, babelResult);
t.is(hits, prevKeys ? 2 : step);
const keys = [...cache.keys()];
if (prevKeys) {
t.deepEqual(keys, prevKeys);
} else {
t.is(keys.length, step);
}

return keys;
};

const doUncachable = async (prevKeys, step) => {
const res = await fetch(host + '/uncachable.js');
const body = await res.text();
t.is(body, babelResult);
t.is(hits, step);
t.deepEqual([...cache.keys()], prevKeys);
};

const iter = async prevKeys => {
let keys = await doFetch('/import.js', 1, prevKeys);
if (prevKeys) {
t.deepEqual(prevKeys, keys);
}

const [importKey] = prevKeys || keys;
t.is(cache.get(importKey), babelResult);

keys = await doFetch('/nofile.js', 2, prevKeys);
const [nofileKey] = keys.filter(key => key !== importKey);
t.is(cache.get(nofileKey), babelResult);

return [importKey, nofileKey];
};

const keys1 = await iter();
const keys2 = await iter(keys1);

t.deepEqual(keys1, keys2);

await doUncachable(keys1, 3);
await doUncachable(keys1, 4);

return keys1;
}

test('caching', async t => {
const key = await testCache(t);
const saltedKey = await testCache(t, 'salt the hash');
t.notDeepEqual(key, saltedKey);
});

0 comments on commit 89c711f

Please sign in to comment.