Replace JSLint with JSHint.

closes #2277
- Added ES6 linting to core/client/
- Fix typeof array comparison
This commit is contained in:
Fabian Becker 2014-02-27 02:44:09 +00:00
parent c4bf3052e1
commit 1a9e91f120
40 changed files with 177 additions and 127 deletions

View File

@ -113,21 +113,39 @@ var path = require('path'),
}
},
// ### Config for grunt-jslint
// JSLint all the things!
jslint: {
// ### Config for grunt-contrib-jshint
// JSHint all the things!
jshint: {
server: {
directives: {
options: {
// node environment
node: true,
// browser environment
browser: false,
// allow dangling underscores in var names
nomen: true,
// allow to do statements
todo: true,
nomen: false,
// don't require use strict pragma
sloppy: true
strict: false,
sub: true,
eqeqeq: true,
laxbreak: true,
bitwise: true,
curly: true,
forin: true,
immed: true,
latedef: true,
newcap: true,
noarg: true,
noempty: true,
nonew: true,
plusplus: true,
regexp: true,
undef: true,
unused: true,
trailing: true,
indent: 4,
onevar: true,
white: true
},
files: {
src: [
@ -138,48 +156,89 @@ var path = require('path'),
}
},
client: {
directives: {
options: {
"predef": {
"document": true,
"window": true,
"location": true,
"setTimeout": true,
"Ember": true,
"Em": true,
"DS": true,
"$": true
},
// node environment
node: false,
// browser environment
browser: true,
// allow dangling underscores in var names
nomen: true,
// allow to do statements
todo: true
nomen: false,
bitwise: true,
curly: true,
eqeqeq: true,
forin: true,
immed: true,
latedef: true,
newcap: true,
noarg: true,
noempty: true,
nonew: true,
plusplus: true,
regexp: true,
undef: true,
unused: true,
trailing: true,
indent: 4,
esnext: true,
onevar: true,
white: true
},
files: {
src: 'core/client/**/*.js'
},
exclude: [
'core/client/assets/vendor/**/*.js',
'core/client/tpl/**/*.js'
src: [
'core/client/**/*.js',
// Ignore files
'!core/client/assets/vendor/**/*.js',
'!core/client/tpl/**/*.js'
]
}
},
shared: {
directives: {
options: {
// node environment
node: true,
// browser environment
browser: false,
// allow dangling underscores in var names
nomen: true,
// allow to do statements
todo: true,
// allow unused parameters
unparam: true,
// don't require use strict pragma
sloppy: true
strict: false,
// allow dangling underscores in var names
nomen: false,
bitwise: true,
curly: true,
eqeqeq: true,
forin: true,
immed: true,
latedef: true,
newcap: true,
noarg: true,
noempty: true,
nonew: true,
plusplus: true,
regexp: true,
undef: true,
unused: true,
trailing: true,
indent: 4,
onevar: true,
white: true
},
files: {
src: [
'core/shared/**/*.js'
]
},
exclude: [
'core/shared/vendor/**/*.js'
'core/shared/**/*.js',
// Ignore files
'!core/shared/vendor/**/*.js'
]
}
}
},
// ### Config for grunt-mocha-cli
@ -520,7 +579,7 @@ var path = require('path'),
stdio: 'inherit'
}
}, function (error, result, code) {
/*jslint unparam:true*/
/*jshint unused:false*/
if (error) {
grunt.fail.fatal(result.stdout);
}
@ -649,7 +708,7 @@ var path = require('path'),
data.replace(
commitRegex,
function (wholeCommit, hash, author, email, date, message) {
/*jslint unparam:true*/
/*jshint unused:false*/
// The author name and commit message may have trailing space.
author = author.trim();
@ -760,7 +819,7 @@ var path = require('path'),
when.reduce(tags,
function (prev, tag, idx) {
/*jslint unparam:true*/
/*jshint unused:false*/
return when.promise(function (resolve) {
processTag(tag, function (releaseData) {
resolve(prev + '\n' + releaseData);
@ -824,7 +883,7 @@ var path = require('path'),
grunt.registerTask('test-routes', 'Run functional route tests (mocha)', ['clean:test', 'setTestEnv', 'loadConfig', 'express:test', 'mochacli:routes', 'express:test:stop']);
grunt.registerTask('validate', 'Run tests and lint code', ['jslint', 'test-routes', 'test-unit', 'test-api', 'test-integration', 'test-functional']);
grunt.registerTask('validate', 'Run tests and lint code', ['jshint', 'test-routes', 'test-unit', 'test-api', 'test-integration', 'test-functional']);
// ### Coverage report for Unit and Integration Tests

5
core/bootstrap.js vendored
View File

@ -7,7 +7,6 @@
var fs = require('fs'),
url = require('url'),
when = require('when'),
path = require('path'),
errors = require('./server/errorHandling'),
config = require('./server/config'),
@ -36,14 +35,14 @@ function writeConfigFile() {
// Copy config.example.js => config.js
read = fs.createReadStream(configExample);
read.on('error', function (err) {
/*jslint unparam:true*/
/*jshint unused:false*/
return errors.logError(new Error('Could not open config.example.js for read.'), appRoot, 'Please check your deployment for config.js or config.example.js.');
});
read.on('end', written.resolve);
write = fs.createWriteStream(configFile);
write.on('error', function (err) {
/*jslint unparam:true*/
/*jshint unused:false*/
return errors.logError(new Error('Could not open config.js for write.'), appRoot, 'Please check your deployment for config.js or config.example.js.');
});

View File

@ -16,7 +16,7 @@
* @returns {boolean}
*/
$.expr[":"].containsExact = function (obj, index, meta, stack) {
/*jslint unparam:true*/
/*jshint unused:false*/
return (obj.textContent || obj.innerText || $(obj).text() || "") === meta[3];
};

View File

@ -1,4 +1,4 @@
/*global jQuery, Ghost, document, Image, window */
/*global jQuery, Ghost */
(function ($) {
"use strict";
@ -68,7 +68,7 @@
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
},
add: function (e, data) {
/*jslint unparam:true*/
/*jshint unused:false*/
$('.js-button-accept').prop('disabled', true);
$dropzone.find('.js-fileupload').removeClass('right');
$dropzone.find('.js-url').remove();
@ -86,7 +86,7 @@
},
dropZone: settings.fileStorage ? $dropzone : null,
progressall: function (e, data) {
/*jslint unparam:true*/
/*jshint unused:false*/
var progress = parseInt(data.loaded / data.total * 100, 10);
if (!settings.editor) {$progress.find('div.js-progress').css({"position": "absolute", "top": "40px"}); }
if (settings.progressbar) {
@ -95,7 +95,7 @@
}
},
fail: function (e, data) {
/*jslint unparam:true*/
/*jshint unused:false*/
$('.js-button-accept').prop('disabled', false);
$dropzone.trigger("uploadfailure", [data.result]);
$dropzone.find('.js-upload-progress-bar').addClass('fail');
@ -115,7 +115,7 @@
});
},
done: function (e, data) {
/*jslint unparam:true*/
/*jshint unused:false*/
self.complete(data.result);
}
});

View File

@ -1,6 +1,6 @@
// # Surrounds given text with Markdown syntax
/*global $, window, CodeMirror, Showdown, moment */
/*global $, CodeMirror, Showdown, moment */
(function () {
'use strict';
var Markdown = {

View File

@ -1,4 +1,4 @@
/*global window, document, setTimeout, Ghost, $, _, Backbone, JST, shortcut, NProgress */
/*global Ghost, _, Backbone, NProgress */
(function () {
"use strict";
@ -10,8 +10,10 @@
if (options !== undefined && _.isObject(options)) {
NProgress.start();
/*jshint validthis:true */
var self = this,
oldSuccess = options.success;
/*jshint validthis:false */
options.success = function () {
NProgress.done();
@ -19,6 +21,7 @@
};
}
/*jshint validthis:true */
return Backbone.sync.call(this, method, model, options);
}

View File

@ -1,4 +1,4 @@
/*global window, document, Ghost, $, _, Backbone */
/*global Ghost, _, Backbone */
(function () {
'use strict';

View File

@ -1,4 +1,4 @@
/*global window, document, Ghost, $, _, Backbone */
/*global Ghost */
(function () {
'use strict';
//id:0 is used to issue PUT requests

View File

@ -1,4 +1,4 @@
/*global window, document, Ghost, $, _, Backbone */
/*global Ghost */
(function () {
'use strict';

View File

@ -1,4 +1,4 @@
/*global window, document, Ghost, $, _, Backbone */
/*global Ghost, Backbone */
(function () {
'use strict';

View File

@ -1,4 +1,4 @@
/*global window, document, Ghost, $, _, Backbone */
/*global Ghost */
(function () {
'use strict';

View File

@ -1,4 +1,4 @@
/*global window, document, Ghost, $, _, Backbone */
/*global Ghost */
(function () {
'use strict';

View File

@ -1,4 +1,4 @@
/*global window, document, Ghost, Backbone, $, _, NProgress */
/*global Ghost, Backbone, NProgress */
(function () {
"use strict";

View File

@ -215,7 +215,7 @@
},
url: Ghost.paths.apiRoot + '/notifications/' + $(self).find('.close').data('id')
}).done(function (result) {
/*jslint unparam:true*/
/*jshint unused:false*/
bbSelf.$el.slideUp(250, function () {
$(this).show().css({height: "auto"});
$(self).remove();
@ -249,7 +249,7 @@
},
url: Ghost.paths.apiRoot + '/notifications/' + $(self).data('id')
}).done(function (result) {
/*jslint unparam:true*/
/*jshint unused:false*/
var height = bbSelf.$('.js-notification').outerHeight(true),
$parent = $(self).parent();
bbSelf.$el.css({height: height});

View File

@ -1,4 +1,4 @@
/*global window, document, Ghost, $, _, Backbone, JST, NProgress */
/*global window, Ghost, $, _, Backbone, NProgress */
(function () {
"use strict";
@ -10,7 +10,7 @@
// ----------
Ghost.Views.Blog = Ghost.View.extend({
initialize: function (options) {
/*jslint unparam:true*/
/*jshint unused:false*/
var self = this,
finishProgress = function () {
NProgress.done();
@ -108,7 +108,7 @@
staticPages: 'all'
}
}).then(function onSuccess(response) {
/*jslint unparam:true*/
/*jshint unused:false*/
self.render();
self.isLoading = false;
}, function onError(e) {
@ -245,7 +245,7 @@
});
},
error : function (model, xhr) {
/*jslint unparam:true*/
/*jshint unused:false*/
Ghost.notifications.addItem({
type: 'error',
message: Ghost.Views.Utils.getRequestErrorMessage(xhr),

View File

@ -1,4 +1,4 @@
/*global window, document, Ghost, $, _, Backbone, JST */
/*global Ghost, $ */
(function () {
"use strict";
@ -25,7 +25,7 @@
},
dataType: 'json',
add: function (e, data) {
/*jslint unparam:true*/
/*jshint unused:false*/
// Bind the upload data to the view, so it is
// available to the click handler, and enable the
@ -34,7 +34,7 @@
data.context = view.uploadButton.removeProp('disabled');
},
done: function (e, data) {
/*jslint unparam:true*/
/*jshint unused:false*/
$('#startupload').text('Import');
if (!data.result) {
throw new Error('No response received from server.');

View File

@ -1,6 +1,6 @@
// The Tag UI area associated with a post
/*global window, document, setTimeout, $, _, Backbone, Ghost */
/*global window, document, setTimeout, $, _, Ghost */
(function () {
"use strict";

View File

@ -1,6 +1,6 @@
// # Article Editor
/*global window, document, setTimeout, navigator, $, _, Backbone, Ghost, Showdown, CodeMirror, shortcut, Countable, JST */
/*global window, document, setTimeout, navigator, $, _, Backbone, Ghost, Showdown, CodeMirror, shortcut, Countable */
(function () {
"use strict";
@ -704,7 +704,7 @@
var value = editor.getValue();
_.each(markerMgr.markers, function (marker, id) {
/*jslint unparam:true*/
/*jshint unused:false*/
value = value.replace(markerMgr.getMarkerRegexForId(id), '');
});
@ -720,7 +720,7 @@
// initialise
editor.on('change', function (cm, changeObj) {
/*jslint unparam:true*/
/*jshint unused:false*/
var linesChanged = _.range(changeObj.from.line, changeObj.from.line + changeObj.text.length);
_.each(linesChanged, function (ln) {

View File

@ -1,4 +1,4 @@
/*global window, document, Ghost, $, _, Backbone, JST */
/*global window, Ghost, $ */
(function () {
"use strict";

View File

@ -1,6 +1,6 @@
// The Post Settings Menu available in the content preview screen, as well as the post editor.
/*global window, document, $, _, Backbone, Ghost, moment */
/*global window, $, _, Ghost, moment */
(function () {
"use strict";
@ -124,7 +124,7 @@
slug: newSlug
}, {
success : function (model, response, options) {
/*jslint unparam:true*/
/*jshint unused:false*/
// Repopulate slug in case it changed on the server (e.g. 'new-slug-2')
slugEl.value = model.get('slug');
Ghost.notifications.addItem({
@ -134,7 +134,7 @@
});
},
error : function (model, xhr) {
/*jslint unparam:true*/
/*jshint unused:false*/
slugEl.value = model.previous('slug');
Ghost.notifications.addItem({
type: 'error',
@ -244,7 +244,7 @@
});
},
error : function (model, xhr) {
/*jslint unparam:true*/
/*jshint unused:false*/
// Reset back to original value
pubDateEl.value = pubDateMoment ? pubDateMoment.format(displayDateFormat) : '';
Ghost.notifications.addItem({
@ -275,7 +275,7 @@
page: page
}, {
success : function (model, response, options) {
/*jslint unparam:true*/
/*jshint unused:false*/
pageEl.prop('checked', page);
Ghost.notifications.addItem({
type: 'success',
@ -284,7 +284,7 @@
});
},
error : function (model, xhr) {
/*jslint unparam:true*/
/*jshint unused:false*/
pageEl.prop('checked', model.previous('page'));
Ghost.notifications.addItem({
type: 'error',

View File

@ -1,4 +1,4 @@
/*global window, document, Ghost, $, _, Backbone, Countable */
/*global document, Ghost, $, _, Countable */
(function () {
"use strict";
@ -112,7 +112,7 @@
this.$el.addClass('active');
},
saveSuccess: function (model, response, options) {
/*jslint unparam:true*/
/*jshint unused:false*/
Ghost.notifications.clearEverything();
Ghost.notifications.addItem({
type: 'success',
@ -121,7 +121,7 @@
});
},
saveError: function (model, xhr) {
/*jslint unparam:true*/
/*jshint unused:false*/
Ghost.notifications.clearEverything();
Ghost.notifications.addItem({
type: 'error',

View File

@ -2,12 +2,10 @@ var dataExport = require('../data/export'),
dataImport = require('../data/import'),
dataProvider = require('../models'),
fs = require('fs-extra'),
path = require('path'),
when = require('when'),
nodefn = require('when/node/function'),
_ = require('lodash'),
validation = require('../data/validation'),
config = require('../config'),
errors = require('../../server/errorHandling'),
api = {},
db;

View File

@ -4,7 +4,6 @@
var _ = require('lodash'),
when = require('when'),
config = require('../config'),
errors = require('../errorHandling'),
db = require('./db'),
settings = require('./settings'),
notifications = require('./notifications'),

View File

@ -1,6 +1,5 @@
var fs = require('fs'),
path = require('path'),
var path = require('path'),
Module = require('module'),
_ = require('lodash');

View File

@ -100,7 +100,7 @@ function config() {
if (_.isEmpty(ghostConfig)) {
try {
ghostConfig = require(path.resolve(__dirname, '../../../', 'config.js'))[process.env.NODE_ENV] || {};
} catch (ignore) {/*jslint sloppy: true */}
} catch (ignore) {/*jslint strict: true */}
ghostConfig = updateConfig(ghostConfig);
}

View File

@ -13,7 +13,6 @@ var moment = require('moment'),
api = require('../api'),
config = require('../config'),
errors = require('../errorHandling'),
filters = require('../../server/filters'),
template = require('../helpers/template'),

View File

@ -3,8 +3,7 @@ var sequence = require('when/sequence'),
Post = require('../../models/post').Post,
Tag = require('../../models/tag').Tag,
Role = require('../../models/role').Role,
Permission = require('../../models/permission').Permission,
uuid = require('node-uuid');
Permission = require('../../models/permission').Permission;
var fixtures = {
posts: [

View File

@ -1,7 +1,6 @@
var when = require('when'),
_ = require('lodash'),
models = require('../../models'),
errors = require('../../errorHandling'),
Importer000;

View File

@ -1,7 +1,6 @@
var schema = require('../schema').tables,
_ = require('lodash'),
validator = require('validator'),
when = require('when'),
validateSchema,
validateSettings,
@ -74,10 +73,11 @@ validate = function (value, key, validations) {
if (validationOptions === true) {
validationOptions = null;
}
/* jshint ignore:start */
if (typeof validationOptions !== 'array') {
validationOptions = [validationOptions];
}
/* jshint ignore:end */
// equivalent of validation.isSomething(option1, option2)
validation[validationName].apply(validation, validationOptions);
}, this);

View File

@ -1,7 +1,6 @@
/*jslint regexp: true */
var _ = require('lodash'),
colors = require('colors'),
fs = require('fs'),
config = require('./config'),
path = require('path'),
when = require('when'),
@ -15,6 +14,9 @@ var _ = require('lodash'),
ONE_HOUR_S = 60 * 60;
// This is not useful but required for jshint
colors.setTheme({silly: 'rainbow'});
/**
* Basic error handling helpers
*/
@ -107,7 +109,7 @@ errors = {
},
logErrorWithRedirect: function (msg, context, help, redirectTo, req, res) {
/*jslint unparam:true*/
/*jshint unused:false*/
var self = this;
return function () {
@ -120,7 +122,7 @@ errors = {
},
renderErrorPage: function (code, err, req, res, next) {
/*jslint unparam:true*/
/*jshint unused:false*/
var self = this;

View File

@ -1,7 +1,6 @@
var downsize = require('downsize'),
hbs = require('express-hbs'),
moment = require('moment'),
path = require('path'),
polyglot = require('node-polyglot').instance,
_ = require('lodash'),
when = require('when'),
@ -96,7 +95,7 @@ coreHelpers.encode = function (context, str) {
// context.
//
coreHelpers.page_url = function (context, block) {
/*jslint unparam:true*/
/*jshint unused:false*/
var url = config().paths.subdir;
if (this.tagSlug !== undefined) {
@ -125,7 +124,7 @@ coreHelpers.pageUrl = function (context, block) {
'The helper pageUrl has been replaced with page_url in Ghost 0.5, and will be removed entirely in Ghost 0.6\n' +
'In your theme\'s pagination.hbs file, pageUrl should be renamed to page_url');
/*jslint unparam:true*/
/*jshint unused:false*/
var self = this;
return coreHelpers.page_url.call(self, context, block);
@ -198,7 +197,7 @@ coreHelpers.asset = function (context, options) {
// if the author could not be determined.
//
coreHelpers.author = function (context, options) {
/*jslint unparam:true*/
/*jshint unused:false*/
return this.author ? this.author.name : '';
};
@ -320,7 +319,7 @@ coreHelpers.excerpt = function (options) {
//
// Returns the config value for fileStorage.
coreHelpers.file_storage = function (context, options) {
/*jslint unparam:true*/
/*jshint unused:false*/
if (config().hasOwnProperty('fileStorage')) {
return config().fileStorage.toString();
}
@ -345,7 +344,7 @@ coreHelpers.ghost_script_tags = function () {
*/
coreHelpers.body_class = function (options) {
/*jslint unparam:true*/
/*jshint unused:false*/
var classes = [],
post = this.post,
tags = this.post && this.post.tags ? this.post.tags : this.tags || [],
@ -391,7 +390,7 @@ coreHelpers.body_class = function (options) {
};
coreHelpers.post_class = function (options) {
/*jslint unparam:true*/
/*jshint unused:false*/
var classes = ['post'],
tags = this.post && this.post.tags ? this.post.tags : this.tags || [],
featured = this.post && this.post.featured ? this.post.featured : this.featured || false,
@ -416,7 +415,7 @@ coreHelpers.post_class = function (options) {
};
coreHelpers.ghost_head = function (options) {
/*jslint unparam:true*/
/*jshint unused:false*/
var self = this,
blog = config.theme(),
head = [],
@ -441,7 +440,7 @@ coreHelpers.ghost_head = function (options) {
};
coreHelpers.ghost_foot = function (options) {
/*jslint unparam:true*/
/*jshint unused:false*/
var foot = [];
foot.push(scriptTemplate({
@ -456,7 +455,7 @@ coreHelpers.ghost_foot = function (options) {
};
coreHelpers.meta_title = function (options) {
/*jslint unparam:true*/
/*jshint unused:false*/
var title = "",
blog;
@ -478,7 +477,7 @@ coreHelpers.meta_title = function (options) {
};
coreHelpers.meta_description = function (options) {
/*jslint unparam:true*/
/*jshint unused:false*/
var description,
blog;
@ -627,7 +626,7 @@ coreHelpers.has = function (options) {
// `{{pagination}}`
// Outputs previous and next buttons, along with info about the current page
coreHelpers.pagination = function (options) {
/*jslint unparam:true*/
/*jshint unused:false*/
if (!_.isObject(this.pagination) || _.isFunction(this.pagination)) {
errors.logAndThrowError('pagination data is not an object or is a function');
return;

View File

@ -247,7 +247,7 @@ function setup(server) {
if (getSocket()) {
// Make sure the socket is gone before trying to create another
fs.unlink(getSocket(), function (err) {
/*jslint unparam:true*/
/*jshint unused:false*/
server.listen(
getSocket(),
startGhost

View File

@ -1,5 +1,4 @@
var cp = require('child_process'),
url = require('url'),
_ = require('lodash'),
when = require('when'),
nodefn = require('when/node/function'),

View File

@ -10,7 +10,6 @@ var api = require('../api'),
fs = require('fs'),
hbs = require('express-hbs'),
middleware = require('./middleware'),
models = require('../models'),
packageInfo = require('../../../package.json'),
path = require('path'),
slashes = require('connect-slashes'),

View File

@ -1,7 +1,6 @@
var ghostBookshelf = require('./base'),
User = require('./user').User,
Role = require('./role').Role,
validation = require('../data/validation'),
Permission,
Permissions;

View File

@ -42,7 +42,7 @@ Post = ghostBookshelf.Model.extend({
},
saving: function (newPage, attr, options) {
/*jslint unparam:true*/
/*jshint unused:false*/
var self = this;
// keep tags for 'saved' event
@ -76,7 +76,7 @@ Post = ghostBookshelf.Model.extend({
},
creating: function (newPage, attr, options) {
/*jslint unparam:true*/
/*jshint unused:false*/
// set any dynamic default properties
if (!this.get('author_id')) {
@ -87,7 +87,7 @@ Post = ghostBookshelf.Model.extend({
},
updateTags: function (newPost, attr, options) {
/*jslint unparam:true*/
/*jshint unused:false*/
var self = this;
options = options || {};

View File

@ -10,8 +10,8 @@ Session = ghostBookshelf.Model.extend({
}, {
destroyAll: function (options) {
options = options || {};
return ghostBookshelf.Collection.forge([], {model: this}).fetch().
then(function (collection) {
return ghostBookshelf.Collection.forge([], {model: this}).fetch()
.then(function (collection) {
collection.invokeThen('destroy', options);
});
}

View File

@ -1,5 +1,4 @@
var _ = require('lodash'),
uuid = require('node-uuid'),
when = require('when'),
errors = require('../errorHandling'),
nodefn = require('when/node/function'),
@ -112,7 +111,7 @@ User = ghostBookshelf.Model.extend({
// Add this user to the admin role (assumes admin = role_id: 1)
return userData.roles().attach(1);
}).then(function (addedUserRole) {
/*jslint unparam:true*/
/*jshint unused:false*/
// Return the added user as expected
return when.resolve(userData);

View File

@ -1,5 +1,4 @@
var _ = require('lodash'),
moment = require('moment'),
var moment = require('moment'),
path = require('path'),
when = require('when'),
baseStore;

View File

@ -66,12 +66,12 @@
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-copy": "~0.4.1",
"grunt-contrib-handlebars": "~0.6.0",
"grunt-contrib-jshint": "~0.8.0",
"grunt-contrib-sass": "~0.5.0",
"grunt-contrib-uglify": "~0.2.5",
"grunt-contrib-watch": "~0.5.3",
"grunt-express-server": "~0.4.11",
"grunt-groc": "~0.4.0",
"grunt-jslint": "~1.1.1",
"grunt-mocha-cli": "~1.4.0",
"grunt-shell": "~0.6.1",
"grunt-update-submodules": "~0.2.1",