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

Environment is not respected #2

Open
mnpenner opened this issue Dec 15, 2013 · 2 comments
Open

Environment is not respected #2

mnpenner opened this issue Dec 15, 2013 · 2 comments

Comments

@mnpenner
Copy link
Contributor

Perhaps I'm doing something wrong, but I can't figure it out.

The documentation says we need to use the env option when using extensions.

So, I've configured my Gruntfile like this,

nunjucks: {
    precompile: {
        src: 'app/views/**/*.njs',
        dest: 'app/scripts/nunjucks-templates.js',
        options: {
            env: njEnv
        }
    }
},

Where njEnv is defined like this:

var nj = require('nunjucks');
var njExt = require('./app/scripts/nunjucks-ext');
var njEnv = new nj.Environment(null, { autoescape: true });

njEnv.addExtension('NgBind', new njExt.NgBind);

I then ran the Grunt task, which spits out a file like this:

(function () {
    (window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["app/views/hello.njs"] = (function () {
        function root(env, context, frame, runtime, cb) {
            var lineno = null;
            var colno = null;
            var output = "";
            try {
                output += "<!DOCTYPE html>\r\n<html>\r\n<head>\r\n    <title>marrow app</title>\r\n    <script type=\"text/javascript\" src=\"/js/marrow.js\"></script>\r\n</head>\r\n<body>\r\n    What's up ";
                console.log(env);
                output += runtime.suppressValue(env.getExtension("NgBind")["run"](context, "name"), env.autoesc);
                output += "\r\n</body>\r\n</html>\r\n";
                cb(null, output);
                ;
            } catch (e) {
                cb(runtime.handleError(e, lineno, colno));
            }
        }

        return {
            root: root
        };
    })();
})();

(I added in the console.log)

And ran it in my browser, via the JavaScript console:

nunjucks.render('app/views/hello.njs',{name:'Mark'})

But it just gives me the following error:

Template render error: (app/views/hello.njs)
TypeError: env.getExtension(...) is undefined

Further, from the log, I can see that autoesc is false, which means it's not even respecting the options I passed in.

Why not? Aren't my settings supposed to be copied into the precompilation?

@mnpenner
Copy link
Contributor Author

I figured it out. I will post my solution here since it took my several hours to figure out.

You need to pass the Environment object to the precompiler in Gruntfile.js so that it can parse your templates. Your options and extensions, however, are not copied to the runtime instance.

So, if you want to get nunjucks working both server- and client-side, you essentially need to specify the environment three times: once for the Node server, once for the precompiler, and once for the client-side.

The easiest way to share your extensions both server and client-side is to put them into a separate JS file, like so:

// nunjucks-ext.js
var _ = require('lodash');

module.exports = function(nj) {
    return {
        NgBind: function() {
            this.tags = ['bind'];

            this.parse = function(parser, nodes, lexer) {
                var tag = parser.nextToken();
                var args = new nodes.NodeList(tag.lineno, tag.colno);
                var symbol = parser.nextToken();
                args.addChild(new nodes.Literal(symbol.lineno, symbol.colno, symbol.value));
                parser.advanceAfterBlockEnd(tag.value);
                return new nodes.CallExtension(this, 'run', args);
            };

            this.run = function(context, symbol) {
                return new nj.runtime.SafeString(_.template('<span ng-bind="<%- key %>"><%- val %></span>', {'key': symbol, 'val': context.ctx[symbol]}));
            };
        }
    }
};

Note that I've defined the exports this way so that the nunjucks module has to be passed in. This way we can use the full library server-side and the slim version client-side, and still share the same copy of the extensions.

The precompiler and Node can share the same environment. So put that in a separate file too:

// nunjucks-env.js
var nj = require('nunjucks');
var _ = require('lodash');
var env = new nj.Environment(new nj.FileSystemLoader('app/views'), { autoescape: true });

_.forOwn(require('./app/scripts/nunjucks-ext')(nj), function (ext, name) {
    env.addExtension(name, new ext);
});

module.exports = env;

Then in your Node application, you can simply require your environment and use it immediately:

// app.js
var njEnv = require('./nunjucks-env');
njEnv.render('hello.njs', { name: 'Mark' })

Lastly, you can configure the environment for client like so:

// nunjucks-client.js
var _ = require('lodash');
require('./nunjucks-templates');
require('../../node_modules/nunjucks/browser/nunjucks-slim');

var njEnv = new nunjucks.Environment(null, { autoescape: true });

_.forOwn(require('./nunjucks-ext')(nunjucks), function (ext, name) {
    njEnv.addExtension(name, new ext);
});

console.log(njEnv.render('app/views/hello.njs', {name: 'Ralph'}));

(Note that we need to specify the full template path until Issue #1 is resolved)

This file can be browserified to pull in the needed requirements. I've used lodash here, but you can easily rewrite it without.

@jlongster
Copy link
Owner

Thanks a lot for this (unfortunately missed some of the issues on this repo until now). Precompilation is hard for these reasons, and I'm sorry it took you a while to figure it out. I can make the docs clearer about that.

The problem is that env really is a runtime thing, but the extensions need to be known at compile-time. What you ended up with was what I intended; you isolate the env configuration in a separate module that you can load and use in several places. You'll always need to create at least 2 envs, one for server-side and one for client-side. Precompiling happens on the server but with the client-side env, and we can't possibly "embed" the env within the precompile templates because there are lots of function instances and other things that aren't reliably serializable, so you need to create another instance there too.

You only really feel this pain if you are using extensions, because otherwise don't need to pass an env to the precompiler. But once you figure it out, it's not too bad, and you get the power of extensions. I'm not exactly sure how we can improve this, but I'll think about it. I can at least improve the docs for grunt-nunjucks.

Thanks for the detailed write-up!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants