#78 Change compiled templates to export a loaded Template instance

This commit is contained in:
Patrick Steele-Idem 2015-05-26 16:57:39 -06:00
parent 2ddf263a16
commit 7c00085380
12 changed files with 270 additions and 56 deletions

View File

@ -491,7 +491,7 @@ TemplateBuilder.prototype = {
} else {
params = ['out'];
}
out.append('exports.create = function(__helpers) {\n');
out.append('function create(__helpers) {\n');
//Write out the static variables
this.writer.flush();
this._writeVars(this.staticVars, out, INDENT);
@ -508,7 +508,10 @@ TemplateBuilder.prototype = {
out.append('\n');
}
out.append(this.writer.getOutput());
out.append(INDENT + '};\n}');
// We generate code that assign a partially Template instance to module.exports
// and then we fully initialize the Template instance. This was done to avoid
// problems with circular dependencies.
out.append(INDENT + '};\n}\n(module.exports = require("marko").c(__filename)).c(create);');
return out.toString();
},
setTemplateName: function (templateName) {

View File

@ -25,6 +25,10 @@ exports.enable = function() {
// Patch the Template prototype to proxy all render methods...
Object.keys(Template.prototype).forEach(function(k) {
if (k === 'c') {
return;
}
var v = Template.prototype[k];
if (typeof v === 'function') {
@ -45,9 +49,9 @@ exports.enable = function() {
});
var oldLoad = runtime.load;
var oldCreateTemplate = runtime.c;
runtime.load = function hotReloadLoad(path) {
runtime.c = function hotReloadCreateTemplate(path) {
if (!path) {
throw new Error('Invalid path');
}
@ -62,7 +66,7 @@ exports.enable = function() {
templatePath = templatePath.replace(/\.js$/, '');
}
var template = oldLoad.apply(runtime, arguments);
var template = oldCreateTemplate.apply(runtime, arguments);
// Store the current last modified with the template
template.__hotReloadModifiedFlag = modifiedFlag;

View File

@ -28,11 +28,8 @@ function compile(templatePath, markoCompiler, compilerOptions) {
fs.renameSync(tempFile, targetFile);
}
// The compiled output is for a CommonJS module. The code that we want to load should
// actually export a loaded template instance so we append some additional code to
// load the compiled template and assign it to `module.exports`. We also attach a
// path to the compiled template so that hot reloading will work.
return compiledSrc + '\nexports.path=__filename;\nmodule.exports = require("marko").load(exports);';
// We attach a path to the compiled template so that hot reloading will work.
return compiledSrc;
}
exports.install = function(options) {

View File

@ -24,6 +24,18 @@
// async-writer provides all of the magic to support asynchronous
// rendering to a stream
/**
* Method is for internal usage only. This method
* is invoked by code in a compiled Marko template and
* it is used to create a new Template instance.
* @private
*/
exports.c = function createTemplate(path) {
return new Template(path);
};
var asyncWriter = require('async-writer');
// helpers can the core set of utility methods
@ -39,6 +51,8 @@ var Readable;
var AsyncWriter = asyncWriter.AsyncWriter;
var extend = require('raptor-util/extend');
exports.AsyncWriter = AsyncWriter;
var stream;
@ -53,12 +67,25 @@ if (streamPath) {
stream = require(streamPath);
}
function Template( options) {
this._ = null;
function Template(path, func, options) {
this.path = path;
this._ = func;
this.buffer = !options || options.buffer !== false;
}
Template.prototype = {
/**
* Internal method to initialize a loaded template with a
* given create function that was generated by the compiler.
* Warning: User code should not depend on this method. For
* internal usage only.
*
* @private
* @param {Function(__helpers)} createFunc The function used to produce the render(data, out) function.
*/
c: function(createFunc) {
this._ = createFunc(helpers);
},
renderSync: function(data) {
var out = new AsyncWriter();
out.sync();
@ -192,9 +219,28 @@ if (stream) {
require('raptor-util/inherit')(Readable, stream.Readable);
}
function load(templatePath, options) {
var cache = exports.cache;
function createRenderProxy(template) {
return function(data, out) {
template._(data, out);
};
}
function initTemplate(rawTemplate, templatePath) {
if (rawTemplate.render) {
return rawTemplate;
}
var createFunc = rawTemplate.create || rawTemplate;
var template = createFunc.loaded;
if (!template) {
template = createFunc.loaded = new Template(templatePath);
template.c(createFunc);
}
return template;
}
function load(templatePath, options) {
if (!templatePath) {
throw new Error('"templatePath" is required');
}
@ -202,42 +248,18 @@ function load(templatePath, options) {
var template;
if (typeof templatePath === 'string') {
template = cache[templatePath];
if (!template) {
// The template has not been loaded
// Cache the Template instance before actually loading and initializing
// the compiled template. This allows circular dependencies since the
// partially loaded Template instance will be found in the cache.
template = cache[templatePath] = new Template(options);
// Now load the template to get access to the factory function that is used to produce
// the actual compiled template function. We pass the helpers
// as the first argument to the factory function to produce
// the template rendering function
template._ = loader(templatePath).create(helpers); // Load the template factory and invoke it
}
template = initTemplate(loader(templatePath), templatePath);
} else if (templatePath.render) {
template = templatePath;
} else {
// If the first argument is already a loaded template then just return it
if (templatePath.render) {
return templatePath;
}
template = initTemplate(templatePath);
}
// Instead of a path, assume we got a compiled template module
// We store the loaded template with the factory function that was
// used to get access to the compiled template function
template = templatePath._;
if (!template) {
// First put the partially loaded Template instance on the
// the compiled template module before actually loading and
// initializing the compiled template. This allows for circular
// dependencies during template loading.
template = templatePath._ = new Template(options);
// Now fully initialize the template by adding the needed render
// function.
template._ = templatePath.create(helpers);
}
if (options) {
template = new Template(
template.path,
createRenderProxy(template),
options);
}
return template;

View File

@ -6,6 +6,8 @@ var nodePath = require('path');
var marko = require('../');
var through = require('through');
require('../node-require').install();
describe('marko/api' , function() {
before(function() {
@ -275,4 +277,21 @@ describe('marko/api' , function() {
});
});
it('should allow a template to be required', function(done) {
var templatePath = nodePath.join(__dirname, 'fixtures/templates/api-tests/hello.marko');
var template = require(templatePath);
template.render(
{
name: 'John'
},
function(err, output) {
if (err) {
return done(err);
}
expect(output).to.equal('Hello John!');
done();
});
});
});

View File

@ -1,4 +1,4 @@
exports.create = function(__helpers) {
function create(__helpers) {
var str = __helpers.s,
empty = __helpers.e,
notEmpty = __helpers.ne,
@ -13,4 +13,5 @@ exports.create = function(__helpers) {
"name": "World"
});
};
}
}
(module.exports = require("marko").c(__filename)).c(create);

View File

@ -1,4 +1,4 @@
exports.create = function(__helpers) {
function create(__helpers) {
var str = __helpers.s,
empty = __helpers.e,
notEmpty = __helpers.ne;
@ -6,4 +6,5 @@ exports.create = function(__helpers) {
return function render(data, out) {
out.w('Hello John & Suzy Invalid Entity: &b ; Valid Numeric Entity: "\nValid Hexadecimal Entity:\n¢');
};
}
}
(module.exports = require("marko").c(__filename)).c(create);

View File

@ -1,4 +1,4 @@
exports.create = function(__helpers) {
function create(__helpers) {
var str = __helpers.s,
empty = __helpers.e,
notEmpty = __helpers.ne,
@ -13,4 +13,5 @@ exports.create = function(__helpers) {
str(data.missing) +
'!');
};
}
}
(module.exports = require("marko").c(__filename)).c(create);

View File

@ -1,4 +1,4 @@
exports.create = function(__helpers) {
function create(__helpers) {
var str = __helpers.s,
empty = __helpers.e,
notEmpty = __helpers.ne;
@ -6,4 +6,5 @@ exports.create = function(__helpers) {
return function render(data, out) {
out.w('Hello John');
};
}
}
(module.exports = require("marko").c(__filename)).c(create);

61
test/hot-reload-test.js Normal file
View File

@ -0,0 +1,61 @@
'use strict';
var chai = require('chai');
chai.Assertion.includeStack = true;
var expect = require('chai').expect;
var nodePath = require('path');
var marko = require('../');
var fs = require('fs');
require('../node-require').install();
describe('marko/hot-reload' , function() {
before(function() {
require('../hot-reload').enable();
require('../compiler').defaultOptions.checkUpToDate = false;
});
it('should allow a required template to be hot reloaded', function() {
var srcTemplatePath = nodePath.join(__dirname, 'fixtures/templates/api-tests/hello.marko');
var templateSrc = fs.readFileSync(srcTemplatePath, { encoding: 'utf8' });
var tempTemplatePath = nodePath.join(__dirname, 'temp/hello.marko');
fs.writeFileSync(tempTemplatePath, templateSrc, { encoding: 'utf8' });
var template = require(tempTemplatePath);
expect(template.renderSync({ name: 'John' })).to.equal('Hello John!');
fs.writeFileSync(tempTemplatePath, templateSrc + '!', { encoding: 'utf8' });
expect(template.renderSync({ name: 'John' })).to.equal('Hello John!');
require('../hot-reload').handleFileModified(tempTemplatePath);
expect(template.renderSync({ name: 'John' })).to.equal('Hello John!!');
});
it('should allow a non-required template to be hot reloaded', function() {
var srcTemplatePath = nodePath.join(__dirname, 'fixtures/templates/api-tests/hello.marko');
var templateSrc = fs.readFileSync(srcTemplatePath, { encoding: 'utf8' });
var tempTemplatePath = nodePath.join(__dirname, 'temp/hello2.marko');
fs.writeFileSync(tempTemplatePath, templateSrc, { encoding: 'utf8' });
var template = marko.load(tempTemplatePath);
expect(template.renderSync({ name: 'John' })).to.equal('Hello John!');
fs.writeFileSync(tempTemplatePath, templateSrc + '!', { encoding: 'utf8' });
expect(template.renderSync({ name: 'John' })).to.equal('Hello John!');
require('../hot-reload').handleFileModified(tempTemplatePath);
expect(template.renderSync({ name: 'John' })).to.equal('Hello John!!');
});
});

View File

@ -0,0 +1,103 @@
'use strict';
var chai = require('chai');
chai.Assertion.includeStack = true;
var expect = require('chai').expect;
var nodePath = require('path');
var marko = require('../');
var fs = require('fs');
require('../node-require').install();
describe('marko/legacy-compiled' , function() {
before(function() {
require('../compiler').defaultOptions.checkUpToDate = false;
});
it('should allow an exports.create template to be loaded', function() {
var template = require('marko').load({
create: function(__helpers) {
var escapeXml = __helpers.x;
return function render(data, out) {
out.w('Hello ' +
escapeXml(data.name) +
'!');
};
}
});
var output = template.renderSync({
name: 'Frank'
});
expect(output).to.equal('Hello Frank!');
});
it('should only load an exports.create template once', function() {
var compiled = {
create: function(__helpers) {
var escapeXml = __helpers.x;
return function render(data, out) {
out.w('Hello ' +
escapeXml(data.name) +
'!');
};
}
};
var template1 = require('marko').load(compiled);
var output = template1.renderSync({ name: 'Frank' });
expect(output).to.equal('Hello Frank!');
var template2 = require('marko').load(compiled);
expect(template1).to.equal(template2);
});
it('should allow a module.exports = function create() {} template to be loaded', function() {
var template = require('marko').load(function create(__helpers) {
var escapeXml = __helpers.x;
return function render(data, out) {
out.w('Hello ' +
escapeXml(data.name) +
'!');
};
});
var output = template.renderSync({
name: 'Frank'
});
expect(output).to.equal('Hello Frank!');
});
it('should only load a module.exports = function create() {} template once', function() {
var compiled = function create(__helpers) {
var escapeXml = __helpers.x;
return function render(data, out) {
out.w('Hello ' +
escapeXml(data.name) +
'!');
};
};
var template1 = require('marko').load(compiled);
var output = template1.renderSync({ name: 'Frank' });
expect(output).to.equal('Hello Frank!');
var template2 = require('marko').load(compiled);
expect(template1).to.equal(template2);
});
});

1
test/temp/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.marko