├── .eslintrc ├── .gitignore ├── .jshintignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── LICENCE.txt ├── README.md ├── examples ├── benchmark.js ├── delegates.soy ├── errors.soy ├── example.js └── message.soy ├── lib ├── SoyCompiler.js ├── SoyOptions.js ├── SoyVmContext.js ├── copy.js └── soynode.js ├── package.json ├── package.json.orig └── test ├── assets ├── template1.soy ├── template1_namespace.js ├── template2.soy ├── template3.soy ├── translations_es.xlf └── translations_pt-BR.xlf └── test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": 0, 4 | "quotes": [ 5 | 2, 6 | "single" 7 | ], 8 | "linebreak-style": [ 9 | 2, 10 | "unix" 11 | ], 12 | "semi": [ 13 | 2, 14 | "never" 15 | ], 16 | "no-unused-vars": [ 17 | 2, 18 | {"args": "after-used", "argsIgnorePattern": "^var_args$"} 19 | ], 20 | "no-constant-condition": [0] 21 | }, 22 | "env": { 23 | "node": true 24 | }, 25 | "extends": "eslint:recommended" 26 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | lib-cov 3 | *.seed 4 | *.log 5 | *.csv 6 | *.dat 7 | *.out 8 | *.pid 9 | *.gz 10 | 11 | pids 12 | logs 13 | results 14 | 15 | node_modules 16 | npm-debug.log 17 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | examples -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | 4 | "asi": true, // Tolerate trailing semicolons 5 | "eqeqeq": false, // Require triple equals i.e. `===`. 6 | "eqnull": true, // Tolerate use of `== null`. 7 | "-W041": true // Tolerate use of `== 0`. 8 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | test 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "0.12" 5 | - "4.1" 6 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2012 The Obvious Corporation. 2 | http://obvious.com/ 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | 17 | ------------------------------------------------------------------------- 18 | Apache License 19 | Version 2.0, January 2004 20 | http://www.apache.org/licenses/ 21 | 22 | 23 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 24 | 25 | 1. Definitions. 26 | 27 | "License" shall mean the terms and conditions for use, reproduction, 28 | and distribution as defined by Sections 1 through 9 of this document. 29 | 30 | "Licensor" shall mean the copyright owner or entity authorized by 31 | the copyright owner that is granting the License. 32 | 33 | "Legal Entity" shall mean the union of the acting entity and all 34 | other entities that control, are controlled by, or are under common 35 | control with that entity. For the purposes of this definition, 36 | "control" means (i) the power, direct or indirect, to cause the 37 | direction or management of such entity, whether by contract or 38 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 39 | outstanding shares, or (iii) beneficial ownership of such entity. 40 | 41 | "You" (or "Your") shall mean an individual or Legal Entity 42 | exercising permissions granted by this License. 43 | 44 | "Source" form shall mean the preferred form for making modifications, 45 | including but not limited to software source code, documentation 46 | source, and configuration files. 47 | 48 | "Object" form shall mean any form resulting from mechanical 49 | transformation or translation of a Source form, including but 50 | not limited to compiled object code, generated documentation, 51 | and conversions to other media types. 52 | 53 | "Work" shall mean the work of authorship, whether in Source or 54 | Object form, made available under the License, as indicated by a 55 | copyright notice that is included in or attached to the work 56 | (an example is provided in the Appendix below). 57 | 58 | "Derivative Works" shall mean any work, whether in Source or Object 59 | form, that is based on (or derived from) the Work and for which the 60 | editorial revisions, annotations, elaborations, or other modifications 61 | represent, as a whole, an original work of authorship. For the purposes 62 | of this License, Derivative Works shall not include works that remain 63 | separable from, or merely link (or bind by name) to the interfaces of, 64 | the Work and Derivative Works thereof. 65 | 66 | "Contribution" shall mean any work of authorship, including 67 | the original version of the Work and any modifications or additions 68 | to that Work or Derivative Works thereof, that is intentionally 69 | submitted to Licensor for inclusion in the Work by the copyright owner 70 | or by an individual or Legal Entity authorized to submit on behalf of 71 | the copyright owner. For the purposes of this definition, "submitted" 72 | means any form of electronic, verbal, or written communication sent 73 | to the Licensor or its representatives, including but not limited to 74 | communication on electronic mailing lists, source code control systems, 75 | and issue tracking systems that are managed by, or on behalf of, the 76 | Licensor for the purpose of discussing and improving the Work, but 77 | excluding communication that is conspicuously marked or otherwise 78 | designated in writing by the copyright owner as "Not a Contribution." 79 | 80 | "Contributor" shall mean Licensor and any individual or Legal Entity 81 | on behalf of whom a Contribution has been received by Licensor and 82 | subsequently incorporated within the Work. 83 | 84 | 2. Grant of Copyright License. Subject to the terms and conditions of 85 | this License, each Contributor hereby grants to You a perpetual, 86 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 87 | copyright license to reproduce, prepare Derivative Works of, 88 | publicly display, publicly perform, sublicense, and distribute the 89 | Work and such Derivative Works in Source or Object form. 90 | 91 | 3. Grant of Patent License. Subject to the terms and conditions of 92 | this License, each Contributor hereby grants to You a perpetual, 93 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 94 | (except as stated in this section) patent license to make, have made, 95 | use, offer to sell, sell, import, and otherwise transfer the Work, 96 | where such license applies only to those patent claims licensable 97 | by such Contributor that are necessarily infringed by their 98 | Contribution(s) alone or by combination of their Contribution(s) 99 | with the Work to which such Contribution(s) was submitted. If You 100 | institute patent litigation against any entity (including a 101 | cross-claim or counterclaim in a lawsuit) alleging that the Work 102 | or a Contribution incorporated within the Work constitutes direct 103 | or contributory patent infringement, then any patent licenses 104 | granted to You under this License for that Work shall terminate 105 | as of the date such litigation is filed. 106 | 107 | 4. Redistribution. You may reproduce and distribute copies of the 108 | Work or Derivative Works thereof in any medium, with or without 109 | modifications, and in Source or Object form, provided that You 110 | meet the following conditions: 111 | 112 | (a) You must give any other recipients of the Work or 113 | Derivative Works a copy of this License; and 114 | 115 | (b) You must cause any modified files to carry prominent notices 116 | stating that You changed the files; and 117 | 118 | (c) You must retain, in the Source form of any Derivative Works 119 | that You distribute, all copyright, patent, trademark, and 120 | attribution notices from the Source form of the Work, 121 | excluding those notices that do not pertain to any part of 122 | the Derivative Works; and 123 | 124 | (d) If the Work includes a "NOTICE" text file as part of its 125 | distribution, then any Derivative Works that You distribute must 126 | include a readable copy of the attribution notices contained 127 | within such NOTICE file, excluding those notices that do not 128 | pertain to any part of the Derivative Works, in at least one 129 | of the following places: within a NOTICE text file distributed 130 | as part of the Derivative Works; within the Source form or 131 | documentation, if provided along with the Derivative Works; or, 132 | within a display generated by the Derivative Works, if and 133 | wherever such third-party notices normally appear. The contents 134 | of the NOTICE file are for informational purposes only and 135 | do not modify the License. You may add Your own attribution 136 | notices within Derivative Works that You distribute, alongside 137 | or as an addendum to the NOTICE text from the Work, provided 138 | that such additional attribution notices cannot be construed 139 | as modifying the License. 140 | 141 | You may add Your own copyright statement to Your modifications and 142 | may provide additional or different license terms and conditions 143 | for use, reproduction, or distribution of Your modifications, or 144 | for any such Derivative Works as a whole, provided Your use, 145 | reproduction, and distribution of the Work otherwise complies with 146 | the conditions stated in this License. 147 | 148 | 5. Submission of Contributions. Unless You explicitly state otherwise, 149 | any Contribution intentionally submitted for inclusion in the Work 150 | by You to the Licensor shall be under the terms and conditions of 151 | this License, without any additional terms or conditions. 152 | Notwithstanding the above, nothing herein shall supersede or modify 153 | the terms of any separate license agreement you may have executed 154 | with Licensor regarding such Contributions. 155 | 156 | 6. Trademarks. This License does not grant permission to use the trade 157 | names, trademarks, service marks, or product names of the Licensor, 158 | except as required for reasonable and customary use in describing the 159 | origin of the Work and reproducing the content of the NOTICE file. 160 | 161 | 7. Disclaimer of Warranty. Unless required by applicable law or 162 | agreed to in writing, Licensor provides the Work (and each 163 | Contributor provides its Contributions) on an "AS IS" BASIS, 164 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 165 | implied, including, without limitation, any warranties or conditions 166 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 167 | PARTICULAR PURPOSE. You are solely responsible for determining the 168 | appropriateness of using or redistributing the Work and assume any 169 | risks associated with Your exercise of permissions under this License. 170 | 171 | 8. Limitation of Liability. In no event and under no legal theory, 172 | whether in tort (including negligence), contract, or otherwise, 173 | unless required by applicable law (such as deliberate and grossly 174 | negligent acts) or agreed to in writing, shall any Contributor be 175 | liable to You for damages, including any direct, indirect, special, 176 | incidental, or consequential damages of any character arising as a 177 | result of this License or out of the use or inability to use the 178 | Work (including but not limited to damages for loss of goodwill, 179 | work stoppage, computer failure or malfunction, or any and all 180 | other commercial damages or losses), even if such Contributor 181 | has been advised of the possibility of such damages. 182 | 183 | 9. Accepting Warranty or Additional Liability. While redistributing 184 | the Work or Derivative Works thereof, You may choose to offer, 185 | and charge a fee for, acceptance of support, warranty, indemnity, 186 | or other liability obligations and/or rights consistent with this 187 | License. However, in accepting such obligations, You may act only 188 | on Your own behalf and on Your sole responsibility, not on behalf 189 | of any other Contributor, and only if You agree to indemnify, 190 | defend, and hold each Contributor harmless for any liability 191 | incurred by, or claims asserted against, such Contributor by reason 192 | of your accepting any such warranty or additional liability. 193 | 194 | END OF TERMS AND CONDITIONS 195 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | soynode 2 | ======= 3 | 4 | [![Build Status](https://travis-ci.org/Medium/soynode.svg)](https://travis-ci.org/Medium/soynode) 5 | 6 | Utility for working with [Closure Templates](https://developers.google.com/closure/templates/), 7 | aka Soy, from within a node.js application. Supports dynamic recompilation and loading for fast 8 | iteration during development. 9 | 10 | Installing 11 | ---------- 12 | 13 | Either: 14 | 15 | 1. Fork, clone or download the source from GitHub, or 16 | 2. Install via NPM using `npm install soynode` 17 | 18 | 19 | Usage 20 | ----- 21 | 22 | ```js 23 | var soynode = require('../lib/soynode') 24 | 25 | soynode.setOptions({ 26 | outputDir: '/tmp/soynode-example' 27 | , allowDynamicRecompile: true 28 | }) 29 | 30 | soynode.compileTemplates(__dirname, function (err) { 31 | if (err) throw err 32 | // Templates are now ready to use. 33 | console.log(soynode.render('example.message.hello', { 34 | name: process.env.USER 35 | , date: new Date().toLocaleTimeString() 36 | })) 37 | }) 38 | ``` 39 | 40 | Also, see `examples/example.js`. 41 | 42 | `soynode.get(templatename)` - Returns a JS function corresponding to the template name. 43 | 44 | `soynode.render(templatename, data)` - Returns a string that results from executing a template. 45 | 46 | `soynode.setOptions(opts)` - Change the options, see section below. 47 | 48 | `soynode.compileTemplates(dir, callback)` - Compiles and loads all `.soy` files in the directory. 49 | 50 | `soynode.compileTemplateFiles(files, callback)` - Compiles and loads all `.soy` files. 51 | 52 | `soynode.loadCompiledTemplates(dir, callback)` - Loads already compiled templates. 53 | 54 | `soynode.loadCompiledTemplateFiles(files, callback)` - Loads already compiled templates. 55 | 56 | Where "template name" is referred to, it means the namespace + template name as defined in the Soy 57 | file, and the full JS name that the Soy Compiler generates, for example `project.section.screen`. 58 | See the [Hello World JS](https://developers.google.com/closure/templates/docs/helloworld_js) doc on 59 | the Closure site for more background. 60 | 61 | Options 62 | ------- 63 | 64 | Options can be set via `soynode.setOptions(options)`. Most of these mirror 65 | [the command-line arguments](https://developers.google.com/closure/templates/docs/javascript_usage) 66 | for `SoyToJsSrcCompiler`. The keys can contain the following: 67 | 68 | - `tmpDir` {string} Path to a directory where temporary files will be written during compilation. {Deprecated: use outputDir} 69 | [Default: /tmp/soynode] 70 | - `inputDir` {string} Optional path to a directory where files will be read. When compiled from a directory, this option will be overwritten with the caller inputDir. [Default: process.cwd()] 71 | - `outputDir` {string} Path to a directory where files will be written. [Default: null] 72 | - `uniqueDir` {boolean} Determines whether the compiled files will be placed in a unique directory. [Default: true] 73 | - `allowDynamicRecompile` {boolean} Whether to watch for changes to the templates. [Default: false] 74 | - `loadCompiledTemplates` {boolean} Whether or not to load the compiled templates. Relevant when you only need to build templates. [Default: true] 75 | - `eraseTemporaryFiles` {boolean} Whether to erase temporary files after a compilation. This option does nothing if allowDynamicRecompile is on, because allowDynamicRecompile reuses the files. 76 | [Default: false] 77 | - `concatOutput` {boolean} Whether the compiled soy.js files should be joined into a single file. This is helpful for loading templates in a browser and simplest to use when `outputDir` is explicitly set and `uniqueDir` is false. [Default: false] 78 | - `concatFileName` {string} File name used for concatenated files, only relevant when concatOutput is true, ".soy.concat.js" is appended, so don't include ".js" yourself. [Default: compiled] 79 | - `locales` {Array.} List of locales to translate the templates to. 80 | - `messageFilePathFormat` {string} Path to the translation file to use, which can contain any of the placeholders allowed on the --messageFilePathFormat option of SoyToJsSrcCompiler.jar. 81 | - `cssHandlingScheme` {string} Processing options for the `css` command. [More info](https://developers.google.com/closure/templates/docs/commands#css) 82 | - `useClosureStyle` {boolean} Whether or not to use goog.provide and goog.require for JS functions and Soy namespaces. 83 | - `shouldGenerateJsdoc` {boolean} Whether or not to generate JSDoc on each template function, with type info for the Closure Compiler. [More info](https://developers.google.com/closure/templates/docs/javascript_usage) 84 | - `shouldProvideRequireSoyNamespaces` {boolean} Use this option to enable usage with Closure Library. [More info](https://developers.google.com/closure/templates/docs/javascript_usage) 85 | - `shouldProvideRequireJsFunctions` {boolean} Use this option to enable usage with Closure Library. [More info](https://developers.google.com/closure/templates/docs/javascript_usage) 86 | - `precompiledDir` {string} A directory of precompiled soy.js files. 87 | Soynode will check these first and use them if available. 88 | You can set this to the same value as outputDir to re-use results from previous runs. 89 | 90 | **NOTE: Options should be set before templates are loaded or compiled.** 91 | 92 | Internationalizion 93 | -------------------- 94 | 95 | To take advantage of soy's [translation](https://developers.google.com/closure/templates/docs/translation) features through soynode, you should set the `locales` and `messageFilePathFormat` options, like in the example below: 96 | 97 | ```js 98 | var soynode = require('../lib/soynode') 99 | 100 | soynode.setOptions({ 101 | locales: ['pt-BR', 'es'], 102 | messageFilePathFormat: '/tmp/soynode-example/translations.xlf', 103 | outputDir: '/tmp/soynode-example' 104 | }) 105 | 106 | soynode.compileTemplates(__dirname, function (err) { 107 | if (err) throw err 108 | // Templates are now ready to use, render specifying the desired locale. 109 | console.log(soynode.render('example.message.hello', {}, {}, 'pt-BR')) 110 | console.log(soynode.render('example.message.hello', {}, {}, 'es')) 111 | }) 112 | ``` 113 | 114 | Implementation Notes 115 | -------------------- 116 | 117 | The templates are loaded using Node's [VM Module](http://nodejs.org/api/vm.html). This allows us to 118 | execute the generated `.soy.js` files as is without a post processing step and without leaking the 119 | template functions into the global scope. 120 | 121 | Calling `soynode.get` executes code which returns a reference to the template function within the 122 | VM Context. The reference is cached, providing a 10x speed up over fetching the template function 123 | each time, or evaluating it in place and returning the template output over the VM boundary. 124 | 125 | Contributing 126 | ------------ 127 | 128 | Questions, comments, bug reports, and pull requests are all welcome. Submit them at 129 | [the project on GitHub](https://github.com/Obvious/soynode/). 130 | 131 | Bug reports that include steps-to-reproduce (including code) are the best. Even better, make them in 132 | the form of pull requests. 133 | 134 | Author 135 | ------ 136 | 137 | [Dan Pupius](https://github.com/dpup) 138 | ([personal website](http://pupius.co.uk/about/)), supported by 139 | [The Obvious Corporation](http://obvious.com/). 140 | 141 | License 142 | ------- 143 | 144 | Copyright 2012 [The Obvious Corporation](http://obvious.com/). 145 | 146 | Licensed under the Apache License, Version 2.0. 147 | See the top-level file `LICENSE.txt` and 148 | (http://www.apache.org/licenses/LICENSE-2.0). 149 | -------------------------------------------------------------------------------- /examples/benchmark.js: -------------------------------------------------------------------------------- 1 | // Copyright (c)2012 The Obvious Corporation 2 | 3 | /** 4 | * @fileoverview Basic benchmark for hello world template that calls one nested template. 5 | */ 6 | 7 | var soynode = require('../lib/soynode') 8 | 9 | var USER = process.env.USER || 'Stranger' 10 | 11 | soynode.setOptions({ 12 | outputDir: '/tmp/soynode-example' 13 | , uniqueDir: true 14 | , eraseTemporaryFiles: true 15 | }) 16 | 17 | var iterations = 5000 18 | 19 | soynode.compileTemplates(__dirname, function (err) { 20 | if (err) throw err 21 | var d = Date.now() 22 | var results = [] 23 | for (var i = 0; i < iterations; i++) { 24 | results.push(soynode.render('example.message.hello', { 25 | name: USER 26 | , date: new Date().toLocaleTimeString() 27 | })) 28 | } 29 | var diff = Date.now() - d 30 | 31 | console.log('Total time spent:', diff, 'ms') 32 | console.log('Number of iterations:', iterations) 33 | console.log('Ops per sec:', Math.round((1000 / diff) * iterations)) 34 | }) 35 | -------------------------------------------------------------------------------- /examples/delegates.soy: -------------------------------------------------------------------------------- 1 | {namespace example.delegates autoescape="contextual"} 2 | 3 | /** 4 | * Template for testing delegates 5 | * @param variantToUse 6 | */ 7 | {template .test} 8 | Using delegate : {delcall example.delegates.delegated variant="$variantToUse" /} 9 | {/template} 10 | 11 | /** Implementation 'alpha'. */ 12 | {deltemplate example.delegates.delegated variant="'alpha'"} 13 | Alpha!! 14 | {/deltemplate} 15 | 16 | /** Implementation 'beta'. */ 17 | {deltemplate example.delegates.delegated variant="'beta'"} 18 | Beta?? 19 | {/deltemplate} 20 | -------------------------------------------------------------------------------- /examples/errors.soy: -------------------------------------------------------------------------------- 1 | {namespace example.errors} 2 | 3 | 4 | /** 5 | * Template for 404 pages. 6 | */ 7 | {template .error404} 8 |

Not Found!

9 | {/template} 10 | 11 | 12 | /** 13 | * Template for 500 errors. 14 | */ 15 | {template .error500} 16 |

Oops, my bad

17 | {/template} 18 | 19 | -------------------------------------------------------------------------------- /examples/example.js: -------------------------------------------------------------------------------- 1 | // Copyright (c)2012 The Obvious Corporation 2 | 3 | /** 4 | * @fileoverview Very basic example showing usage of `soynode`. Try changing message.soy while 5 | * running this example to see the effects of dynamic recompilation. 6 | */ 7 | 8 | var soynode = require('../lib/soynode') 9 | 10 | var USER = process.env.USER || 'Stranger' 11 | 12 | soynode.setOptions({ 13 | outputDir: '/tmp/soynode-example' 14 | , uniqueDir: true 15 | , allowDynamicRecompile: true 16 | , eraseTemporaryFiles: true 17 | }) 18 | 19 | soynode.compileTemplates(__dirname, function (err) { 20 | if (err) throw err 21 | 22 | console.log('Templates are ready, Ctrl-C to exit') 23 | 24 | setInterval(function () { 25 | console.log(soynode.render('example.message.hello', { 26 | name: USER 27 | , date: new Date().toLocaleTimeString() 28 | , variantToUse: Date.now() % 2 ? 'alpha' : 'beta' 29 | })) 30 | }, 1000) 31 | 32 | process.on('SIGINT', function () { 33 | console.log(soynode.render('example.message.bye', { 34 | name: USER 35 | })) 36 | process.exit(0) 37 | }) 38 | 39 | }) 40 | 41 | -------------------------------------------------------------------------------- /examples/message.soy: -------------------------------------------------------------------------------- 1 | {namespace example.message} 2 | 3 | /** 4 | * Shows a hello message. 5 | * @param date The current data 6 | */ 7 | {template .hello} 8 | Hello {call .name data="all" /}, it is {$date}! 9 | {call example.delegates.test data="all" /} 10 | {/template} 11 | 12 | 13 | /** 14 | * A goodbye message 15 | */ 16 | {template .bye} 17 | Goodbye {call .name data="all" /}, sorry to see you go. 18 | {/template} 19 | 20 | 21 | /** 22 | * @param name The user's name 23 | */ 24 | {template .name} 25 | __{$name}__ 26 | {/template} 27 | -------------------------------------------------------------------------------- /lib/SoyCompiler.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014. A Medium Corporation. 2 | 3 | var SoyOptions = require('./SoyOptions') 4 | var SoyVmContext = require('./SoyVmContext') 5 | 6 | var EventEmitter = require('events').EventEmitter 7 | var child_process = require('child_process') 8 | var exec = child_process.exec 9 | var closureTemplates = require('closure-templates') 10 | var fs = require('fs-extra') 11 | var path = require('path') 12 | var Q = require('q') 13 | var copy = require('./copy') 14 | 15 | 16 | /** 17 | * The key in vmContexts for the default vm context (with no locale). 18 | */ 19 | var DEFAULT_VM_CONTEXT = 'default' 20 | 21 | 22 | /** 23 | * Resolved path to the executable jar for the Closure Template compiler. 24 | * @type {string} 25 | */ 26 | var PATH_TO_SOY_JAR = closureTemplates['SoyToJsSrcCompiler.jar'] 27 | 28 | 29 | /** 30 | * The main public API of soynode. 31 | * @constructor 32 | */ 33 | function SoyCompiler() { 34 | /** @private {SoyOptions} */ 35 | this._options = this.getDefaultOptions() 36 | 37 | /** 38 | * VM Context that is used as the global when fetching templates. The end result is that this 39 | * object contains references to the JS functions rendered by Soy. 40 | * @type {Object.} 41 | */ 42 | this._vmContexts = {} 43 | 44 | /** 45 | * Map of filenames that have a watch to the last time it was called. 46 | * @param {Object.} 47 | */ 48 | this._watches = {} 49 | } 50 | 51 | 52 | /** @return {SoyOptions} */ 53 | SoyCompiler.prototype.getDefaultOptions = function () { 54 | return new SoyOptions() 55 | } 56 | 57 | 58 | /** 59 | * Sets options which affect how soynode operates. 60 | * @param {{ 61 | * tmpDir: string=, //Deprecated 62 | * outputDir: string=, 63 | * uniqueDir: boolean=, 64 | * allowDynamicRecompile: boolean=, 65 | * eraseTemporaryFiles: boolean=}}} opts 66 | */ 67 | SoyCompiler.prototype.setOptions = function (opts) { 68 | this._options.merge(opts) 69 | } 70 | 71 | 72 | /** 73 | * Gets a reference to a template function. 74 | * 75 | * Note: If dynamic recompilation is enabled the reference will not get updated. 76 | * 77 | * @param {string} templateName 78 | * @param {string=} vmType optional type of the vm 79 | * @return {function (Object) : string} 80 | */ 81 | SoyCompiler.prototype.get = function (templateName, vmType) { 82 | return this.getSoyVmContext(vmType || DEFAULT_VM_CONTEXT).get(templateName) 83 | } 84 | 85 | 86 | /** 87 | * Renders a template using the provided data and returns the resultant string. 88 | * @param {string} templateName 89 | * @param {Object=} data 90 | * @param {Object=} injectedData optional injected data available via $ij 91 | * @param {string=} vmType optional type of the vm 92 | * @return {string} 93 | */ 94 | SoyCompiler.prototype.render = function (templateName, data, injectedData, vmType) { 95 | // Certain autoescape modes of closure-templates return a Content object 96 | // instead of a string, so force a string. 97 | return String(this.get(templateName, vmType)(data, null, injectedData)) 98 | } 99 | 100 | 101 | /** 102 | * Gets the SoyVmContext object for the for the given locale, or the default if no locale is given. 103 | * 104 | * @param {string=} vmType optional type of the vm 105 | */ 106 | SoyCompiler.prototype.getSoyVmContext = function (vmType) { 107 | vmType = vmType || DEFAULT_VM_CONTEXT 108 | 109 | if (!this._vmContexts[vmType]) { 110 | this._vmContexts[vmType] = new SoyVmContext(vmType, this._options) 111 | } 112 | 113 | return this._vmContexts[vmType] 114 | } 115 | 116 | 117 | /** 118 | * Gets the vm context for the given locale, or the default if no locale is given. 119 | * 120 | * @param {string=} vmType optional type of the vm 121 | * @return {Object} 122 | */ 123 | SoyCompiler.prototype.getVMContext = function (vmType) { 124 | return this.getSoyVmContext(vmType).getContext() 125 | } 126 | 127 | 128 | /** 129 | * Compiles all soy files within the provided directory and loads them into memory. The callback 130 | * will be called when templates are ready, or an error occurred along the way. 131 | * @param {string} inputDir 132 | * @param {function (Error, boolean)=} callback 133 | * @return {EventEmitter} An EventEmitter that publishes a "compile" event after every compile 134 | * This is particularly useful if you have allowDynamicRecompile on, so that your server 135 | * can propagate the error appropriately. The "compile" event has two arguments: (error, success). 136 | */ 137 | SoyCompiler.prototype.compileTemplates = function (inputDir, callback) { 138 | var options = this._options 139 | var emitter = new EventEmitter() 140 | if (options.allowDynamicRecompile) { 141 | emitter.on('compile', logErrorOrDone) 142 | } 143 | if (callback) { 144 | emitter.once('compile', callback) 145 | } 146 | this._compileTemplatesAndEmit(inputDir, emitter) 147 | return emitter 148 | } 149 | 150 | 151 | /** 152 | * Compiles all soy files within the provided array and loads them into memory. The callback 153 | * will be called when templates are ready, or an error occurred along the way. 154 | * @param {Array.} files 155 | * @param {function (Error, boolean)=} callback 156 | * @return {EventEmitter} An EventEmitter that publishes a "compile" event after every compile. 157 | */ 158 | SoyCompiler.prototype.compileTemplateFiles = function (files, callback) { 159 | var emitter = new EventEmitter() 160 | if (callback) { 161 | emitter.once('compile', callback) 162 | } 163 | var outputDir = this._createOutputDir() 164 | var inputDir = this._options.inputDir 165 | var self = this 166 | this._maybeUsePrecompiledFiles(outputDir, files) 167 | .then(function (dirtyFiles) { 168 | self._maybeSetupDynamicRecompile(inputDir, outputDir, files, emitter) 169 | self._compileTemplateFilesAndEmit(inputDir, outputDir, files, dirtyFiles, emitter) 170 | }) 171 | .done() 172 | return emitter 173 | } 174 | 175 | 176 | /** 177 | * Resolves the output directory from the current options. 178 | * @return {string} 179 | * @private 180 | */ 181 | SoyCompiler.prototype._createOutputDir = function () { 182 | var options = this._options 183 | var dir = options.outputDir || options.tmpDir 184 | if (options.uniqueDir !== false) { 185 | var timeDirectory = new Date().toISOString().replace(/\:/g, '_') 186 | dir = path.join(dir, timeDirectory) 187 | } 188 | return dir 189 | } 190 | 191 | 192 | /** 193 | * Compiles all soy files, but takes an emitter to use instead of a callback. 194 | * @see compileTemplates for the emitter API. 195 | * @param {string} inputDir Input directory from where the compiler spawns. 196 | * @param {string} outputDir 197 | * @param {Array.} allFiles All files, expressed relative to inputDir 198 | * @param {Array.} dirtyFiles Dirty files, expressed relative to inputDir 199 | * @param {EventEmitter} emitter 200 | * @return {Promise} 201 | * @private 202 | */ 203 | SoyCompiler.prototype._compileTemplateFilesAndEmit = function (inputDir, outputDir, allFiles, dirtyFiles, emitter) { 204 | var self = this 205 | return this._compileTemplateFilesAsync(inputDir, outputDir, allFiles, dirtyFiles) 206 | .then(function () { 207 | self._finalizeCompileTemplates(outputDir, emitter) 208 | }, function (err) { 209 | emitCompile(emitter, err) 210 | }) 211 | } 212 | 213 | 214 | /** 215 | * Compiles all soy files, returning a promise. 216 | * @see compileTemplates for the emitter API. 217 | * @param {string} inputDir Input directory from where the compiler spawns. 218 | * @param {string} outputDir 219 | * @param {Array.} allFiles All files, expressed relative to inputDir 220 | * @param {Array.} dirtyFiles Dirty files, expressed relative to inputDir 221 | * @return {Promise} 222 | * @private 223 | */ 224 | SoyCompiler.prototype._compileTemplateFilesAsync = function (inputDir, outputDir, allFiles, dirtyFiles) { 225 | var options = this._options 226 | var outputPathFormat = path.join(outputDir, '{INPUT_DIRECTORY}', '{INPUT_FILE_NAME}.js') 227 | 228 | // Arguments for running the soy compiler via java. 229 | var args = [ 230 | '-classpath', [ PATH_TO_SOY_JAR ].concat(options.classpath).join(path.delimiter), 231 | 'com.google.template.soy.SoyToJsSrcCompiler', 232 | '--shouldGenerateJsdoc' 233 | ] 234 | 235 | // Handling soy compiler options. 236 | if (options.shouldGenerateJsdoc) { 237 | args.push('--shouldGenerateJsdoc') 238 | } 239 | 240 | if (options.useClosureStyle || options.shouldProvideRequireSoyNamespaces) { 241 | args.push('--shouldProvideRequireSoyNamespaces') 242 | } else if (options.shouldProvideRequireJsFunctions) { 243 | args.push('--shouldProvideRequireJsFunctions') 244 | } 245 | 246 | if (options.cssHandlingScheme !== undefined) { 247 | args.push('--cssHandlingScheme', options.cssHandlingScheme) 248 | } 249 | 250 | if (options.pluginModules && options.pluginModules.length > 0) { 251 | args.push('--pluginModules', options.pluginModules.join(',')) 252 | } 253 | 254 | if (options.locales && options.locales.length > 0) { 255 | args.push('--locales', options.locales.join(',')) 256 | 257 | if (options.locales.length > 1) { 258 | outputPathFormat = path.join(outputDir, '{LOCALE}', '{INPUT_DIRECTORY}', '{INPUT_FILE_NAME}.js') 259 | } 260 | } 261 | 262 | if (options.messageFilePathFormat) { 263 | args.push('--messageFilePathFormat', options.messageFilePathFormat) 264 | } 265 | 266 | if (!options.shouldDeclareTopLevelNamespaces) { 267 | args.push('--shouldDeclareTopLevelNamespaces', 'false') 268 | } 269 | 270 | if (options.protoFileDescriptors) { 271 | args.push('--protoFileDescriptors', options.protoFileDescriptors) 272 | } 273 | 274 | args.push('--outputPathFormat', outputPathFormat) 275 | 276 | // List of files 277 | args = args.concat(dirtyFiles) 278 | 279 | var terminated = false 280 | var self = this 281 | 282 | function runCompiler() { 283 | if (!dirtyFiles.length) { 284 | return Q.resolve(true) 285 | } 286 | 287 | var deferred = Q.defer() 288 | 289 | // Execute the command inside the input directory. 290 | var cp = child_process.spawn('java', args, {cwd: inputDir}) 291 | var stderr = '' 292 | cp.stderr.on('data', function (data) { 293 | stderr += data 294 | }) 295 | 296 | cp.on('error', function (err) { 297 | stderr += String(err) 298 | onExit(1) 299 | }) 300 | 301 | function onExit(exitCode) { 302 | if (terminated) return 303 | 304 | if (exitCode != 0) { 305 | // Log all the errors and execute the callback with a generic error object. 306 | terminated = true 307 | console.error('soynode: Compile error\n', stderr) 308 | deferred.reject(new Error('Error compiling templates')) 309 | } else { 310 | deferred.resolve(true) 311 | } 312 | } 313 | 314 | cp.on('exit', onExit) 315 | return deferred.promise 316 | } 317 | 318 | return runCompiler().then(function () { 319 | var vmTypes = [DEFAULT_VM_CONTEXT] 320 | if (options.locales && options.locales.length > 0) { 321 | vmTypes = options.locales.concat() // clone 322 | } 323 | 324 | var next = function () { 325 | if (vmTypes.length === 0) { 326 | return Q.resolve(true) 327 | } else { 328 | return self._postCompileProcess(outputDir, allFiles, vmTypes.pop()).then(next) 329 | } 330 | } 331 | return next().fail(function (err) { 332 | console.error('Error post-processing templates', err) 333 | throw err 334 | }) 335 | }) 336 | } 337 | 338 | 339 | /** 340 | * Compiles all soy files from an input directory, but takes an emitter to use 341 | * instead of a callback. 342 | * @see compileTemplates for the emitter API. 343 | * @param {string} inputDir 344 | * @param {EventEmitter} emitter 345 | * @private 346 | */ 347 | SoyCompiler.prototype._compileTemplatesAndEmit = function (inputDir, emitter) { 348 | var self = this 349 | findFiles(inputDir, 'soy', function (err, files) { 350 | if (err) return emitCompile(emitter, err) 351 | if (files.length == 0) return emitCompile(emitter) 352 | 353 | var outputDir = self._createOutputDir() 354 | self._maybeUsePrecompiledFiles(outputDir, files) 355 | .then(function (dirtyFiles) { 356 | self._maybeSetupDynamicRecompile(inputDir, outputDir, files, emitter) 357 | self._compileTemplateFilesAndEmit(inputDir, outputDir, files, dirtyFiles, emitter) 358 | }) 359 | .done() 360 | }) 361 | } 362 | 363 | 364 | /** 365 | * Finalizes compile templates. 366 | * @param {EventEmitter} emitter 367 | * @private 368 | */ 369 | SoyCompiler.prototype._finalizeCompileTemplates = function (outputDir, emitter) { 370 | emitCompile(emitter) 371 | 372 | if (this._options.eraseTemporaryFiles && !this._options.allowDynamicRecompile) { 373 | exec('rm -r \'' + outputDir + '\'', {}, function (err) { 374 | // TODO(dan): This is a pretty nasty way to delete the files. Maybe use rimraf 375 | if (err) console.error('soynode: Error deleting temporary files', err) 376 | }) 377 | } 378 | } 379 | 380 | 381 | /** 382 | * Loads precompiled templates into memory. All .soy.js files within the provided inputDir will be 383 | * loaded. 384 | * @param {string} inputDir 385 | * @param {function (Error, boolean)} 386 | */ 387 | SoyCompiler.prototype.loadCompiledTemplates = function(inputDir, callback) { 388 | var self = this 389 | findFiles(inputDir, 'soy.js', function (err, files) { 390 | if (err) return callback(err, false) 391 | files = files.map(function (file) { 392 | return path.join(inputDir, file) 393 | }) 394 | self.loadCompiledTemplateFiles(files, callback) 395 | }) 396 | } 397 | 398 | 399 | /** 400 | * Loads an array of template files into memory. 401 | * @param {Array.} files 402 | * @param {function (Error, boolean) | Object} callbackOrOptions 403 | * @param {function (Error, boolean)=} callback 404 | */ 405 | SoyCompiler.prototype.loadCompiledTemplateFiles = function (files, callbackOrOptions, callback) { 406 | var vmType = DEFAULT_VM_CONTEXT 407 | 408 | if (typeof(callbackOrOptions) === 'function') { 409 | callback = callbackOrOptions 410 | } else { 411 | vmType = callbackOrOptions.vmType 412 | } 413 | 414 | this.getSoyVmContext(vmType).loadCompiledTemplateFiles(files, callback) 415 | } 416 | 417 | 418 | /** 419 | * Adds a file system watch to the provided files, and executes the fn when changes are detected. 420 | * @param {string} inputDir 421 | * @param {string} outputDir 422 | * @param {Array.} relativeFilePaths 423 | * @param {EventEmitter} emitter 424 | * @private 425 | */ 426 | SoyCompiler.prototype._maybeSetupDynamicRecompile = function (inputDir, outputDir, relativeFilePaths, emitter) { 427 | if (!this._options.allowDynamicRecompile) { 428 | return 429 | } 430 | 431 | var currentCompilePromise = Q.resolve(true) 432 | var dirtyFileSet = {} 433 | var self = this 434 | relativeFilePaths.forEach(function (relativeFile) { 435 | var file = path.resolve(inputDir, relativeFile) 436 | if (self._watches[file]) return 437 | try { 438 | self._watches[file] = Date.now() 439 | 440 | fs.watchFile(file, {}, function () { 441 | var now = Date.now() 442 | // Ignore spurious change events. 443 | console.log('soynode: caught change to ', file) 444 | if (now - self._watches[file] < 1000) return Q.resolve(true) 445 | 446 | dirtyFileSet[relativeFile] = true 447 | self._watches[file] = now 448 | 449 | // Wait until the previous compile has completed before starting a new one. 450 | currentCompilePromise = currentCompilePromise 451 | .then(function () { 452 | var dirtyFiles = Object.keys(dirtyFileSet) 453 | if (!dirtyFiles.length) { 454 | // Nothing needs to be recompiled because it was already caught by another job. 455 | return 456 | } 457 | dirtyFileSet = {} 458 | console.log('soynode: Recompiling templates due to change in %s', dirtyFiles) 459 | return self._compileTemplateFilesAndEmit(inputDir, outputDir, relativeFilePaths, dirtyFiles, emitter) 460 | }).fail(function (err) { 461 | console.warn('soynode: Error recompiling ', err) 462 | }) 463 | 464 | // Return the promise, for use when testing. fs.watchFile will just ignore this. 465 | return currentCompilePromise 466 | }) 467 | } catch (e) { 468 | console.warn('soynode: Error watching ' + file, e) 469 | } 470 | }, this) 471 | } 472 | 473 | /** 474 | * Checks if precompiled files are available, using them as necessary. 475 | * @param {string} outputDir 476 | * @param {Array.} files 477 | * @return {Promise>} Files that we could not find precompiled versions of. 478 | * @private 479 | */ 480 | SoyCompiler.prototype._maybeUsePrecompiledFiles = function (outputDir, files) { 481 | var precompiledDir = this._options.precompiledDir 482 | if (!precompiledDir) { 483 | return Q.resolve(files) 484 | } 485 | 486 | var vmTypes = [DEFAULT_VM_CONTEXT] 487 | var options = this._options 488 | if (options.locales && options.locales.length > 0) { 489 | vmTypes = options.locales.concat() // clone 490 | } 491 | 492 | var self = this 493 | return Q.resolve(true) 494 | .then(function () { 495 | // Return an array of files that don't have precompiled versions. 496 | return Q.all(files.map(function (file) { 497 | return self._preparePrecompiledFile(outputDir, precompiledDir, file, vmTypes) 498 | .then(function (ok) { 499 | return ok ? '' : file 500 | }) 501 | })) 502 | }) 503 | .then(function (dirtyFiles) { 504 | dirtyFiles = dirtyFiles.filter(Boolean) // filter out empty strings. 505 | if (dirtyFiles.length != files.length) { 506 | console.log('Loaded %s precompiled files', files.length - dirtyFiles.length) 507 | } 508 | return dirtyFiles 509 | }) 510 | .fail(function (err) { 511 | console.error('Failed loading precompiled files', err) 512 | return files 513 | }) 514 | } 515 | 516 | 517 | /** 518 | * Checks if all locales of a file have been precompiled, and move them to the output directory. 519 | * @param {string} outputDir 520 | * @param {string} precompiledDir 521 | * @param {string} file 522 | * @param {Array} vmTypes 523 | * @return {Promise} True on success 524 | * @private 525 | */ 526 | SoyCompiler.prototype._preparePrecompiledFile = function (outputDir, precompiledDir, file, vmTypes) { 527 | var self = this 528 | var precompiledFilesOkPromise = Q.all(vmTypes.map(function (vmType) { 529 | var precompiledFileName = self._getOutputFile(precompiledDir, file, vmType) 530 | var outputFileName = self._getOutputFile(outputDir, file, vmType) 531 | 532 | var precompiledFileOkPromise = Q.nfcall(fs.stat, precompiledFileName) 533 | .then(function (exists) { 534 | if (!exists) { 535 | return false 536 | } 537 | 538 | if (outputFileName != precompiledFileName) { 539 | return Q.nfcall(fs.mkdirs, path.dirname(outputFileName)) 540 | .then(function () { 541 | return Q.nfcall(copy, precompiledFileName, outputFileName) 542 | }) 543 | .then(function () { 544 | return true 545 | }) 546 | } else { 547 | return true 548 | } 549 | }, function () { 550 | return false // stat is expected to error out if the file isn't there. 551 | }) 552 | return precompiledFileOkPromise 553 | })) 554 | return precompiledFilesOkPromise 555 | .then(function (array) { 556 | return array.every(Boolean) 557 | }) 558 | } 559 | 560 | 561 | /** 562 | * Concatenates all output files into a single file. 563 | * @param {string} outputDir 564 | * @param {Array.} files 565 | * @param {string=} vmType optional type of the vm 566 | * @private 567 | */ 568 | SoyCompiler.prototype._concatOutput = function (outputDir, files, vmType) { 569 | var options = this._options 570 | var concatFileName = options.concatFileName 571 | if (options.locales && options.locales.length > 1) { 572 | concatFileName += '_' + vmType 573 | } 574 | concatFileName += '.soy.concat.js' 575 | 576 | var target = path.join(outputDir, concatFileName) 577 | var concatenated = files.map(function (file) { 578 | return fs.readFileSync(file).toString() 579 | }).join('') 580 | 581 | fs.writeFileSync(target, concatenated) 582 | } 583 | 584 | 585 | /** 586 | * @param {string} outputDir 587 | * @param {string} file 588 | * @param {string=} vmType 589 | */ 590 | SoyCompiler.prototype._getOutputFile = function (outputDir, file, vmType) { 591 | var options = this._options 592 | vmType = vmType || DEFAULT_VM_CONTEXT 593 | if (options.locales && options.locales.length > 1) { 594 | return path.join(outputDir, vmType, file) + '.js' 595 | } else { 596 | return path.join(outputDir, file) + '.js' 597 | } 598 | } 599 | 600 | 601 | /** 602 | * Does all processing that happens after the compiling ends. 603 | * @param {string} outputDir 604 | * @param {Array.} files 605 | * @param {string=} vmType optional type of the vm 606 | * @return {Promise} 607 | * @private 608 | */ 609 | SoyCompiler.prototype._postCompileProcess = function (outputDir, files, vmType) { 610 | var options = this._options 611 | vmType = vmType || DEFAULT_VM_CONTEXT 612 | 613 | // Build a list of paths that we expect as output of the soy compiler. 614 | var templatePaths = files.map(function (file) { 615 | return this._getOutputFile(outputDir, file, vmType) 616 | }, this) 617 | 618 | try { 619 | if (options.concatOutput) this._concatOutput(outputDir, templatePaths, vmType) 620 | } catch (e) { 621 | console.warn('soynode: Error concatenating files', e) 622 | } 623 | 624 | if (options.loadCompiledTemplates) { 625 | // Load the compiled templates into memory. 626 | return Q.nfcall(this.loadCompiledTemplateFiles.bind(this, templatePaths, {vmType: vmType})) 627 | } else { 628 | return Q.resolve(true) 629 | } 630 | } 631 | 632 | 633 | /** 634 | * Performs a recursive directory traversal of the given directory, accumulating all files with the 635 | * provided extension. The resultant array is a list of paths relative to the input directory. 636 | * @param {string} directory 637 | * @param {string} extension 638 | * @param {function(Error, Array.)} callback 639 | */ 640 | function findFiles(directory, extension, callback) { 641 | var files = [] 642 | var stack = [directory] 643 | 644 | function next() { 645 | if (stack.length === 0) { 646 | callback(null, files) 647 | } else { 648 | var dir = stack.pop() 649 | fs.stat(dir, function (err, stats) { 650 | if (err) return callback(err, []) 651 | if (!stats.isDirectory()) return next() 652 | fs.readdir(dir, function (err, dirContents) { 653 | if (err) return callback(err, []) 654 | dirContents.forEach(function (file) { 655 | var fullpath = path.join(dir, file) 656 | // If the file is a soy file then push it onto the files array. 657 | if (file.substr(-1 - extension.length) == '.' + extension) { 658 | files.push(path.relative(directory, fullpath)) 659 | 660 | // If the file has no extension add it to the stack for potential processing. We 661 | // optimistically add potential dirs here to simplify the async nature of fs calls. 662 | } else if (file.indexOf('.') == -1) { 663 | stack.push(fullpath) 664 | } 665 | }) 666 | next() 667 | }) 668 | }) 669 | } 670 | } 671 | next() 672 | } 673 | 674 | 675 | /** 676 | * Emits the compile event. Swallows any errors thrown by the receiver. 677 | */ 678 | function emitCompile(emitter, err) { 679 | try { 680 | emitter.emit('compile', err, !!err) 681 | } catch (e) { 682 | console.error('soynode: emit error', e) 683 | } 684 | } 685 | 686 | 687 | /** 688 | * Callback that will log an error. 689 | */ 690 | function logErrorOrDone(err) { 691 | if (err) console.error('soynode:', err) 692 | else console.log('soynode: Done') 693 | } 694 | 695 | module.exports = SoyCompiler 696 | -------------------------------------------------------------------------------- /lib/SoyOptions.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014. A Medium Corporation. 2 | 3 | var path = require('path') 4 | 5 | /** 6 | * Describes the possible options set to a SoyCompiler. 7 | * @constructor 8 | */ 9 | function SoyOptions() { 10 | /** 11 | * A temporary directory where compiled .soy.js files will be stored after compilation. 12 | * @type {string} 13 | */ 14 | this.tmpDir = '/tmp/soynode' 15 | 16 | /** 17 | * Directory where the compiler will spawn compilation process. 18 | * When compiling from files defaults to process.cwd(), if compiling from a directory inputDir is used instead. 19 | * @type {string} 20 | */ 21 | this.inputDir = process.cwd() 22 | 23 | /** 24 | * An output directory, which compiled soy.js files is stored. 25 | * @type {?string} 26 | */ 27 | this.outputDir = null 28 | 29 | /** 30 | * A directory of precompiled soy.js files. Soynode will check these first and 31 | * use them if available. 32 | * 33 | * You can set this to the same value as outputDir to re-use 34 | * results from previous runs. 35 | * 36 | * @type {?string} 37 | */ 38 | this.precompiledDir = null 39 | 40 | /** 41 | * Whether the compiled soy files should be placed into a unique directory(timestamped). 42 | * @type {boolean} 43 | */ 44 | this.uniqueDir = true 45 | 46 | /** 47 | * Whether to watch any files that are loaded and to refetch them when they change. 48 | * @type {boolean} 49 | */ 50 | this.allowDynamicRecompile = false 51 | 52 | /** 53 | * Whether or not to load the compiled templates in the VM context. 54 | * @type {boolean} 55 | */ 56 | this.loadCompiledTemplates = true 57 | 58 | /** 59 | * Whether to delete temporary files created during the compilation process. 60 | * @type {boolean} 61 | */ 62 | this.eraseTemporaryFiles = false 63 | 64 | /** 65 | * Whether or not to use goog.provide and goog.require for JS functions and Soy namespaces. 66 | * @type {boolean} 67 | */ 68 | this.useClosureStyle = false 69 | 70 | /** 71 | * Whether or not to generate JSDoc on each template function, with type info for the Closure Compiler. 72 | * @type {boolean} 73 | */ 74 | this.shouldGenerateJsdoc = false 75 | 76 | /** 77 | * Whether or not to use goog.provide and goog.require for JS functions and Soy namespaces. 78 | * If you set this flag, each generated JavaScript file contains: 79 | * - one goog.provide statement for the corresponding Soy file's namespace 80 | * - goog.require statements for the namespaces of the called template 81 | * @type {boolean} 82 | */ 83 | this.shouldProvideRequireSoyNamespaces = false 84 | 85 | /** 86 | * Whether or not to use goog.provide and goog.require for JS functions and Soy namespaces. 87 | * If you set this flag, each generated JS file contains: 88 | * - goog.provide statements for the full names of each of its template JS functions 89 | * - goog.require statements for the full names of each of the called templates. 90 | * @type {boolean} 91 | */ 92 | this.shouldProvideRequireJsFunctions = false 93 | 94 | /** 95 | * The scheme to use for handling 'css' commands. Specifying 96 | * 'literal' will cause command text to be inserted as literal 97 | * text. Specifying 'reference' will cause command text to be 98 | * evaluated as a data or global reference. Specifying 'goog' 99 | * will cause generation of calls goog.getCssName. This option 100 | * has no effect if the Soy code does not contain 'css' 101 | * commands. 102 | * @type {?string} 103 | */ 104 | this.cssHandlingScheme = undefined 105 | 106 | /** 107 | * Additional classpath to pass to the soy template compiler. This makes adding plugins possible. 108 | * @type {Array} 109 | */ 110 | this.classpath = [] 111 | 112 | /** 113 | * Plugin module Java classnames to pass to the soy template compiler. 114 | * @type {Array} 115 | */ 116 | this.pluginModules = [] 117 | 118 | /** 119 | * Additional JS files to be evaluated in the VM context for the soy templates. 120 | * Useful for soy function support libs 121 | * @type {Array} 122 | */ 123 | this.contextJsPaths = [] 124 | 125 | /** 126 | * Whether the compiled soy.js files should be joined into a single file 127 | * @type {boolean} 128 | */ 129 | this.concatOutput = false 130 | 131 | /** 132 | * File name used for concatenated files, only relevant when concatOutput is true. 133 | * @type {string} 134 | */ 135 | this.concatFileName = 'compiled' 136 | 137 | /** 138 | * List of locales to translate the templates to. 139 | * @type {Array} 140 | */ 141 | this.locales = [] 142 | 143 | /** 144 | * Path to the translation file to use, which can contain any of the placeholders 145 | * allowed on the --messageFilePathFormat option of SoyToJsSrcCompiler.jar. 146 | * @type {?string} 147 | */ 148 | this.messageFilePathFormat = null 149 | 150 | /** 151 | * When this option is set to false, each generated JS file 152 | * will not attempt to declare the top-level name in its 153 | * namespace, instead assuming the top-level name is already 154 | * declared in the global scope. E.g. for namespace aaa.bbb, 155 | * the code will not attempt to declare aaa, but will still 156 | * define aaa.bbb if it's not already defined. 157 | * @type {boolean} 158 | */ 159 | this.shouldDeclareTopLevelNamespaces = true 160 | 161 | /** 162 | * Points to a directory with proto files 163 | * @type {string} 164 | */ 165 | this.protoFileDescriptors = '' 166 | } 167 | 168 | 169 | 170 | /** 171 | * Sets options which affect how soynode operates. 172 | */ 173 | SoyOptions.prototype.merge = function (opts) { 174 | for (var key in opts) { 175 | var isFunction = typeof this[key] == 'function' 176 | if (isFunction && this[key] == opts[key]) { 177 | continue 178 | } 179 | 180 | if (!(key in this) || (typeof this[key] == 'function')) { 181 | throw new Error('soynode: Invalid option key [' + key + ']') 182 | } 183 | 184 | // When setting the tmpDir make sure to resolve the absolute path so as to avoid accidents 185 | // caused by changes to the working directory. 186 | if (key == 'tmpDir') { 187 | this.tmpDir = path.resolve(opts.tmpDir) 188 | } else if (key == 'outputDir') { 189 | this.outputDir = opts.outputDir == null ? null : path.resolve(opts.outputDir) 190 | } else { 191 | this[key] = opts[key] 192 | } 193 | } 194 | } 195 | 196 | module.exports = SoyOptions 197 | -------------------------------------------------------------------------------- /lib/SoyVmContext.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014. A Medium Corporation. 2 | 3 | var Q = require('q') 4 | var fs = require('fs') 5 | var vm = require('vm') 6 | var path = require('path') 7 | var closureTemplates = require('closure-templates') 8 | var closureLibrary = require('obvious-closure-library') 9 | 10 | 11 | /** 12 | * Resolved path to Soy utils JS script. 13 | * @type {string} 14 | */ 15 | var SOY_UTILS_PATH = closureTemplates['soyutils_usegoog.js'] 16 | 17 | /** 18 | * All the dependencies of soyutils_usegoog.js 19 | * 20 | * In theory, it'd be more robust to load these with goog.require 21 | * but I haven't figured out how to make the bootstrapping work 22 | * in the VM environment. 23 | */ 24 | var CLOSURE_PATHS = [ 25 | 'closure/goog/base.js', 26 | 'closure/goog/deps.js', 27 | 'closure/goog/debug/error.js', 28 | 'closure/goog/dom/nodetype.js', 29 | 'closure/goog/string/string.js', 30 | 'closure/goog/asserts/asserts.js', 31 | 'closure/goog/array/array.js', 32 | 'closure/goog/dom/tagname.js', 33 | 'closure/goog/object/object.js', 34 | 'closure/goog/dom/tags.js', 35 | 'closure/goog/string/typedstring.js', 36 | 'closure/goog/string/const.js', 37 | 'closure/goog/html/safestyle.js', 38 | 'closure/goog/html/safestylesheet.js', 39 | 'closure/goog/fs/url.js', 40 | 'closure/goog/i18n/bidi.js', 41 | 'closure/goog/html/safeurl.js', 42 | 'closure/goog/html/trustedresourceurl.js', 43 | 'closure/goog/html/safehtml.js', 44 | 'closure/goog/html/safescript.js', 45 | 'closure/goog/html/uncheckedconversions.js', 46 | 'closure/goog/structs/structs.js', 47 | 'closure/goog/structs/collection.js', 48 | 'closure/goog/functions/functions.js', 49 | 'closure/goog/math/math.js', 50 | 'closure/goog/iter/iter.js', 51 | 'closure/goog/structs/map.js', 52 | 'closure/goog/structs/set.js', 53 | 'closure/goog/labs/useragent/util.js', 54 | 'closure/goog/labs/useragent/browser.js', 55 | 'closure/goog/labs/useragent/engine.js', 56 | 'closure/goog/labs/useragent/platform.js', 57 | 'closure/goog/useragent/useragent.js', 58 | 'closure/goog/debug/debug.js', 59 | 'closure/goog/dom/browserfeature.js', 60 | 'closure/goog/dom/safe.js', 61 | 'closure/goog/math/coordinate.js', 62 | 'closure/goog/math/size.js', 63 | 'closure/goog/dom/dom.js', 64 | 'closure/goog/structs/inversionmap.js', 65 | 'closure/goog/i18n/graphemebreak.js', 66 | 'closure/goog/format/format.js', 67 | 'closure/goog/html/legacyconversions.js', 68 | 'closure/goog/i18n/bidiformatter.js', 69 | 'closure/goog/soy/data.js', 70 | 'closure/goog/soy/soy.js', 71 | 'closure/goog/string/stringbuffer.js' 72 | ].map(function (file) { 73 | return path.join(closureLibrary.dirname, file) 74 | }) 75 | 76 | 77 | /** 78 | * Closure-templates keeps a global registry of all deltemplates. 79 | * We want to be able to reset the registry when we recompile. 80 | * 81 | * This is kind of a terrible solution, but it seems faster and more 82 | * robust than trying to reload all the support code every time. 83 | */ 84 | var RESET_DELTEMPLATE_REGISTRY_CODE = 85 | 'soy.$$DELEGATE_REGISTRY_PRIORITIES_ = {};\n' + 86 | 'soy.$$DELEGATE_REGISTRY_FUNCTIONS_ = {};' 87 | 88 | 89 | /** 90 | * An abstract API over a soynode VM context. 91 | * 92 | * SoyNode operates by creating a VM sandbox, and loading the soy functions into 93 | * that sandbox. If you use SoyNode's i18n features, you may have multiple sandboxes, 94 | * one for each locale. 95 | * 96 | * @param {string} name 97 | * @param {SoyOptions} options 98 | * @constructor 99 | */ 100 | function SoyVmContext(name, options) { 101 | /** @private {string} */ 102 | this._name = name 103 | 104 | /** @private {SoyOptions} */ 105 | this._options = options 106 | 107 | /** 108 | * A cache for function pointers returned by the vm.runInContext call. Caching the reference 109 | * results in a 10x speed improvement, over calling getting the function each time. 110 | * @type {Object} 111 | */ 112 | this._templateCache = {} 113 | 114 | this._context = vm.createContext({}) 115 | 116 | /** @private {boolean} Whether the context has been initialized with soyutils */ 117 | this._contextInitialized = false 118 | } 119 | 120 | 121 | /** 122 | * The unique name of the sandbox. 123 | * @return {string} 124 | */ 125 | SoyVmContext.prototype.getName = function () { 126 | return this._name 127 | } 128 | 129 | 130 | /** 131 | * @return {Object} Get the internal vm context. Useful for injecting globals 132 | * manually into the context. 133 | * @return {Object} 134 | */ 135 | SoyVmContext.prototype.getContext = function () { 136 | return this._context 137 | } 138 | 139 | 140 | /** 141 | * @param {Object} context Sets the context. Useful for injecting globals 142 | * manually into the context, but beware of overwriting the soy support code. 143 | */ 144 | SoyVmContext.prototype.setContext = function (context) { 145 | this._context = context 146 | } 147 | 148 | 149 | /** 150 | * Gets a reference to a template function. 151 | * 152 | * Note: If dynamic recompilation is enabled the reference will not get updated. 153 | * 154 | * @param {string} templateName 155 | * @return {function (Object) : string} 156 | */ 157 | SoyVmContext.prototype.get = function (templateName) { 158 | if (!this._options.loadCompiledTemplates) throw new Error('soynode: Cannot load template, try with `loadCompiledTemplates: true`.') 159 | 160 | if (!this._templateCache[templateName]) { 161 | var template 162 | try { 163 | template = vm.runInContext(templateName, this.getContext(), 'soynode.vm') 164 | } catch (e) { 165 | // Fallthrough 166 | } 167 | 168 | if (!template) throw new Error('soynode: Unknown template [' + templateName + ']') 169 | this._templateCache[templateName] = template 170 | } 171 | return this._templateCache[templateName] 172 | } 173 | 174 | 175 | /** 176 | * Loads an array of template files into memory. 177 | * @param {Array.} files 178 | * @param {function (Error, boolean)=} callback 179 | */ 180 | SoyVmContext.prototype.loadCompiledTemplateFiles = function (files, callback) { 181 | var options = this._options 182 | var self = this 183 | 184 | // load the contextJsPaths into the context before the soy template JS 185 | var filePromises = pathsToPromises(options.contextJsPaths.concat(files)) 186 | var supportFilePromises = getSupportFilePromises() 187 | 188 | var result = Q.resolve(true) 189 | if (self._contextInitialized) { 190 | result = Q.fcall(function () { 191 | vm.runInContext(RESET_DELTEMPLATE_REGISTRY_CODE, self.getContext(), 'soynode-reset.') 192 | }) 193 | } else { 194 | result = result.then(function () { 195 | return loadFiles(self.getContext(), supportFilePromises) 196 | }).then(function () { 197 | self._contextInitialized = true 198 | }) 199 | } 200 | 201 | result.then(function () { 202 | return loadFiles(self.getContext(), filePromises) 203 | }) 204 | .then(function (result) { 205 | // Blow away the cache when all files have been loaded 206 | self._templateCache = {} 207 | callback(null, result) 208 | }, function (err) { 209 | callback(err) 210 | }) 211 | } 212 | 213 | /** 214 | * @param {VmContext} context a vm context 215 | * @param {Array.} filePromises Promises of {path, contents} tuples 216 | * @return {Q.Promise} 217 | */ 218 | function loadFiles(context, filePromises) { 219 | var i = 0 220 | 221 | function next(result) { 222 | // Evaluate the template code in the context of the soy VM context. Any variables defined 223 | // in the template file will become members of the vmContext object. 224 | vm.runInContext(result.contents, context, result.path) 225 | 226 | if (i >= filePromises.length) { 227 | return Q.resolve(true) 228 | } else { 229 | return filePromises[i++].then(next) 230 | } 231 | } 232 | 233 | if (!filePromises.length) { 234 | return Q.resolve(true) 235 | } 236 | return filePromises[i++].then(next) 237 | } 238 | 239 | var supportFilePromises = null 240 | 241 | /** 242 | * @return {Array.>} Promises for the file contents of closure/soy support code. 243 | */ 244 | function getSupportFilePromises() { 245 | if (supportFilePromises) return supportFilePromises 246 | 247 | var paths = CLOSURE_PATHS.concat([SOY_UTILS_PATH]) 248 | supportFilePromises = pathsToPromises(paths) 249 | return supportFilePromises 250 | } 251 | 252 | function pathsToPromises(paths) { 253 | return paths.map(function (path) { 254 | return Q.nfcall(fs.readFile, path, 'utf8').then(function (contents) { 255 | return {path: path, contents: contents} 256 | }) 257 | }) 258 | } 259 | 260 | module.exports = SoyVmContext 261 | -------------------------------------------------------------------------------- /lib/copy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * A hacky copy method until 5 | * https://github.com/jprichardson/node-fs-extra/issues/326 6 | * is fixed. 7 | */ 8 | 9 | var fs = require('fs') 10 | 11 | function copy(source, target, callback) { 12 | var readStream = fs.createReadStream(source) 13 | var writeStream = fs.createWriteStream(target) 14 | var isDone = false 15 | 16 | function onError(err) { 17 | if (isDone) return 18 | isDone = true 19 | callback(err) 20 | } 21 | readStream.on('error', onError) 22 | writeStream.on('error', onError) 23 | 24 | writeStream.on('open', function () { 25 | readStream.pipe(writeStream) 26 | }) 27 | 28 | writeStream.once('close', function () { 29 | if (isDone) return 30 | isDone = true 31 | callback(null) 32 | }) 33 | } 34 | 35 | module.exports = copy 36 | -------------------------------------------------------------------------------- /lib/soynode.js: -------------------------------------------------------------------------------- 1 | // Copyright (c)2012 The Obvious Corporation 2 | 3 | /** 4 | * @fileoverview Public interface exposed to users of `soynode`. 5 | */ 6 | 7 | var SoyCompiler = require('./SoyCompiler') 8 | 9 | // Public API. See function declarations for JSDoc. 10 | module.exports = new SoyCompiler() 11 | module.exports.SoyCompiler = SoyCompiler 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "soynode", 3 | "version" : "2.1.0", 4 | "description" : "Utility for working with Closure Templates, aka Soy, from within a node.js application.", 5 | "keywords" : ["node", "soy", "templates", "closure"], 6 | "repository" : { 7 | "type" : "git", 8 | "url" : "http://github.com/Medium/soynode.git" 9 | }, 10 | "homepage" : "https://github.com/Medium/soynode", 11 | "author" : { 12 | "name" : "Daniel Pupius", 13 | "email" : "dan@medium.com", 14 | "url" : "https://github.com/dpup" 15 | }, 16 | "main" : "./lib/soynode.js", 17 | "directories": {"lib": "./lib"}, 18 | "dependencies": { 19 | "closure-templates": "^20160825.0.0", 20 | "fs-extra": "1.0.0", 21 | "obvious-closure-library": "^20161024.0.0", 22 | "q": "1.4.1" 23 | }, 24 | "devDependencies": { 25 | "eslint": "1.6.0", 26 | "nodeunit": "0.9.0", 27 | "nodeunitq": "0.1.1" 28 | }, 29 | "license": "Apache-2.0", 30 | "scripts": { 31 | "test": "node_modules/.bin/nodeunit test && eslint lib" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /package.json.orig: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "soynode", 3 | <<<<<<< HEAD 4 | "version" : "0.3.3", 5 | ======= 6 | "version" : "0.3.4", 7 | >>>>>>> Fix usage when setOptions is never called, broken in 34554556 8 | "description" : "Utility for working with Closure Templates, aka Soy, from within a node.js application.", 9 | "keywords" : ["node", "soy", "templates", "closure"], 10 | "repository" : { 11 | "type" : "git", 12 | "url" : "http://github.com/Obvious/soynode.git" 13 | }, 14 | "homepage" : "https://github.com/Obvious/soynode", 15 | "author" : { 16 | "name" : "Daniel Pupius", 17 | "email" : "dan@obvious.com", 18 | "url" : "https://github.com/dpup" 19 | }, 20 | "main" : "./lib/soynode.js", 21 | "directories": {"lib": "./lib"}, 22 | "dependencies": { 23 | "closure-templates": "20130416.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/assets/template1.soy: -------------------------------------------------------------------------------- 1 | {namespace template1} 2 | 3 | /** 4 | * @param title 5 | * @param surname 6 | **/ 7 | {template .formletter} 8 | {msg desc="Promotional message for ordering theme songs"}Dear {$title} {$surname}: With a name like {$title} {$surname}, shouldn't you have your own theme song? We can help!{/msg} 9 | {/template} -------------------------------------------------------------------------------- /test/assets/template1_namespace.js: -------------------------------------------------------------------------------- 1 | var template1 = {}; 2 | -------------------------------------------------------------------------------- /test/assets/template2.soy: -------------------------------------------------------------------------------- 1 | {namespace template2} 2 | 3 | /** 4 | * @param title 5 | * @param surname 6 | **/ 7 | {template .formletter} 8 | {msg desc="Promotional message for ordering theme songs"}Dear {$title} {$surname}: With a name like {$title} {$surname}, shouldn't you have your own theme song? We can help!{/msg} 9 | {/template} 10 | 11 | /** 12 | * @param title 13 | * @param surname 14 | **/ 15 | {template .formletterCall} 16 | {call template1.formletter data="all" /} 17 | {/template} -------------------------------------------------------------------------------- /test/assets/template3.soy: -------------------------------------------------------------------------------- 1 | {namespace template3} 2 | 3 | /** 4 | * @param type 5 | */ 6 | {template .main} 7 | {delcall template3._inner variant="$type" /} 8 | {/template} 9 | 10 | /** 11 | * Default template 12 | */ 13 | {deltemplate template3._inner} 14 | The default template 15 | {/deltemplate} 16 | 17 | /** 18 | * Hello world template 19 | */ 20 | {deltemplate template3._inner variant="'hello'"} 21 | Hello world 22 | {/deltemplate} 23 | -------------------------------------------------------------------------------- /test/assets/translations_es.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dear : With a name like , shouldn't you have your own theme song? We can help! 7 | Promotional message for ordering theme songs 8 | Estimado : Con un nombre como , ¿no debería tener su propia canción? Nosotros podemos ayudarle! 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/assets/translations_pt-BR.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dear : With a name like , shouldn't you have your own theme song? We can help! 7 | Promotional message for ordering theme songs 8 | Querido : Com um nome como , você não deveria ter o seu própro tema musical? Nós podemos ajudar! 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var child_process = require('child_process') 4 | var fs = require('fs-extra'); 5 | var path = require('path'); 6 | var soynode = require('../lib/soynode.js'); 7 | var nodeunitq = require('nodeunitq') 8 | var builder = new nodeunitq.Builder(exports) 9 | var Q = require('q') 10 | 11 | var watchFile = fs.watchFile 12 | var now = Date.now 13 | var spawn = child_process.spawn 14 | 15 | var watchFiles 16 | var watchCallbacks 17 | var spawnOpts 18 | var spawnArgs 19 | var time 20 | var soyCompiler 21 | var tmpDir1 = path.join(__dirname, 'tmp1') 22 | var tmpDir2 = path.join(__dirname, 'tmp2') 23 | 24 | exports.setUp = function (done) { 25 | soyCompiler = new soynode.SoyCompiler() 26 | 27 | time = 1 28 | Date.now = function () { return time; } 29 | 30 | watchFiles = []; 31 | watchCallbacks = []; 32 | fs.watchFile = function (f, opts, callback) { 33 | watchFiles.push(f); 34 | watchCallbacks.push(callback); 35 | }; 36 | 37 | spawnOpts = [] 38 | spawnArgs = [] 39 | child_process.spawn = function (prog, args, opts) { 40 | spawnOpts.push(opts) 41 | spawnArgs.push(args) 42 | return spawn.apply(child_process, arguments) 43 | } 44 | done() 45 | } 46 | 47 | exports.tearDown = function (done) { 48 | Date.now = now; 49 | fs.watchFile = watchFile; 50 | fs.removeSync(tmpDir1) 51 | fs.removeSync(tmpDir2) 52 | child_process.spawn = spawn; 53 | done(); 54 | } 55 | 56 | builder.add(function testCompileTemplates(test) { 57 | soyCompiler.compileTemplates(__dirname + '/assets', function(err) { 58 | test.ifError(err); 59 | test.doesNotThrow(assertTemplatesContents.bind(null, test)); 60 | test.done(); 61 | }); 62 | }) 63 | 64 | builder.add(function testCompileTemplatesWatch(test) { 65 | soyCompiler.setOptions({allowDynamicRecompile: true}) 66 | return Q.nfcall(soyCompiler.compileTemplates.bind(soyCompiler), __dirname + '/assets').then(function () { 67 | test.deepEqual(['template1.soy', 'template2.soy', 'template3.soy'], watchFiles.map(function (f) { 68 | return path.basename(f) 69 | })) 70 | test.deepEqual([{cwd: __dirname + '/assets'}], spawnOpts) 71 | 72 | var args = spawnArgs[0] 73 | test.deepEqual(['template1.soy', 'template2.soy', 'template3.soy'], 74 | args.slice(args.length - 3, args.length)) 75 | 76 | time += 1000 77 | return Q.delay(1) 78 | }).then(function () { 79 | return watchCallbacks[1]() 80 | }).then(function () { 81 | test.deepEqual(['template1.soy', 'template2.soy', 'template3.soy'], watchFiles.map(function (f) { 82 | return path.basename(f) 83 | })) 84 | test.deepEqual([{cwd: __dirname + '/assets'}, {cwd: __dirname + '/assets'}], spawnOpts) 85 | 86 | var args = spawnArgs[1] 87 | var secondLastArg = args[args.length - 2] 88 | test.ok(secondLastArg.indexOf('/tmp/soynode') == 0) 89 | 90 | var lastArg = args[args.length - 1] 91 | test.equal('template2.soy', lastArg) 92 | }) 93 | }) 94 | 95 | builder.add(function testCompileTemplatesWatchDelTemplate(test) { 96 | soyCompiler.setOptions({allowDynamicRecompile: true}) 97 | return Q.nfcall(soyCompiler.compileTemplates.bind(soyCompiler), __dirname + '/assets').then(function () { 98 | test.equal('The default template', soyCompiler.render('template3.main', {})) 99 | test.equal('Hello world', soyCompiler.render('template3.main', {type: 'hello'})) 100 | test.equal('The default template', soyCompiler.render('template3.main', {type: 'goodbye'})) 101 | 102 | time += 1000 103 | return Q.delay(1) 104 | }).then(function () { 105 | return watchCallbacks[1]() 106 | }).then(function () { 107 | test.equal('The default template', soyCompiler.render('template3.main', {})) 108 | test.equal('Hello world', soyCompiler.render('template3.main', {type: 'hello'})) 109 | test.equal('The default template', soyCompiler.render('template3.main', {type: 'goodbye'})) 110 | }); 111 | }) 112 | 113 | builder.add(function testCompileTemplateFiles(test) { 114 | soyCompiler.compileTemplateFiles([__dirname + '/assets/template1.soy', __dirname + '/assets/template2.soy'], function(err) { 115 | test.ifError(err); 116 | test.doesNotThrow(assertTemplatesContents.bind(null, test)); 117 | test.done(); 118 | }); 119 | }) 120 | 121 | builder.add(function testCompileTemplateFilesRelativePath(test) { 122 | soyCompiler.setOptions({ inputDir: __dirname }); 123 | soyCompiler.compileTemplateFiles(['./assets/template1.soy', './assets/template2.soy'], function(err) { 124 | test.ifError(err); 125 | test.doesNotThrow(assertTemplatesContents.bind(null, test)); 126 | test.done(); 127 | }); 128 | }) 129 | 130 | builder.add(function testCompileAndTranslateTemplates(test) { 131 | soyCompiler.setOptions({ 132 | locales: ['pt-BR'], 133 | messageFilePathFormat: __dirname + '/assets/translations_pt-BR.xlf' 134 | }); 135 | soyCompiler.compileTemplates(__dirname + '/assets', function(err) { 136 | test.ifError(err); 137 | test.doesNotThrow(assertTemplatesContents.bind(null, test, 'pt-BR')); 138 | test.done(); 139 | }); 140 | }) 141 | 142 | builder.add(function testCompileAndTranslateMultipleLanguagesTemplates(test) { 143 | soyCompiler.setOptions({ 144 | locales: ['pt-BR', 'es'], 145 | messageFilePathFormat: __dirname + '/assets/translations_{LOCALE}.xlf' 146 | }); 147 | soyCompiler.compileTemplates(__dirname + '/assets', function(err) { 148 | test.ifError(err); 149 | test.doesNotThrow(assertTemplatesContents.bind(null, test, 'pt-BR')); 150 | test.doesNotThrow(assertTemplatesContents.bind(null, test, 'es')); 151 | test.done(); 152 | }); 153 | }) 154 | 155 | builder.add(function testDefaultShouldDeclareTopLevelNamespaces(test) { 156 | soyCompiler.setOptions({ 157 | uniqueDir: false 158 | }); 159 | soyCompiler.compileTemplateFiles([__dirname + '/assets/template1.soy'], function(err) { 160 | test.ifError(err); 161 | 162 | var soyJsFilePath = path.join('/tmp/soynode', __dirname, 'assets/template1.soy.js'); 163 | var contents = fs.readFileSync(soyJsFilePath, 'utf8'); 164 | test.notEqual(-1, contents.indexOf('var template1 =')); 165 | 166 | test.done(); 167 | }); 168 | }) 169 | 170 | builder.add(function testFalseShouldDeclareTopLevelNamespaces(test) { 171 | soyCompiler.setOptions({ 172 | shouldDeclareTopLevelNamespaces: false, 173 | contextJsPaths: [path.join(__dirname, '/assets/template1_namespace.js')], 174 | uniqueDir: false 175 | }); 176 | soyCompiler.compileTemplateFiles([__dirname + '/assets/template1.soy'], function(err) { 177 | test.ifError(err); 178 | 179 | var soyJsFilePath = path.join('/tmp/soynode', __dirname, 'assets/template1.soy.js'); 180 | var contents = fs.readFileSync(soyJsFilePath, 'utf8'); 181 | test.equal(-1, contents.indexOf('var template1 =')); 182 | 183 | test.done(); 184 | }); 185 | }) 186 | 187 | builder.add(function testWithIjData(test) { 188 | soyCompiler.setOptions({ 189 | uniqueDir: false 190 | }); 191 | soyCompiler.compileTemplateFiles([__dirname + '/assets/template1.soy', __dirname + '/assets/template2.soy'], function(err) { 192 | test.ifError(err); 193 | 194 | var soyJsFilePath = path.join('/tmp/soynode', __dirname, 'assets/template2.soy.js'); 195 | var contents = fs.readFileSync(soyJsFilePath, 'utf8'); 196 | test.notEqual(-1, contents.indexOf('template1.formletter(opt_data, null, opt_ijData)')); 197 | 198 | test.done(); 199 | }); 200 | }) 201 | 202 | builder.add(function testPrecompileTemplatesOneCompiler(test) { 203 | soyCompiler.setOptions({ 204 | outputDir: tmpDir1, 205 | uniqueDir: false, 206 | precompiledDir: tmpDir1 207 | }) 208 | 209 | return Q.nfcall(soyCompiler.compileTemplates.bind(soyCompiler, __dirname + '/assets')) 210 | .then(function () { 211 | test.equal(1, spawnOpts.length) 212 | return Q.nfcall(soyCompiler.compileTemplates.bind(soyCompiler, __dirname + '/assets')) 213 | }) 214 | .then(function () { 215 | // Confirm that we re-used the precompiled templates and didn't start a new soy binary. 216 | test.equal(1, spawnOpts.length) 217 | assertTemplatesContents(test, null, soyCompiler) 218 | }) 219 | }) 220 | 221 | builder.add(function testPrecompileTemplatesTwoCompilers(test) { 222 | soyCompiler.setOptions({ 223 | outputDir: tmpDir1, 224 | uniqueDir: false 225 | }) 226 | 227 | var soyCompilerB = new soynode.SoyCompiler() 228 | soyCompilerB.setOptions({ 229 | precompiledDir: tmpDir1, 230 | outputDir: tmpDir2, 231 | uniqueDir: false 232 | }) 233 | 234 | return Q.nfcall(soyCompiler.compileTemplates.bind(soyCompiler, __dirname + '/assets')) 235 | .then(function () { 236 | test.equal(1, spawnOpts.length) 237 | return Q.nfcall(soyCompilerB.compileTemplates.bind(soyCompilerB, __dirname + '/assets')) 238 | }) 239 | .then(function () { 240 | // Confirm that we re-used the precompiled templates and didn't start a new soy binary. 241 | test.equal(1, spawnOpts.length) 242 | assertTemplatesContents(test, null, soyCompiler) 243 | assertTemplatesContents(test, null, soyCompilerB) 244 | }) 245 | }) 246 | 247 | builder.add(function testPrecompileTemplatesOneCompilerMultipleLanguages(test) { 248 | soyCompiler.setOptions({ 249 | outputDir: tmpDir1, 250 | uniqueDir: false, 251 | precompiledDir: tmpDir1, 252 | locales: ['pt-BR', 'es'], 253 | messageFilePathFormat: __dirname + '/assets/translations_{LOCALE}.xlf' 254 | }) 255 | 256 | return Q.nfcall(soyCompiler.compileTemplates.bind(soyCompiler, __dirname + '/assets')) 257 | .then(function () { 258 | test.equal(1, spawnOpts.length) 259 | return Q.nfcall(soyCompiler.compileTemplates.bind(soyCompiler, __dirname + '/assets')) 260 | }) 261 | .then(function () { 262 | // Confirm that we re-used the precompiled templates and didn't start a new soy binary. 263 | test.equal(1, spawnOpts.length) 264 | assertTemplatesContents(test, 'es') 265 | assertTemplatesContents(test, 'pt-BR') 266 | }) 267 | }) 268 | 269 | builder.add(function testDynamicRecompileWhenEventHandlerThrows(test) { 270 | soyCompiler.setOptions({allowDynamicRecompile: true}) 271 | 272 | var defer = Q.defer() 273 | var callback = function (err) { 274 | if (err) { 275 | defer.reject(err) 276 | } else { 277 | defer.resolve(null) 278 | } 279 | } 280 | var emitter = soyCompiler.compileTemplates(__dirname + '/assets', callback) 281 | emitter.on('compile', function () { 282 | throw new Error('Deliberately thrown error') 283 | }) 284 | 285 | return defer.promise.then(function () { 286 | var args = spawnArgs.slice(0)[0] 287 | test.equal('template3.soy', args.pop()) 288 | 289 | time += 1000 290 | return Q.delay(1) 291 | }).then(function () { 292 | return watchCallbacks[1]() 293 | }).then(function () { 294 | var args = spawnArgs.slice(0)[0] 295 | test.equal('template2.soy', args.pop()) 296 | time += 1000 297 | return Q.delay(1) 298 | }).then(function () { 299 | return watchCallbacks[0]() 300 | }).then(function () { 301 | var args = spawnArgs.slice(0)[0] 302 | test.equal('template1.soy', args.pop()) 303 | }) 304 | }) 305 | 306 | function assertTemplatesContents(test, locale, opt_soyCompiler) { 307 | var underTest = opt_soyCompiler || soyCompiler 308 | var template1 = underTest.render('template1.formletter', { title: 'Mr.', surname: 'Pupius' }, null, locale); 309 | var template2 = underTest.render('template2.formletter', { title: 'Mr.', surname: 'Santos' }, null, locale); 310 | 311 | test.equal('string', typeof template1) 312 | test.equal('string', typeof template2) 313 | 314 | switch (locale) { 315 | case 'pt-BR': 316 | test.equal(template1, 'Querido Mr. Pupius: Com um nome como Mr. Pupius, você não deveria ter o seu própro tema musical? Nós podemos ajudar!'); 317 | test.equal(template2, 'Querido Mr. Santos: Com um nome como Mr. Santos, você não deveria ter o seu própro tema musical? Nós podemos ajudar!'); 318 | break; 319 | case 'es': 320 | test.equal(template1, 'Estimado Mr. Pupius: Con un nombre como Mr. Pupius, ¿no debería tener su propia canción? Nosotros podemos ayudarle!'); 321 | test.equal(template2, 'Estimado Mr. Santos: Con un nombre como Mr. Santos, ¿no debería tener su propia canción? Nosotros podemos ayudarle!'); 322 | break; 323 | default: 324 | test.equal(template1, 'Dear Mr. Pupius: With a name like Mr. Pupius, shouldn\'t you have your own theme song? We can help!'); 325 | test.equal(template2, 'Dear Mr. Santos: With a name like Mr. Santos, shouldn\'t you have your own theme song? We can help!'); 326 | break; 327 | } 328 | } 329 | --------------------------------------------------------------------------------