From 1edb87fade7167e80cfb18f7bf2908dd35f3ad4f Mon Sep 17 00:00:00 2001 From: Ben Ng Date: Mon, 1 Jul 2013 22:55:33 -0700 Subject: [PATCH 1/5] Add real CRUD to handlebars/mustache --- .../views/handlebars/add.html.hbs.ejs | 24 ++++++++++--- .../views/handlebars/edit.html.hbs.ejs | 27 +++++++++++--- .../views/handlebars/form.html.hbs.ejs | 36 +++++++++++++++++++ .../views/handlebars/index.html.hbs.ejs | 19 +++++++--- .../views/handlebars/show.html.hbs.ejs | 18 +++++++--- gen/scaffold/views/mustache/add.html.ms.ejs | 22 +++++++++--- gen/scaffold/views/mustache/edit.html.ms.ejs | 25 ++++++++++--- gen/scaffold/views/mustache/form.html.ms.ejs | 36 +++++++++++++++++++ gen/scaffold/views/mustache/index.html.ms.ejs | 19 +++++++--- gen/scaffold/views/mustache/show.html.ms.ejs | 19 +++++++--- 10 files changed, 211 insertions(+), 34 deletions(-) create mode 100644 gen/scaffold/views/handlebars/form.html.hbs.ejs create mode 100644 gen/scaffold/views/mustache/form.html.ms.ejs diff --git a/gen/scaffold/views/handlebars/add.html.hbs.ejs b/gen/scaffold/views/handlebars/add.html.hbs.ejs index c5972a3c..94b9a24b 100644 --- a/gen/scaffold/views/handlebars/add.html.hbs.ejs +++ b/gen/scaffold/views/handlebars/add.html.hbs.ejs @@ -1,6 +1,22 @@
-

Params

- {{#params}} -

{{action}} action on {{controller}} controller.

- {{/params}} +
+
+ Create a new <%= names.constructor.singular %> + {{#if params.errors }} +
+
    + {{#each params.errors}} +
  • {{this}}
  • + {{/each}} +
+
+ {{/if}} + + {{{partial "form" this}}} + +
+ +
+
+
diff --git a/gen/scaffold/views/handlebars/edit.html.hbs.ejs b/gen/scaffold/views/handlebars/edit.html.hbs.ejs index c5972a3c..a5e67dcc 100644 --- a/gen/scaffold/views/handlebars/edit.html.hbs.ejs +++ b/gen/scaffold/views/handlebars/edit.html.hbs.ejs @@ -1,6 +1,23 @@
-

Params

- {{#params}} -

{{action}} action on {{controller}} controller.

- {{/params}} -
+
+
+ Update this <%= names.constructor.singular %> + {{#if params.errors}} +
+
    + {{#each params.errors }} +
  • {{this}}
  • + {{/each}} +
+
+ {{/if}} + + {{{partial "form" this}}} + +
+ + +
+
+
+ \ No newline at end of file diff --git a/gen/scaffold/views/handlebars/form.html.hbs.ejs b/gen/scaffold/views/handlebars/form.html.hbs.ejs new file mode 100644 index 00000000..947d0bc1 --- /dev/null +++ b/gen/scaffold/views/handlebars/form.html.hbs.ejs @@ -0,0 +1,36 @@ + <% for(var i in properties) { -%> + <% if(properties[i].name && properties[i].name !== 'id') { -%> +
+ +
+ <% if(properties[i].type === 'string') { -%> + <% if(properties[i].name === 'password') { -%> + + <% } else { -%> + + <% } -%> + <% } else if(properties[i].type === 'text') { -%> + + <% } else if(properties[i].type === 'number' || properties[i].type === 'int') { -%> + + <% } else if(properties[i].type === 'boolean') { -%> + + <% } else if(properties[i].type === 'datetime') { -%> + + <% } else if(properties[i].type === 'date') { -%> + + <% } else { -%> + <%= properties[i].type %> + <% } -%> +
+
+ <% } -%> + <% } -%> \ No newline at end of file diff --git a/gen/scaffold/views/handlebars/index.html.hbs.ejs b/gen/scaffold/views/handlebars/index.html.hbs.ejs index c5972a3c..f4e5860a 100644 --- a/gen/scaffold/views/handlebars/index.html.hbs.ejs +++ b/gen/scaffold/views/handlebars/index.html.hbs.ejs @@ -1,6 +1,17 @@
-

Params

- {{#params}} -

{{action}} action on {{controller}} controller.

- {{/params}} +

All <%= names.constructor.plural %>

+ Create a new <%= names.constructor.singular %> +
+ +
+{{#each <%= names.property.plural %> }} +
+
+

{{id}}

+
+
+

{{id}}

+
+
+{{/each}}
diff --git a/gen/scaffold/views/handlebars/show.html.hbs.ejs b/gen/scaffold/views/handlebars/show.html.hbs.ejs index c5972a3c..12a315e3 100644 --- a/gen/scaffold/views/handlebars/show.html.hbs.ejs +++ b/gen/scaffold/views/handlebars/show.html.hbs.ejs @@ -1,6 +1,16 @@
-

Params

- {{#params}} -

{{action}} action on {{controller}} controller.

- {{/params}} +

">{{ <%= names.property.singular %>.<%= properties['default'].name || "id" %> }}

+ Edit this <%= names.property.singular %>
+ +

<%= names.constructor.singular %> Properties

+{{#each <%= names.property.singular %> }} +
+
+

{{@key}}

+
+
+

{{ this }}

+
+
+{{/each}} \ No newline at end of file diff --git a/gen/scaffold/views/mustache/add.html.ms.ejs b/gen/scaffold/views/mustache/add.html.ms.ejs index c5972a3c..09797745 100644 --- a/gen/scaffold/views/mustache/add.html.ms.ejs +++ b/gen/scaffold/views/mustache/add.html.ms.ejs @@ -1,6 +1,20 @@
-

Params

- {{#params}} -

{{action}} action on {{controller}} controller.

- {{/params}} +
+
+ Create a new <%= names.constructor.singular %> +
+
    + {{#params.errors}} +
  • {{.}}
  • + {{/params.errors}} +
+
+ + {{{partial "form" this}}} + +
+ +
+
+
diff --git a/gen/scaffold/views/mustache/edit.html.ms.ejs b/gen/scaffold/views/mustache/edit.html.ms.ejs index c5972a3c..59585490 100644 --- a/gen/scaffold/views/mustache/edit.html.ms.ejs +++ b/gen/scaffold/views/mustache/edit.html.ms.ejs @@ -1,6 +1,21 @@
-

Params

- {{#params}} -

{{action}} action on {{controller}} controller.

- {{/params}} -
+
+
+ Update this <%= names.constructor.singular %> +
+
    + {{#params.errors}} +
  • {{.}}
  • + {{/params.errors}} +
+
+ + {{{partial "form" this}}} + +
+ + +
+
+
+ \ No newline at end of file diff --git a/gen/scaffold/views/mustache/form.html.ms.ejs b/gen/scaffold/views/mustache/form.html.ms.ejs new file mode 100644 index 00000000..947d0bc1 --- /dev/null +++ b/gen/scaffold/views/mustache/form.html.ms.ejs @@ -0,0 +1,36 @@ + <% for(var i in properties) { -%> + <% if(properties[i].name && properties[i].name !== 'id') { -%> +
+ +
+ <% if(properties[i].type === 'string') { -%> + <% if(properties[i].name === 'password') { -%> + + <% } else { -%> + + <% } -%> + <% } else if(properties[i].type === 'text') { -%> + + <% } else if(properties[i].type === 'number' || properties[i].type === 'int') { -%> + + <% } else if(properties[i].type === 'boolean') { -%> + + <% } else if(properties[i].type === 'datetime') { -%> + + <% } else if(properties[i].type === 'date') { -%> + + <% } else { -%> + <%= properties[i].type %> + <% } -%> +
+
+ <% } -%> + <% } -%> \ No newline at end of file diff --git a/gen/scaffold/views/mustache/index.html.ms.ejs b/gen/scaffold/views/mustache/index.html.ms.ejs index c5972a3c..1c7fff52 100644 --- a/gen/scaffold/views/mustache/index.html.ms.ejs +++ b/gen/scaffold/views/mustache/index.html.ms.ejs @@ -1,6 +1,17 @@
-

Params

- {{#params}} -

{{action}} action on {{controller}} controller.

- {{/params}} +

All <%= names.constructor.plural %>

+ Create a new <%= names.constructor.singular %> +
+ +
+{{#<%= names.property.plural %>}} +
+
+

{{id}}

+
+
+

{{id}}

+
+
+{{/<%= names.property.plural %>}}
diff --git a/gen/scaffold/views/mustache/show.html.ms.ejs b/gen/scaffold/views/mustache/show.html.ms.ejs index c5972a3c..25011a2a 100644 --- a/gen/scaffold/views/mustache/show.html.ms.ejs +++ b/gen/scaffold/views/mustache/show.html.ms.ejs @@ -1,6 +1,17 @@
-

Params

- {{#params}} -

{{action}} action on {{controller}} controller.

- {{/params}} +

">{{ <%= names.property.singular %>.<%= properties['default'].name || "id" %> }}

+ Edit this <%= names.property.singular %>
+ +

<%= names.constructor.singular %> Properties

+<% for(var i in properties) { -%> +
+
+

<%= properties[i].name %>

+
+
+

+ <% if(properties[i].name === 'id') { %>{{ params.id }}<% } else { %>{{ <%= names.property.singular %>.<%= properties[i].name %> }}<% } %>

+
+
+<% } -%> \ No newline at end of file From 2c252920e86a3ba839034f49df71ef69028980e7 Mon Sep 17 00:00:00 2001 From: Ben Ng Date: Mon, 1 Jul 2013 22:55:53 -0700 Subject: [PATCH 2/5] Fix missing property key in swig show --- gen/scaffold/views/swig/show.html.swig.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gen/scaffold/views/swig/show.html.swig.ejs b/gen/scaffold/views/swig/show.html.swig.ejs index eef7a8dc..2a9bfc1b 100644 --- a/gen/scaffold/views/swig/show.html.swig.ejs +++ b/gen/scaffold/views/swig/show.html.swig.ejs @@ -10,7 +10,7 @@

{{loop.key}}

-

{{ prop }}

+

{{ prop }}

{% endfor %} \ No newline at end of file From 3fc40c74939a56ab9400ecb6bdf570a4df23efa5 Mon Sep 17 00:00:00 2001 From: Ben Ng Date: Mon, 1 Jul 2013 22:57:10 -0700 Subject: [PATCH 3/5] Add tests for standard templates --- .gitignore | 1 + Jakefile | 10 +- test/{ => cli}/cmd.js | 2 +- test/scaffolding/standard.js | 365 +++++++++++++++++++++++++++++++++++ 4 files changed, 376 insertions(+), 2 deletions(-) rename test/{ => cli}/cmd.js (95%) create mode 100644 test/scaffolding/standard.js diff --git a/.gitignore b/.gitignore index d6ad3d00..f15132eb 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ site/log/* .log npm-debug.log doc/ +test/tmp diff --git a/Jakefile b/Jakefile index c3427857..b986b784 100644 --- a/Jakefile +++ b/Jakefile @@ -2,6 +2,8 @@ require('./lib/geddy') var fs = require('fs') + , path = require('path') + , utils = require('utilities') , createPackageTask , JSPAT = /\.js$/ , testTask; @@ -61,7 +63,7 @@ testTask = new jake.TestTask('Geddy', function () { }); desc('Run the Geddy tests'); -task('test', function () { +task('test', ['clean'], function () { var t = jake.Task.testBase; t.addListener('error', function (err) { var module @@ -89,3 +91,9 @@ task('test', function () { t.invoke.apply(t, arguments); }, {async: true}); +desc('Clears the test temp dir'); +task('clean', function () { + tmpDir = path.join(__dirname, 'test', 'tmp'); + utils.file.rmRf(tmpDir, {silent:true}); + fs.mkdirSync(tmpDir); +}); diff --git a/test/cmd.js b/test/cli/cmd.js similarity index 95% rename from test/cmd.js rename to test/cli/cmd.js index d7455abc..8220500c 100644 --- a/test/cmd.js +++ b/test/cli/cmd.js @@ -1,5 +1,5 @@ var assert = require('assert') - , cmd = require('../lib/cmd') + , cmd = require('../../lib/cmd') , Cmd = cmd.Cmd , tests; diff --git a/test/scaffolding/standard.js b/test/scaffolding/standard.js new file mode 100644 index 00000000..f07a685f --- /dev/null +++ b/test/scaffolding/standard.js @@ -0,0 +1,365 @@ +(function () { + var utils = require('utilities') + , assert = require('assert') + , request = require('request') + , path = require('path') + , fs = require('fs') + , cmd = require('../../lib/cmd').Cmd + , geddyCli = path.join(__dirname,'../','../','bin','cli')+'.js' + , tmpDir = path.join(__dirname, '../', 'tmp') + , staticId = utils.string.uuid(10) + , engines = { + ejs: null + , jade: '-j' + , swig: '-s' + , handlebars: '-H' + , mustache: '-m' + } + /* + * Runs `geddy gen app` with the specified template engine + */ + , scaffoldApp = function (engine, flag, cb) { + var spawn = require('child_process').spawn + , testDir = tmpDir + , cmd = geddyCli + , opts = ['gen', 'app', engine + 'App'] + , srcModules = path.join(__dirname, '..', '..', 'node_modules') + , appDir = path.join(testDir, engine + 'App') + , proc; + + if(flag) { + opts.push(flag); + } + + proc = spawn(cmd, opts, {cwd:testDir}); + + proc.stderr.setEncoding('utf8'); + + proc.stderr.on('data', function (data) { + console.error(data); + if (/^execvp\(\)/.test(data)) { + assert.ok(false, 'Failed to generate scaffolding for ' + engine); + } + }); + + proc.on('close', function (code) { + // Create a copy of node_modules so the generated apps don't need to + // do an `npm install` + utils.file.cpR(srcModules, appDir, {silent: true}); + cb(); + }); + } + /* + * Runs `geddy gen scaffold zoobies foo:string bar:number` + * with the specified template engine + */ + , scaffoldResource = function (engine, flag, cb) { + var spawn = require('child_process').spawn + , testAppDir = path.join(tmpDir, engine + 'App') + , cmd = geddyCli + , opts = ['gen', 'scaffold'] + , proc; + + if(flag) { + opts.push(flag); + } + + opts = opts.concat(['zoobies', 'foo:string', 'bar:number']); + + proc = spawn(cmd, opts, {cwd:testAppDir}); + + proc.stderr.setEncoding('utf8'); + + proc.stderr.on('data', function (data) { + console.error(data); + if (/^execvp\(\)/.test(data)) { + assert.ok(false, 'Failed to generate scaffolding for ' + engine); + } + }); + + proc.on('close', function (code) { + cb(); + }); + } + /* + * Starts a geddy server + */ + , startServer = function (engine, port, cb) { + var spawn = require('child_process').spawn + , testAppDir = path.join(tmpDir, engine + 'App') + , cmd = geddyCli + , opts = ['--port', port] + , server + , notified = false + , failed = false; + + server = spawn(cmd, opts, {cwd:testAppDir}); + + server.stderr.setEncoding('utf8'); + + server.stderr.on('data', function (data) { + console.log(data); + }); + + console.log("Starting " + engine + " app on port " + port); + + server.stdout.on('data', function (data) { + var ready = data.toString().match(/Server worker running in [a-z]+? on port [0-9]+? with a PID of: [0-9]+?/)?true:false; + + if(!notified && ready) { + notified = true; + cb(server); + } + }); + } + /* + * Kills a geddy server + */ + , killServer = function (server, cb) { + server.on('close', function (code) { + cb(); + }); + + server.kill("SIGHUP"); + } + , tests = {} + , servers = [] + , port = 8080 + , base = 'http://127.0.0.1:' + , baseUrl + , reqOpts = { + timeout: 1000 + }; + + /* + * CRUD Tests + */ + for(var engine in engines) { + (function (baseUrl) { + tests["test " + engine + " shows site index"] = function(next) { + //Request index + request(baseUrl, reqOpts, function(err, res, body) { + assert.strictEqual(err, null, err); + assert.strictEqual(res.statusCode, 200, 'Error '+res.statusCode+' when requesting ' + baseUrl); + assert.notStrictEqual(body.match(/Hello, World!/), null, 'Hello world text is missing') + next(); + }); + }; + + tests["test " + engine + " index when empty"] = function(next) { + //Request index + request(baseUrl + '/zoobies', reqOpts, function(err, res, body) { + assert.strictEqual(err, null, err); + assert.strictEqual(res.statusCode, 200, 'Error '+res.statusCode+' when requesting /zoobies: ' + body); + assert.notStrictEqual(body.match(/
\s*<\/div>/gi), null, 'There should be an empty zoobies list') + next(); + }); + }; + + tests["test " + engine + " create entry"] = function(next) { + //Request index + request({ + method: 'POST' + , uri: baseUrl + '/zoobies' + , form: { + foo: 'zerb' + , bar: '2112' + , id: staticId // Just use a static ID to keep tests cleaner + } + , timeout: 1000 + , followAllRedirects: true + } + , function(err, res, body) { + assert.strictEqual(err, null, err); + assert.strictEqual(res.statusCode, 200, 'Error '+res.statusCode+' when creating zoobies'); + assert.notStrictEqual(body.match(/id="zooby-[a-z0-9\-]+"/gi), null, 'There should be a zooby in the list') + assert.strictEqual(body.match(/id="zooby-[a-z0-9\-]+"/gi).length, 1, 'There should be exactly one zooby in the list') + assert.strictEqual(body.indexOf('id="zooby-' + staticId + '"')>=0, true, 'The zooby should have the expected ID') + next(); + }); + }; + + tests["test " + engine + " show initial values"] = function(next) { + //Request index + request({ + method: 'GET' + , uri: baseUrl + '/zoobies/' + staticId + , timeout: 1000 + } + , function(err, res, body) { + assert.strictEqual(err, null, err); + assert.strictEqual(res.statusCode, 200, 'Error '+res.statusCode+' when showing zoobies'); + assert.strictEqual(body.indexOf('zerb') >= 0 , true , 'Foo was persisted correctly') + assert.strictEqual(body.indexOf('2112') >= 0 , true , 'Bar was persisted correctly') + next(); + }); + }; + + tests["test " + engine + " update form persists values"] = function(next) { + //Request index + request({ + method: 'GET' + , uri: baseUrl + '/zoobies/' + staticId + '/edit' + , timeout: 1000 + } + , function(err, res, body) { + assert.strictEqual(err, null, err); + assert.strictEqual(res.statusCode, 200, 'Error '+res.statusCode+' when updating zooby'); + + // FIXME: Is there a less stupid way of asserting this? + + assert.strictEqual( + body.indexOf('input type="text" class="span6" name="foo" value="zerb"') >= 0 // ms + ||body.indexOf('input class="span6" name="foo" type="text" value="zerb"') >= 0 // ejs + , true , 'Foo was persisted correctly') + + assert.strictEqual( + body.indexOf('input type="number" class="span2" name="bar" value="2112"') >= 0 // ms + ||body.indexOf('input class="span2" name="bar" type="number" value="2112"') >= 0 // ejs + , true , 'Bar was persisted correctly') + + next(); + }); + }; + + tests["test " + engine + " update values"] = function(next) { + //Request index + request({ + method: 'PUT' + , uri: baseUrl + '/zoobies/' + staticId + , form: { + foo: 'overture' + , bar: '1984' + } + , timeout: 1000 + , followAllRedirects: true + } + , function(err, res, body) { + assert.strictEqual(err, null, err); + assert.strictEqual(res.statusCode, 200, 'Error '+res.statusCode+' when updating zooby'); + assert.notStrictEqual(body.match(/id="zooby-[a-z0-9\-]+"/gi), null, 'There should be a zooby in the list') + assert.strictEqual(body.match(/id="zooby-[a-z0-9\-]+"/gi).length, 1, 'There should be exactly one zooby in the list') + assert.strictEqual(body.indexOf('id="zooby-' + staticId + '"')>=0, true, 'The zooby should have the expected ID') + next(); + }); + }; + + tests["test " + engine + " show updated values"] = function(next) { + //Request index + request({ + method: 'GET' + , uri: baseUrl + '/zoobies/' + staticId + , timeout: 1000 + } + , function(err, res, body) { + assert.strictEqual(err, null, err); + assert.strictEqual(res.statusCode, 200, 'Error '+res.statusCode+' when showing zoobies'); + assert.strictEqual(body.indexOf('overture') >= 0 , true , 'Foo was updated correctly') + assert.strictEqual(body.indexOf('1984') >= 0 , true , 'Bar was updated correctly') + next(); + }); + }; + + tests["test " + engine + " delete entry"] = function(next) { + //Request index + request({ + method: 'DELETE' + , uri: baseUrl + '/zoobies/' + staticId + , timeout: 1000 + , followAllRedirects: true + } + , function(err, res, body) { + assert.strictEqual(err, null, err); + assert.strictEqual(res.statusCode, 200, 'Error '+res.statusCode+' when deleting zooby'); + assert.strictEqual(body.match(/id="zooby-[a-z0-9\-]+"/gi), null, 'There should be no zoobies in the list') + next(); + }); + }; + + tests["test " + engine + " 404"] = function(next) { + //Request index + request({ + method: 'GET' + , uri: baseUrl + '/zoobies/' + staticId + , timeout: 1000 + } + , function(err, res, body) { + assert.strictEqual(err, null, err); + assert.strictEqual(res.statusCode, 404, 'Should get error 404 viewing nonexistant zooby'); + next(); + }); + }; + + }(base + port)); + + // Increment Port Number + port++; + } + + /* + * The before action will generate the scaffolded apps + */ + tests.before = function (next) { + var generators = [] + , chain + , port = 8080; + + // Generate a new app for each engine + for(var engine in engines) { + (function (serverPort) { + generators.push({ + func: scaffoldApp + , args: [engine, engines[engine]] + , callback: null + }); + generators.push({ + func: scaffoldResource + , args: [engine, engines[engine]] + , callback: null + }); + generators.push({ + func: startServer + , args: [engine, serverPort] + , callback: function (server) { + servers.push(server); + } + }); + }(port)); + + // Increment port number + port++; + } + + chain = new utils.async.AsyncChain(generators); + + chain.last = next; + + chain.run(); + }; + + /* + * The after action cleans the temporary directory + */ + tests.after = function (next) { + //Kill all the servers + var killers = []; + for(var key in servers) { + killers.push({ + func: killServer + , args: [servers[key]] + , callback: null + }); + } + + chain = new utils.async.AsyncChain(killers); + + chain.last = function () { + //utils.file.rmRf(tmpDir, {silent:true}); + next(); + }; + + chain.run(); + }; + + module.exports = tests; +}()); From 4619ff4004a512345b869d634722d0eddc3d667d Mon Sep 17 00:00:00 2001 From: Ben Ng Date: Mon, 1 Jul 2013 21:01:40 -0700 Subject: [PATCH 4/5] Update testing script --- .travis.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9fdebd6f..0d3471f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,4 +2,10 @@ language: node_js node_js: - "0.10" - "0.8" - - "0.6" \ No newline at end of file + - "0.6" + +before_script: + - rm -R node_modules/model + - rm -R node_modules/utilities + - npm install https://github.com/mde/model/archive/master.tar.gz + - npm install https://github.com/mde/utilities/archive/master.tar.gz From 6f1e191e46770785a02c0970abe5bcb5bc1ca7a6 Mon Sep 17 00:00:00 2001 From: Ben Ng Date: Mon, 1 Jul 2013 21:05:39 -0700 Subject: [PATCH 5/5] Add dependencies and specify script --- .travis.yml | 2 ++ package.json | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0d3471f5..662e40a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,3 +9,5 @@ before_script: - rm -R node_modules/utilities - npm install https://github.com/mde/model/archive/master.tar.gz - npm install https://github.com/mde/utilities/archive/master.tar.gz + +script: node_modules/jake/bin/cli.js test diff --git a/package.json b/package.json index 0a3c65c5..4b76c744 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,11 @@ "preferGlobal": true, "devDependencies": { "browserify": "1.16.x", - "socket.io-client": "0.9.x" + "socket.io-client": "0.9.x", + "handlebars": "latest", + "jade": "latest", + "swig": "latest", + "ejs": "latest" }, "engines": { "node": "*"