Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow AST plugins' output be cached based on external files #238

Merged
merged 28 commits into from
Jun 28, 2019
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a5df3ce
Expose unregistration of handlebar plugins to the broccoli wrapper.
chriseppstein May 16, 2019
d73ffda
Set the parseOptions.srcName option so that ast nodes have a source i…
chriseppstein May 16, 2019
662785d
Add support for dependencyInvalidation from AST Plugins.
chriseppstein May 28, 2019
160cb97
Tests for ast plugins and the caching thereof. Includes tests for
chriseppstein May 16, 2019
9170e07
Document the baseDir option.
chriseppstein May 29, 2019
787b9ab
Don't invalidate the persistent cache if dependencyInvalidation is en…
chriseppstein May 29, 2019
bd9122b
Remove comment that no longer applies.
chriseppstein May 30, 2019
3d1676a
Assert changes when when the file is unchanged or cached.
chriseppstein May 30, 2019
a817709
Use env.meta.moduleName instead of the ast node source location becau…
chriseppstein May 30, 2019
fbccbe0
Preserve the value of Program.keys if it is set.
chriseppstein May 30, 2019
cb2a027
Add documentation that was missing.
chriseppstein May 30, 2019
8233d08
Use the same variable name instead of pluralizing it.
chriseppstein May 30, 2019
4c45e78
Update to use official release from broccoli-persistent-filter. Ran y…
chriseppstein Jun 12, 2019
15ec6d2
Fix failing test.
chriseppstein Jun 12, 2019
6c892d1
Test fix for missing dependent.
chriseppstein Jun 12, 2019
22fbf52
merge master
chriseppstein Jun 12, 2019
41387af
Fix failing test.
chriseppstein Jun 17, 2019
eec6eeb
Fix linting errors.
chriseppstein Jun 17, 2019
3ce4893
Update dependency for testing.
chriseppstein Jun 18, 2019
d61b0fd
Force persistent caching in CI for related tests.
chriseppstein Jun 19, 2019
096a639
Fix issues in legacy builds.
chriseppstein Jun 19, 2019
a15ea1d
Merge remote-tracking branch 'ember-cli/master' into dependency-inval…
chriseppstein Jun 19, 2019
5f7a7af
Handle old name of function that unregisters plugins.
chriseppstein Jun 20, 2019
97c6406
Don't run AST Plugin tests against legacy template compilers for now.
chriseppstein Jun 20, 2019
bc85143
Skip plugin tests when plugins can't be unregistered.
chriseppstein Jun 26, 2019
5c9a6d6
Pick up bug fixes in broccoli-persistent-filter.
chriseppstein Jun 26, 2019
2e7e581
Templates can have nested Program nodes. Since we can't rely on the T…
chriseppstein Jun 27, 2019
7571882
Merge remote-tracking branch 'ember-cli/master' into dependency-inval…
chriseppstein Jun 27, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,49 @@ module.exports = {
};
```

#### Options for registering a `htmlbars-ast-plugin`

* `name` - String. The name of the AST transform for debugging purposes.
* `plugin` - A function of type [`ASTPluginBuilder`](https://github.com/glimmerjs/glimmer-vm/blob/master/packages/%40glimmer/syntax/lib/parser/tokenizer-event-handlers.ts#L329-L341).
* `dependencyInvalidation` - Boolean. A flag that indicates the AST Plugin may, on a per-template basis, depend on other files that affect its output.
* `cacheKey` - function that returns any JSON-compatible value - The value returned is used to invalidate the persistent cache across restarts, usually in the case of a dependency or configuration change.
* `baseDir` - `() => string`. A function that returns the directory on disk of the npm module for the plugin. If provided, a basic cache invalidation is performed if any of the dependencies change (e.g. due to a npm install/upgrade).

#### Implementing Dependency Invalidation in an AST Plugin

Plugins that set the `dependencyInvalidation` option to `true` can provide function for the `plugin` of type `ASTDependencyPlugin` as given below.

Note: the `plugin` function is invoked without a value for `this` in context.

```ts
import {ASTPluginBuilder, ASTPlugin} from "@glimmer/syntax/dist/types/lib/parser/tokenizer-event-handlers";

export type ASTDependencyPlugin = ASTPluginWithDepsBuilder | ASTPluginBuilderWithDeps;

export interface ASTPluginWithDepsBuilder {
(env: ASTPluginEnvironment): ASTPluginWithDeps;
}

export interface ASTPluginBuilderWithDeps extends ASTPluginBuilder {
/**
* @see {ASTPluginWithDeps.dependencies} below.
**/
dependencies(relativePath): string[];
}

export interface ASTPluginWithDeps extends ASTPlugin {
/**
* This method is called just as the template finishes being processed.
*
* @param relativePath {string} A relative path to the file that may have dependencies.
* @return {string[]} paths to files that are a dependency for the given
* file. Any relative paths returned by this method are taken to be relative
* to the file that was processed.
*/
dependencies(relativePath: string): string[];
}
```

### Precompile HTMLBars template strings within other addons

```javascript
Expand Down
55 changes: 55 additions & 0 deletions addDependencyTracker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
module.exports = function addDependencyTracker(plugin, enableInvalidation) {
if (plugin.prototype && plugin.prototype.transform) {
// we don't track dependencies for legacy plugins.
return plugin;
}
if (!enableInvalidation) {
// Dependency invalidation isn't enabled, so no need to track.
return plugin;
}
if (typeof plugin.getDependencies === "function") {
// Don't add the dependency tracker if it's already added
return plugin;
}
// This variable in our closure allows us to share dependencies from
// the ast plugin that we can't access with the ast plugin generator that
// we can access.
let lastDependencies = {};
let trackedPlugin = (env) => {
let realPlugin = plugin(env);
let visitors = realPlugin.visitor;
chriseppstein marked this conversation as resolved.
Show resolved Hide resolved
let origProgram = visitors.Program;
let origEnter, origExit;
if (origProgram) {
if (typeof origProgram === "function") {
origEnter = origProgram;
origExit = undefined;
} else {
origEnter = origProgram.enter;
origExit = origProgram.exit;
}
}
visitors.Program = {
chriseppstein marked this conversation as resolved.
Show resolved Hide resolved
enter: (node) => {
let fileName = node.loc.source;
chriseppstein marked this conversation as resolved.
Show resolved Hide resolved
if (realPlugin.resetDependencies) {
chriseppstein marked this conversation as resolved.
Show resolved Hide resolved
realPlugin.resetDependencies();
}
delete lastDependencies[fileName];
if (origEnter) origEnter(node);
},
exit: (node) => {
if (realPlugin.dependencies) {
let fileName = node.loc.source;
lastDependencies[fileName] = realPlugin.dependencies(fileName);
}
if (origExit) origExit(node)
}
};
return realPlugin;
};
trackedPlugin.getDependencies = (filename) => {
return lastDependencies[filename] || [];
}
return trackedPlugin;
};
12 changes: 9 additions & 3 deletions ember-addon-main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const path = require('path');
const utils = require('./utils');
const addDependencyTracker = require("./addDependencyTracker");
const hashForDep = require('hash-for-dep');

module.exports = {
Expand Down Expand Up @@ -106,6 +107,8 @@ module.exports = {
ast: pluginInfo.plugins
},

dependencyInvalidation: pluginInfo.dependencyInvalidation,

pluginCacheKey: pluginInfo.cacheKeys
};

Expand All @@ -121,16 +124,18 @@ module.exports = {
let pluginWrappers = this.parentRegistry.load('htmlbars-ast-plugin');
let plugins = [];
let cacheKeys = [];
let dependencyInvalidation = false;

for (let i = 0; i < pluginWrappers.length; i++) {
let wrapper = pluginWrappers[i];
dependencyInvalidation = dependencyInvalidation || wrapper.dependencyInvalidation;
plugins.push(addDependencyTracker(wrapper.plugin, wrapper.dependencyInvalidation));

plugins.push(wrapper.plugin);

let providesBaseDir = typeof wrapper.baseDir === 'function';
let augmentsCacheKey = typeof wrapper.cacheKey === 'function';

if (providesBaseDir || augmentsCacheKey) {
if (providesBaseDir || augmentsCacheKey || wrapper.dependencyInvalidation) {
if (providesBaseDir) {
let pluginHashForDep = hashForDep(wrapper.baseDir());
cacheKeys.push(pluginHashForDep);
Expand All @@ -147,7 +152,8 @@ module.exports = {

return {
plugins: plugins,
cacheKeys: cacheKeys
cacheKeys: cacheKeys,
dependencyInvalidation: dependencyInvalidation,
};
}
};
49 changes: 47 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const fs = require('fs');
const path = require('path');
const utils = require('./utils');
const Filter = require('broccoli-persistent-filter');
const crypto = require('crypto');
Expand Down Expand Up @@ -36,6 +37,7 @@ class TemplateCompiler extends Filter {

this.precompile = this.options.templateCompiler.precompile;
this.registerPlugin = this.options.templateCompiler.registerPlugin;
this.unregisterPlugin = this.options.templateCompiler.unregisterPlugin

this.registerPlugins();
this.initializeFeatures();
Expand All @@ -45,6 +47,13 @@ class TemplateCompiler extends Filter {
return __dirname;
}

registeredASTPlugins() {
// This is a super obtuse way to get access to the plugins we've registered
// it also returns other plugins that are registered by ember itself.
rwjblue marked this conversation as resolved.
Show resolved Hide resolved
let options = this.options.templateCompiler.compileOptions();
return options.plugins.ast;
}

registerPlugins() {
let plugins = this.options.plugins;

Expand All @@ -56,6 +65,17 @@ class TemplateCompiler extends Filter {
}
}
}
unregisterPlugins() {
let plugins = this.options.plugins;

if (plugins) {
for (let type in plugins) {
for (let i = 0, l = plugins[type].length; i < l; i++) {
this.unregisterPlugin(type, plugins[type][i]);
}
}
}
}

initializeFeatures() {
let EmberENV = this.options.EmberENV;
Expand All @@ -73,11 +93,26 @@ class TemplateCompiler extends Filter {
}

processString(string, relativePath) {
let srcDir = this.inputPaths[0];
let srcName = path.join(srcDir, relativePath);
try {
return 'export default ' + utils.template(this.options.templateCompiler, stripBom(string), {
let result = 'export default ' + utils.template(this.options.templateCompiler, stripBom(string), {
contents: string,
moduleName: relativePath
moduleName: relativePath,
parseOptions: {
chriseppstein marked this conversation as resolved.
Show resolved Hide resolved
srcName: srcName
}
}) + ';';
if (this.options.dependencyInvalidation) {
let plugins = pluginsWithDependencies(this.registeredASTPlugins());
let dependencies = [];
for (let i = 0; i < plugins.length; i++) {
let pluginDeps = plugins[i].getDependencies(srcName);
dependencies = dependencies.concat(pluginDeps);
}
this.dependencies.setDependencies(relativePath, dependencies);
}
return result;
} catch(error) {
rethrowBuildError(error);
}
Expand Down Expand Up @@ -122,4 +157,14 @@ class TemplateCompiler extends Filter {
TemplateCompiler.prototype.extensions = ['hbs', 'handlebars'];
TemplateCompiler.prototype.targetExtension = 'js';

function pluginsWithDependencies(registeredPlugins) {
let found = [];
for (let i = 0; i < registeredPlugins.length; i++) {
if (registeredPlugins[i].getDependencies) {
found.push(registeredPlugins[i]);
}
}
return found;
}

module.exports = TemplateCompiler;
Loading