Skip to content

Instantly share code, notes, and snippets.

@loganfsmyth
Last active November 15, 2017 06:29
Show Gist options
  • Save loganfsmyth/0d6a9b0555028b07a220a3fcd0ba6b82 to your computer and use it in GitHub Desktop.
Save loganfsmyth/0d6a9b0555028b07a220a3fcd0ba6b82 to your computer and use it in GitHub Desktop.
babel plugin invalidation example

For explanation, essentially we'd generate a cache UUID based on cacheKey of your entire Babel config data and on all the plugin's cacheKeys, along with other stuff like the plugin options. Then we can take that cache key and fetch the cached data from wherever.

Each function in the cached block will be wrapped with logic to record the inputs and outputs, and the data will be stored in the output cacheable data. After a transform, we'd write Babel's normal output object, plus the data from the calls to the cached functions, and write it to some cache storage backend based on the final overall cacheKey.

When loading data from cache, we'll load the config and build the key as usually, and then before transforming anything, we try to load from the cache based on the config's key. Once the data is loaded from the cache, Babel itself will replay each call to a cached function, based on whatever the previous cached result had done. If they return matching values, we'll consider it a cache hit.

This seems like it gives optimal flexibility. I think the vast majority of plugins can be fully qualified based on their cacheKey, but plugins that depend on unrelated content have no way to signal that to Babel. By exposing the generic cached block, plugins have a general way to say "if this returns the wrong thing, consider me invalid".

import fs from "fs";
import {name, version} from "./package";
export default function() {
return {
// Return essentially a unique identifier for this plugin.
// Name and version works, but we can allow most things that have a meaningful stringified result.
// For more complex plugins, this would have to be both the plugin name/version, and also the name/version
// of any other dependencies that the plugin might use.
cacheKey: `${name}@${version}`,
// A set of functions who's inputs and outputs must be storable, and which should expect to be called
// both by the plugin, and by Babel's core itself to validate cache consistency. See the README for more.
cached: {
loadFile: filename => fs.readFileSync(filename, "utf8"),
},
visitor: {
StringLiteral({ node }) {
// Returns like normal, but will also be automatically re-called to verify that the same value
// is returned, to make sure the cached result is still valid. See README for more.
const file = this.cached.loadFile(node.value);
},
},
};
}
@kentcdodds
Copy link

Oh interesting, so this would require that anything that could invalidate the cache be put in a function in the cached object of my babel config, then I call that function using this.cached.nameOfThing? Doesn't this mean that you have to parse and traverse the code again just to see whether the cache is still valid?

@loganfsmyth
Copy link
Author

@kentcdodds My idea is that we'd record each call to each function in the cached block for a given run. Then next time you run Babel, if it is able to load anything from the cache based on the key, you already know that the input arguments are all identical. That means instead of traversing, you could essentially reply each cached call without having to traverse.

If any of the replayed calls fail, it'd then essentially behave as if there was no cached value, and then run the full traversal. You'd have to think about how you want to build on this API for your own macros. It seems like macro evaluation would probably need to be part of the cached block, then on a cache hit, Babel would essentially re-execute each macro to see if it still returned the same thing. Would that be reasonable?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment