diff --git a/CHANGELOG.md b/CHANGELOG.md index 83907ab2..5f441514 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ Changelog ========= +* Emit 'load' events on `Environment` instances, to allow runtime dependency + tracking. Fixes [#1153](https://github.com/mozilla/nunjucks/issues/1153). + 3.1.7 (Jan 12 2019) ------------------ diff --git a/docs/api.md b/docs/api.md index 2bf4f354..8c516a57 100644 --- a/docs/api.md +++ b/docs/api.md @@ -353,6 +353,23 @@ SafeString (to be documented) if one was passed in, so the output will copy the safeness of the input, but this property is helpful in rare circumstances. {% endapi %} + +{% api %} +'load' event +env.on('load', function(name, source, loader)) + +The 'load' event gets emitted whenever a Loader retrieves the source of a +template. It can be listened to in order to determine template dependencies +at runtime. The arguments emitted to the callback are: + +* **name** *(String)* The template name, as passed to the loader +* **source** *(Object)* The object that gets returned from Loader.getSource + * **src** *(String)* The template source + * **path** *(String)* The full path to the template + * **noCache** *(Bool)* If `true`, the template wasn't cached. +* **loader** The Loader instance that triggered the event. +{% endapi %} + {% raw %} ## Template diff --git a/docs/fr/api.md b/docs/fr/api.md index 847ff344..babac4b9 100644 --- a/docs/fr/api.md +++ b/docs/fr/api.md @@ -344,6 +344,22 @@ de rares circonstances. {% endapi %} {% raw %} +{% api %} +'load' événement +env.on('load', function(name, source, loader)) + +Quand une instance 'Loader' charge un template, elle émet l'événement +'load'. On peut utiliser cette événement pour détérminer les dépendances +à l'exécution. Ses arguments sont : + +* **name** *(String)* Le nom du template +* **source** *(Object)* Le resultat de l'appel de `Loader.getSource` + * **src** *(String)* Le code source du template + * **path** *(String)* Le chemin d'accèss du fichier ou l'URL + * **noCache** *(Bool)* Si la valeur n'est pas mise en cache. +* **loader** L'instance du Loader qui a émetté l'événement +{% endapi %} + ## Template Un `Template` est un objet qui gère la compilation des chaînes de template diff --git a/nunjucks/src/compiler.js b/nunjucks/src/compiler.js index 6ef3c149..baa469f3 100644 --- a/nunjucks/src/compiler.js +++ b/nunjucks/src/compiler.js @@ -5,7 +5,7 @@ const transformer = require('./transformer'); const nodes = require('./nodes'); const {TemplateError} = require('./lib'); const {Frame} = require('./runtime'); -const Obj = require('./object'); +const {Obj} = require('./object'); // These are all the same for now, but shouldn't be passed straight // through diff --git a/nunjucks/src/environment.js b/nunjucks/src/environment.js index bb1c3b90..1cd75371 100644 --- a/nunjucks/src/environment.js +++ b/nunjucks/src/environment.js @@ -8,7 +8,7 @@ const filters = require('./filters'); const {FileSystemLoader, WebLoader, PrecompiledLoader} = require('./loaders'); const tests = require('./tests'); const globals = require('./globals'); -const Obj = require('./object'); +const {Obj, EmitterObj} = require('./object'); const globalRuntime = require('./runtime'); const {handleError, Frame} = globalRuntime; const expressApp = require('./express-app'); @@ -37,7 +37,7 @@ const noopTmplSrc = { } }; -class Environment extends Obj { +class Environment extends EmitterObj { init(loaders, opts) { // The dev flag determines the trace that'll be shown on errors. // If set to true, returns the full trace from the error point, @@ -82,7 +82,7 @@ class Environment extends Obj { ); } - this.initCache(); + this._initLoaders(); this.globals = globals(); this.filters = {}; @@ -95,18 +95,28 @@ class Environment extends Obj { lib._entries(tests).forEach(([name, test]) => this.addTest(name, test)); } - initCache() { - // Caching and cache busting + _initLoaders() { this.loaders.forEach((loader) => { + // Caching and cache busting loader.cache = {}; if (typeof loader.on === 'function') { - loader.on('update', (template) => { - loader.cache[template] = null; + loader.on('update', (name, fullname) => { + loader.cache[name] = null; + this.emit('update', name, fullname, loader); + }); + loader.on('load', (name, source) => { + this.emit('load', name, source, loader); }); } }); } + invalidateCache() { + this.loaders.forEach((loader) => { + loader.cache = {}; + }); + } + addExtension(name, extension) { extension.__name = name; this.extensions[name] = extension; diff --git a/nunjucks/src/loader.js b/nunjucks/src/loader.js index 1ff3b208..139939ef 100644 --- a/nunjucks/src/loader.js +++ b/nunjucks/src/loader.js @@ -1,23 +1,9 @@ 'use strict'; const path = require('path'); -const Obj = require('./object'); - -module.exports = class Loader extends Obj { - on(name, func) { - this.listeners = this.listeners || {}; - this.listeners[name] = this.listeners[name] || []; - this.listeners[name].push(func); - } - - emit(name, ...args) { - if (this.listeners && this.listeners[name]) { - this.listeners[name].forEach((listener) => { - listener(...args); - }); - } - } +const {EmitterObj} = require('./object'); +module.exports = class Loader extends EmitterObj { resolve(from, to) { return path.resolve(path.dirname(from), to); } diff --git a/nunjucks/src/node-loaders.js b/nunjucks/src/node-loaders.js index 4e3d6f90..42405a04 100644 --- a/nunjucks/src/node-loaders.js +++ b/nunjucks/src/node-loaders.js @@ -45,7 +45,7 @@ class FileSystemLoader extends Loader { watcher.on('all', (event, fullname) => { fullname = path.resolve(fullname); if (event === 'change' && fullname in this.pathsToNames) { - this.emit('update', this.pathsToNames[fullname]); + this.emit('update', this.pathsToNames[fullname], fullname); } }); watcher.on('error', (error) => { @@ -76,11 +76,13 @@ class FileSystemLoader extends Loader { this.pathsToNames[fullpath] = name; - return { + const source = { src: fs.readFileSync(fullpath, 'utf-8'), path: fullpath, noCache: this.noCache }; + this.emit('load', name, source); + return source; } } diff --git a/nunjucks/src/nodes.js b/nunjucks/src/nodes.js index 65fd2faa..20c60d57 100644 --- a/nunjucks/src/nodes.js +++ b/nunjucks/src/nodes.js @@ -1,6 +1,6 @@ 'use strict'; -const Obj = require('./object'); +const {Obj} = require('./object'); function traverseAndCheck(obj, type, results) { if (obj instanceof type) { diff --git a/nunjucks/src/object.js b/nunjucks/src/object.js index 39c49ac0..e3a34c67 100644 --- a/nunjucks/src/object.js +++ b/nunjucks/src/object.js @@ -1,6 +1,7 @@ 'use strict'; // A simple class system, more documentation to come +const EventEmitter = require('events'); const lib = require('./lib'); function parentWrap(parent, prop) { @@ -59,4 +60,26 @@ class Obj { } } -module.exports = Obj; +class EmitterObj extends EventEmitter { + constructor(...args) { + super(); + // Unfortunately necessary for backwards compatibility + this.init(...args); + } + + init() {} + + get typename() { + return this.constructor.name; + } + + static extend(name, props) { + if (typeof name === 'object') { + props = name; + name = 'anonymous'; + } + return extendClass(this, name, props); + } +} + +module.exports = { Obj, EmitterObj }; diff --git a/nunjucks/src/parser.js b/nunjucks/src/parser.js index 45d0dc5b..a98142c2 100644 --- a/nunjucks/src/parser.js +++ b/nunjucks/src/parser.js @@ -2,7 +2,7 @@ var lexer = require('./lexer'); var nodes = require('./nodes'); -var Obj = require('./object'); +var Obj = require('./object').Obj; var lib = require('./lib'); class Parser extends Obj { diff --git a/nunjucks/src/web-loaders.js b/nunjucks/src/web-loaders.js index c592aa53..609e5880 100644 --- a/nunjucks/src/web-loaders.js +++ b/nunjucks/src/web-loaders.js @@ -45,6 +45,7 @@ class WebLoader extends Loader { path: name, noCache: !useCache }; + this.emit('load', name, result); if (cb) { cb(null, result); } diff --git a/tests/api.js b/tests/api.js index 17d644a8..275f2edf 100644 --- a/tests/api.js +++ b/tests/api.js @@ -82,5 +82,14 @@ path: path.resolve(templatesPath, 'string.njk') })).to.be('FooTest3BazFizzle'); }); + + it('should emit "load" event on Environment instance', function(done) { + var env = new Environment(new Loader(templatesPath)); + env.on('load', function(name, source) { + expect(name).to.equal('item.njk'); + done(); + }); + env.render('item.njk', {}); + }); }); }()); diff --git a/tests/loader.js b/tests/loader.js index 4c31428d..c3b9a58c 100644 --- a/tests/loader.js +++ b/tests/loader.js @@ -22,21 +22,6 @@ } describe('loader', function() { - it('should have default opts for WebLoader', function() { - var webLoader = new WebLoader(templatesPath); - expect(webLoader).to.be.a(WebLoader); - expect(webLoader.useCache).to.be(false); - expect(webLoader.async).to.be(false); - }); - - if (typeof FileSystemLoader !== 'undefined') { - it('should have default opts for FileSystemLoader', function() { - var fileSystemLoader = new FileSystemLoader(templatesPath); - expect(fileSystemLoader).to.be.a(FileSystemLoader); - expect(fileSystemLoader.noCache).to.be(false); - }); - } - it('should allow a simple loader to be created', function() { // From Docs: http://mozilla.github.io/nunjucks/api.html#writing-a-loader // We should be able to create a loader that only exposes getSource @@ -82,5 +67,49 @@ done(); }); }); + + describe('WebLoader', function() { + it('should have default opts for WebLoader', function() { + var webLoader = new WebLoader(templatesPath); + expect(webLoader).to.be.a(WebLoader); + expect(webLoader.useCache).to.be(false); + expect(webLoader.async).to.be(false); + }); + + it('should emit a "load" event', function(done) { + var loader = new WebLoader(templatesPath); + + if (typeof window === 'undefined') { + this.skip(); + } + + loader.on('load', function(name, source) { + expect(name).to.equal('simple-base.njk'); + done(); + }); + + loader.getSource('simple-base.njk'); + }); + }); + + if (typeof FileSystemLoader !== 'undefined') { + describe('FileSystemLoader', function() { + it('should have default opts', function() { + var loader = new FileSystemLoader(templatesPath); + expect(loader).to.be.a(FileSystemLoader); + expect(loader.noCache).to.be(false); + }); + + it('should emit a "load" event', function(done) { + var loader = new FileSystemLoader(templatesPath); + loader.on('load', function(name, source) { + expect(name).to.equal('simple-base.njk'); + done(); + }); + + loader.getSource('simple-base.njk'); + }); + }); + } }); }());