From 391aa80a9a7b64c38eafc72dc129fc607e06b2b4 Mon Sep 17 00:00:00 2001 From: Patrick Steele-Idem Date: Thu, 21 May 2015 17:16:29 -0600 Subject: [PATCH 1/9] Fixes #78 Custom require extension for marko files --- hot-reload/index.js | 14 ++++++++++++- node-require.js | 51 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 node-require.js diff --git a/hot-reload/index.js b/hot-reload/index.js index 3cdfdac02..f7c724f92 100644 --- a/hot-reload/index.js +++ b/hot-reload/index.js @@ -51,11 +51,22 @@ exports.enable = function() { if (!path) { throw new Error('Invalid path'); } + + var templatePath = path; + + if (typeof templatePath !== 'string') { + templatePath = path.path; + } + + if (typeof templatePath === 'string') { + templatePath = templatePath.replace(/\.js$/, ''); + } + var template = oldLoad.apply(runtime, arguments); // Store the current last modified with the template template.__hotReloadModifiedFlag = modifiedFlag; - template.__hotReloadPath = path; + template.__hotReloadPath = templatePath; return template; }; @@ -74,6 +85,7 @@ exports.handleFileModified = function(path) { console.log('[marko/hot-reload] File modified: ' + path); if (path.endsWith('.marko') || path.endsWith('.marko.html')) { + delete require.cache[path]; delete require.cache[path + '.js']; } diff --git a/node-require.js b/node-require.js new file mode 100644 index 000000000..7a7033e45 --- /dev/null +++ b/node-require.js @@ -0,0 +1,51 @@ +var path = require('path'); +var resolveFrom = require('resolve-from'); +var fs = require('fs'); +var fsReadOptions = { encoding: 'utf8' }; + +function compile(templatePath, markoCompiler, compilerOptions) { + + var targetDir = path.dirname(templatePath); + + var targetFile = templatePath + '.js'; + var compiler = markoCompiler.createCompiler(templatePath, compilerOptions); + var isUpToDate = compiler.checkUpToDate(targetFile); + var compiledSrc; + + if (isUpToDate) { + compiledSrc = fs.readFileSync(targetFile, fsReadOptions); + } else { + var templateSrc = fs.readFileSync(templatePath, fsReadOptions); + compiledSrc = compiler.compile(templateSrc); + var filename = path.basename(targetFile); + var tempFile = path.join(targetDir, '.' + process.pid + '.' + Date.now() + '.' + filename); + fs.writeFileSync(tempFile, compiledSrc, fsReadOptions); + fs.renameSync(tempFile, targetFile); + } + + return compiledSrc + '\nexports.path=__filename;\nmodule.exports = require("marko").load(exports);'; +} + +exports.install = function(options) { + options = options || {}; + + var compilerOptions = options.compilerOptions; + + var extension = options.extension || '.marko'; + + if (extension.charAt(0) !== '.') { + extension = '.' + extension; + } + + if (require.extensions[extension]) { + return; + } + + require.extensions[extension] = function markoExtension(module, filename) { + var dirname = path.dirname(filename); + var markoCompilerModulePath = resolveFrom(dirname, 'marko/compiler'); + var markoCompiler = require(markoCompilerModulePath); + var compiledSrc = compile(filename, markoCompiler, compilerOptions); + module._compile(compiledSrc, filename + '.js'); + }; +}; \ No newline at end of file From 62dcc4e3bfb2f9c1c8d15fc9f35d90884650f8a1 Mon Sep 17 00:00:00 2001 From: Patrick Steele-Idem Date: Fri, 22 May 2015 14:23:27 -0600 Subject: [PATCH 2/9] Added @deprecated tags for top-level render and stream methods --- runtime/marko-runtime.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/runtime/marko-runtime.js b/runtime/marko-runtime.js index d223222da..1e1204855 100644 --- a/runtime/marko-runtime.js +++ b/runtime/marko-runtime.js @@ -240,10 +240,16 @@ function load(templatePath, options) { exports.load = load; +/** + * @deprecated Use load(templatePath) instead + */ exports.render = function (templatePath, data, out) { return load(templatePath).render(data, out); }; +/** + * @deprecated Use load(templatePath) instead + */ exports.stream = function(templatePath, data) { return load(templatePath).stream(data); }; From e34d957f858641e8cb802d1677fcd875cc7348c4 Mon Sep 17 00:00:00 2001 From: Patrick Steele-Idem Date: Fri, 22 May 2015 14:24:06 -0600 Subject: [PATCH 3/9] Docs for #78 Updated README.md --- README.md | 219 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 197 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index aa88bfd8f..7d6bef023 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ Marko ================ -[![Build Status](https://travis-ci.org/raptorjs/marko.svg?branch=master)](https://travis-ci.org/raptorjs/marko) [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/raptorjs/marko?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Build Status](https://travis-ci.org/raptorjs/marko.svg?branch=master)](https://travis-ci.org/raptorjs/marko) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/raptorjs/marko?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![NPM](https://nodei.co/npm/marko.png?downloads=true)](https://nodei.co/npm/marko/) @@ -28,6 +28,7 @@ Syntax highlighting available for [Atom](https://atom.io/) by installing the [la - [Another Templating Language?](#another-templating-language) - [Design Philosophy](#design-philosophy) - [Usage](#usage) + - [Template Loading](#template-loading) - [Template Rendering](#template-rendering) - [Callback API](#callback-api) - [Streaming API](#streaming-api) @@ -68,6 +69,24 @@ Syntax highlighting available for [Atom](https://atom.io/) by installing the [la - [Custom Tags and Attributes](#custom-tags-and-attributes) - [Async Taglib](#async-taglib) - [Layout Taglib](#layout-taglib) +- [API](#api) + - [require('marko')](#requiremarko) + - [Methods](#methods) + - [load(templatePath[, options]) : Template](#loadtemplatepath-options--template) + - [createWriter([stream]) : AsyncWriter](#createwriterstream--asyncwriter) + - [render(templatePath, templateData, stream.Writable)](#rendertemplatepath-templatedata-streamwritable) + - [render(templatePath, templateData, callback)](#rendertemplatepath-templatedata-callback) + - [stream(templatePath, templateData) : stream.Readable](#streamtemplatepath-templatedata--streamreadable) + - [Properties](#properties) + - [helpers](#helpers) + - [Template](#template) + - [Template](#template-1) + - [Methods](#methods-1) + - [renderSync(templateData) : String](#rendersynctemplatedata--string) + - [render(templateData, stream.Writable)](#rendertemplatedata-streamwritable) + - [render(templateData, AsyncWriter)](#rendertemplatedata-asyncwriter) + - [render(templateData, callback)](#rendertemplatedata-callback) + - [stream(templateData) : stream.Readable](#streamtemplatedata--streamreadable) - [Custom Taglibs](#custom-taglibs) - [Tag Renderer](#tag-renderer) - [marko-taglib.json](#marko-taglibjson) @@ -125,8 +144,9 @@ Hello ${data.name}! The template can then be rendered as shown in the following sample code: ```javascript -var templatePath = require.resolve('./hello.marko'); -var template = require('marko').load(templatePath); +require('marko/node-require').install(); + +var template = require('./hello.marko'); template.render({ name: 'World', @@ -169,7 +189,7 @@ Hello World! The streaming API can be used to stream the output to an HTTP response stream or any other writable stream. For example, with Express: ```javascript -var template = require('marko').load(require.resolve('./template.marko')); +var template = require('./template.marko'); app.get('/profile', function(req, res) { template.stream({ @@ -185,15 +205,15 @@ Marko also supports custom tags so you can easily extend the HTML grammar to sup Welcome to Marko! - + Content for Home - - + + Content for Profile - - + + Content for Messages - + ``` @@ -298,12 +318,38 @@ Finally, another distinguishing feature of Marko is that it supports _asynchrono # Usage +## Template Loading + +Marko provides a custom Node.js require extension that allows Marko templates to be required just like a standard JavaScript module. For example: + +```javascript +// The following line installs the Node.js require extension +// for `.marko` files. Once installed, `*.marko` files can be +// required just like any other JavaScript modules. +require('marko/node-require').install(); + +// ... + +// Load a Marko template by requiring a .marko file: +var template = require('./template.marko'); +``` + +_NOTE: The require extension only needs to be installed once, but it does not hurt if it is installed multiple times. For example, it is okay if the main app registers the require extension and an installed module also registers the require extension. The require extension will always resolve the proper `marko` module relative to the template being required so that there can still be multiple versions of marko in use for a single app._ + +However, if you prefer to not rely on the require extension for Marko templates, the following code will work as well: + +```javascript +var template = require('marko').load(require.resolve('./template.marko')); +``` + +A loaded Marko template has multiple methods for rendering the template as described in the next section. + ## Template Rendering ### Callback API ```javascript -var template = require('marko').load('template.marko'); +var template = require('./template.marko'); template.render({ name: 'Frank', @@ -404,9 +450,7 @@ Given the following module code that will be used to render a template on the cl _run.js_: ```javascript - -var templatePath = require.resolve('./hello.marko'); -var template = require('marko').load(templatePath); +var template = require('./hello.marko'); templatePath.render({ name: 'John' @@ -1232,7 +1276,7 @@ It's also possible to pass helper functions to a template as part of the view mo ```javascript -var template = require('marko').load(require.resolve('./template.marko')); +var template = require('./template.marko'); template.render({ reverse: function(str) { @@ -1380,12 +1424,143 @@ _Usage of `default-layout.marko`:_ For more details, please see [https://github.com/raptorjs/marko-layout](https://github.com/raptorjs/marko-layout). -# Custom Taglibs +# API +## require('marko') + +### Methods + +#### load(templatePath[, options]) : Template + +Loads a template instance for the given template path. + +Example usage: + +```javascript +var templatePath = require.resolve('./template.marko'); +var template = require('marko').load(templatePath); +template.render({ name: 'Frank' }, process.stdout); +``` + +Supported `options`: + +- `buffer` (`boolean`) - If `true` (default) then rendered output will be buffered until `out.flush()` is called or until rendering is completed. Otherwise, the output will be written to the underlying stream as soon as it is produced. + +#### createWriter([stream]) : AsyncWriter + +Creates an instance of an [AsyncWriter](https://github.com/raptorjs/async-writer) instance that can be used to support asynchronous rendering. + +Example usage: + +```javascript +var out = require('async-writer').create(process.stdout); +out.write('Hello'); + +var asyncOut = out.beginAsync(); +setTimeout(function() { + asyncOut.write('World') + asyncOut.end(); +}, 100); + +require('./template.marko').render({}, out); +``` + +#### render(templatePath, templateData, stream.Writable) + +Deprecated. Do not use. + +#### render(templatePath, templateData, callback) + +Deprecated. Do not use. + +#### stream(templatePath, templateData) : stream.Readable + +Deprecated. Do not use. + +### Properties + +#### helpers + +Global helpers passed to all templates. Available in compiled templates as the `__helpers` variable. It is not recommended to use this property to introduce global helpers (globals are evil). + +#### Template + +The `Template` type. + +## Template + +### Methods + +#### renderSync(templateData) : String + +Synchronously renders a template to a `String`. + +_NOTE: If `out.beginAsync()` is called during synchronous rendering an error will be thrown._ + +Example usage: + +```javascript +var template = require('./template.marko'); +var html = template.renderSync({ name: 'Frank' }); +console.log(html); +``` + +#### render(templateData, stream.Writable) + +Renders a template to a writable stream. + +Example usage: + +```javascript +var template = require('./template.marko'); +template.render({ name: 'Frank' }, process.stdout); +``` + +#### render(templateData, AsyncWriter) + +Renders a template to an [AsyncWriter](https://github.com/raptorjs/async-writer) instance that wraps an underlying stream. + +Example usage: + +```javascript +var template = require('./template.marko'); +var out = require('marko').createWriter(process.stdout); +template.render({ name: 'Frank' }, out); +``` + +#### render(templateData, callback) + +Asynchronously renders a template and provides the output to the provided callback function. + +```javascript +var template = require('./template.marko'); +template.render({ name: 'Frank' }, function(err, html, out) { + if (err) { + // Handel the error... + } + + console.log(html); +}); +``` + +_NOTE: The `out` argument will rarely be used, but it will be a reference to the [AsyncWriter](https://github.com/raptorjs/async-writer) instance that was created to faciltate rendering of the template._ + +#### stream(templateData) : stream.Readable + +Returns a readable stream that can be used to read the output of rendering a template. + +Example usage: + +```javascript +var template = require('./template.marko'); +template.stream({ name: 'Frank' }).pipe(process.stdout); +``` + +# Custom Taglibs ## Tag Renderer -Every tag should be mapped to an object with a "render" function. The render function is just a function that takes two arguments: `input` and `out`. The `input` argument is an arbitrary object that contains the input data for the renderer. The `out` argument is an [asynchronous writer](https://github.com/raptorjs/async-writer) that wraps an output stream. Output can be produced using `out.write(someString)` There is no class hierarchy or tie-ins to Marko when implementing a tag renderer. A simple tag renderer is shown below: +Every tag should be mapped to an object with a `render(input, out)` function. The render function is just a function that takes two arguments: `input` and `out`. The `input` argument is an arbitrary object that contains the input data for the renderer. The `out` argument is an [asynchronous writer](https://github.com/raptorjs/async-writer) that wraps an output stream. Output can be produced using `out.write(someString)` There is no class hierarchy or tie-ins to Marko when implementing a tag renderer. A simple tag renderer is shown below: ```javascript exports.render = function(input, out) { @@ -1573,7 +1748,7 @@ This allows a `tabs` to be provided using nested `` tags or the tab ___ui-tabs/renderer.js___ ```javascript -var template = require('marko').load(require.resolve('./template.marko')); +var template = require('./template.marko'); exports.renderer = function(input, out) { var tabs = input.tabs; @@ -1662,7 +1837,7 @@ ___ui-overlay/marko-tag.json___ The renderer for the `` tag will be similar to the following: ```javascript -var template = require('marko').load(require.resolve('./template.marko')); +var template = require('./template.marko'); exports.renderer = function(input, out) { var header = input.header; @@ -1746,7 +1921,7 @@ __Question:__ _How can Marko be used with Express?_ __Answer__: The recommended way to use Marko with Express is to bypass the Express view engine and instead have Marko render directly to the response stream as shown in the following code: ```javascript -var template = require('marko').load(require.resolve('./template.marko')); +var template = require('./template.marko'); app.get('/profile', function(req, res) { template @@ -1761,7 +1936,7 @@ With this approach, you can benefit from streaming and there is no middleman (le Alternatively, you can use the streaming API to produce an intermediate stream that can then be piped to the response stream as shown below: ```javascript -var template = require('view-engine').load(require.resolve('./template.marko')); +var template = require('./template.marko'); app.get('/profile', function(req, res) { template.stream({ @@ -1813,7 +1988,7 @@ See [CHANGELOG.md](CHANGELOG.md) # Discuss -Chat channel: [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/raptorjs/marko?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +Chat channel: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/raptorjs/marko?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) Questions or comments can also be posted on the [RaptorJS Google Groups Discussion Forum](http://groups.google.com/group/raptorjs). From c0f497443e73b6f2b07588103711a9cd14310f77 Mon Sep 17 00:00:00 2001 From: Patrick Steele-Idem Date: Fri, 22 May 2015 14:24:14 -0600 Subject: [PATCH 4/9] Added code comments --- node-require.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/node-require.js b/node-require.js index 7a7033e45..414e6572e 100644 --- a/node-require.js +++ b/node-require.js @@ -17,12 +17,21 @@ function compile(templatePath, markoCompiler, compilerOptions) { } else { var templateSrc = fs.readFileSync(templatePath, fsReadOptions); compiledSrc = compiler.compile(templateSrc); + + // Write to a temporary file and move it into place to avoid problems + // assocatiated with multiple processes write to teh same file. We only + // write the compiled source code to disk so that stack traces will + // be accurate. var filename = path.basename(targetFile); var tempFile = path.join(targetDir, '.' + process.pid + '.' + Date.now() + '.' + filename); fs.writeFileSync(tempFile, compiledSrc, fsReadOptions); 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);'; } @@ -42,10 +51,18 @@ exports.install = function(options) { } require.extensions[extension] = function markoExtension(module, filename) { + // Resolve the appropriate compiler relative to the location of the + // marko template file on disk using the "resolve-from" module. var dirname = path.dirname(filename); var markoCompilerModulePath = resolveFrom(dirname, 'marko/compiler'); var markoCompiler = require(markoCompilerModulePath); + + // Now use the appropriate Marko compiler to compile the Marko template + // file to JavaScript source code: var compiledSrc = compile(filename, markoCompiler, compilerOptions); + + // Append ".js" to the filename since that is where we write the compiled + // source code that is being loaded. This allows stack traces to match up. module._compile(compiledSrc, filename + '.js'); }; }; \ No newline at end of file From db951ce90ddc2ce02ae9341e4f217359ae61416e Mon Sep 17 00:00:00 2001 From: Patrick Steele-Idem Date: Fri, 22 May 2015 14:28:14 -0600 Subject: [PATCH 5/9] Updated changelog for #78 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81679de91..c0c2300e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ Changelog # 2.x +## 2.5.x + +### 2.5.0 + +- Fixes #78 - Custom Node.js require extension for Marko template files + ## 2.4.x ### 2.4.3 From 41e92df68c1349c329864fad2df2a37e7b38939b Mon Sep 17 00:00:00 2001 From: Patrick Steele-Idem Date: Fri, 22 May 2015 15:20:28 -0600 Subject: [PATCH 6/9] Fixes #78 - Passthrough if a provided template is already a loaded template instance --- runtime/marko-runtime.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/runtime/marko-runtime.js b/runtime/marko-runtime.js index 1e1204855..3943db670 100644 --- a/runtime/marko-runtime.js +++ b/runtime/marko-runtime.js @@ -218,6 +218,11 @@ function load(templatePath, options) { template._ = loader(templatePath).create(helpers); // Load the template factory and invoke it } } else { + // If the first argument is already a loaded template then just return it + if (templatePath.render) { + return 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 From 2ddf263a16d1103355eb46c4463be7450b61f78f Mon Sep 17 00:00:00 2001 From: Patrick Steele-Idem Date: Tue, 26 May 2015 15:02:31 -0600 Subject: [PATCH 7/9] #78 Added symbolic links to marko so that compiled templates can require the marko runtime --- test/node_modules/marko | 1 + 1 file changed, 1 insertion(+) create mode 120000 test/node_modules/marko diff --git a/test/node_modules/marko b/test/node_modules/marko new file mode 120000 index 000000000..6581736d6 --- /dev/null +++ b/test/node_modules/marko @@ -0,0 +1 @@ +../../ \ No newline at end of file From 7c00085380ea318a9b9a935760e5e323d12612b7 Mon Sep 17 00:00:00 2001 From: Patrick Steele-Idem Date: Tue, 26 May 2015 16:57:39 -0600 Subject: [PATCH 8/9] #78 Change compiled templates to export a loaded Template instance --- compiler/TemplateBuilder.js | 7 +- hot-reload/index.js | 10 +- node-require.js | 7 +- runtime/marko-runtime.js | 98 ++++++++++------- test/api-tests.js | 19 ++++ .../templates/compiler/custom-tag/expected.js | 5 +- .../templates/compiler/entities/expected.js | 5 +- .../compiler/hello-dynamic/expected.js | 5 +- .../templates/compiler/simple/expected.js | 5 +- test/hot-reload-test.js | 61 +++++++++++ test/legacy-compiled-test.js | 103 ++++++++++++++++++ test/temp/.gitignore | 1 + 12 files changed, 270 insertions(+), 56 deletions(-) create mode 100644 test/hot-reload-test.js create mode 100644 test/legacy-compiled-test.js create mode 100644 test/temp/.gitignore diff --git a/compiler/TemplateBuilder.js b/compiler/TemplateBuilder.js index 9f6356a3a..1d9798c4a 100644 --- a/compiler/TemplateBuilder.js +++ b/compiler/TemplateBuilder.js @@ -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) { diff --git a/hot-reload/index.js b/hot-reload/index.js index f7c724f92..112ae1939 100644 --- a/hot-reload/index.js +++ b/hot-reload/index.js @@ -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; diff --git a/node-require.js b/node-require.js index 414e6572e..0246e9428 100644 --- a/node-require.js +++ b/node-require.js @@ -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) { diff --git a/runtime/marko-runtime.js b/runtime/marko-runtime.js index 3943db670..5cbeaccc6 100644 --- a/runtime/marko-runtime.js +++ b/runtime/marko-runtime.js @@ -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; diff --git a/test/api-tests.js b/test/api-tests.js index d442e9df3..a40086a8d 100644 --- a/test/api-tests.js +++ b/test/api-tests.js @@ -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(); + }); + }); + }); diff --git a/test/fixtures/templates/compiler/custom-tag/expected.js b/test/fixtures/templates/compiler/custom-tag/expected.js index 976094499..e86a2f4a5 100644 --- a/test/fixtures/templates/compiler/custom-tag/expected.js +++ b/test/fixtures/templates/compiler/custom-tag/expected.js @@ -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" }); }; -} \ No newline at end of file +} +(module.exports = require("marko").c(__filename)).c(create); \ No newline at end of file diff --git a/test/fixtures/templates/compiler/entities/expected.js b/test/fixtures/templates/compiler/entities/expected.js index 0b66e9457..941c63340 100755 --- a/test/fixtures/templates/compiler/entities/expected.js +++ b/test/fixtures/templates/compiler/entities/expected.js @@ -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¢'); }; -} \ No newline at end of file +} +(module.exports = require("marko").c(__filename)).c(create); \ No newline at end of file diff --git a/test/fixtures/templates/compiler/hello-dynamic/expected.js b/test/fixtures/templates/compiler/hello-dynamic/expected.js index a2149c933..ef40614f5 100644 --- a/test/fixtures/templates/compiler/hello-dynamic/expected.js +++ b/test/fixtures/templates/compiler/hello-dynamic/expected.js @@ -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) + '!'); }; -} \ No newline at end of file +} +(module.exports = require("marko").c(__filename)).c(create); \ No newline at end of file diff --git a/test/fixtures/templates/compiler/simple/expected.js b/test/fixtures/templates/compiler/simple/expected.js index ca384c89f..95bd94712 100644 --- a/test/fixtures/templates/compiler/simple/expected.js +++ b/test/fixtures/templates/compiler/simple/expected.js @@ -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'); }; -} \ No newline at end of file +} +(module.exports = require("marko").c(__filename)).c(create); \ No newline at end of file diff --git a/test/hot-reload-test.js b/test/hot-reload-test.js new file mode 100644 index 000000000..8dc4d2c15 --- /dev/null +++ b/test/hot-reload-test.js @@ -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!!'); + }); + +}); diff --git a/test/legacy-compiled-test.js b/test/legacy-compiled-test.js new file mode 100644 index 000000000..f99f46d39 --- /dev/null +++ b/test/legacy-compiled-test.js @@ -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); + }); + +}); diff --git a/test/temp/.gitignore b/test/temp/.gitignore new file mode 100644 index 000000000..86f8857ef --- /dev/null +++ b/test/temp/.gitignore @@ -0,0 +1 @@ +*.marko \ No newline at end of file From dcb2404d04e0b96a50e2f9848a7b87e75a6679de Mon Sep 17 00:00:00 2001 From: Patrick Steele-Idem Date: Tue, 26 May 2015 16:59:10 -0600 Subject: [PATCH 9/9] #78 Updated CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0c2300e5..faaf9e523 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Changelog ### 2.5.0 - Fixes #78 - Custom Node.js require extension for Marko template files +- Compiled templates now export a loaded Template instance. In the previous version of marko, compiled templates exported a function that could be used to create a loaded Template instance. ## 2.4.x