Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

github-release-notes

Package Overview
Dependencies
Maintainers
1
Versions
37
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

github-release-notes - npm Package Compare versions

Comparing version 0.7.2 to 0.8.0

139

CHANGELOG.md
# Changelog
## 0.6.3 (13/03/2017)
## v0.7.2 (17/03/2017)
- [**bug**] Fix multiple repo information [#48](https://github.com/github-tools/github-release-notes/issues/48)
#### Bug Fixes:
- [#59](https://github.com/github-tools/github-release-notes/issues/59) Changelog action doesn't work in the grunt task
---
---
## 0.6.2 (13/03/2017)
## v0.7.1 (16/03/2017)
- [**bug**] Remove unused option user.name [#45](https://github.com/github-tools/github-release-notes/issues/45)
#### Bug Fixes:
- [#57](https://github.com/github-tools/github-release-notes/issues/57) Fix object-deep-assign bug
---
---
## v0.7.0 (16/03/2017)
## 0.6.1 (12/03/2017)
#### Framework Enhancements:
- [**bug**] Error when there is only one tag [#43](https://github.com/github-tools/github-release-notes/issues/43)
- [**enhancement**] Use different files type for configuration [#39](https://github.com/github-tools/github-release-notes/issues/39)
- [#53](https://github.com/github-tools/github-release-notes/issues/53) Allow functions as template values
- [#50](https://github.com/github-tools/github-release-notes/issues/50) Add GIF in readme.md file
- [#27](https://github.com/github-tools/github-release-notes/issues/27) Add possibility to change date format
---
## v0.3.3 (14/03/2017)
*No changelog for this release.*
---
---
## 0.6.0 (11/03/2017)
## v0.5.0 (14/03/2017)
- [**enhancement**] Unwrap github-api promises [#32](https://github.com/github-tools/github-release-notes/issues/32)
- [**bug**] Remove escaping character on regex [#29](https://github.com/github-tools/github-release-notes/issues/29)
- [**enhancement**] Use external config file [#26](https://github.com/github-tools/github-release-notes/issues/26)
- [**bug**] The changelog action doesn't compile latest release [#24](https://github.com/github-tools/github-release-notes/issues/24)
- [**enhancement**] Introduce templates for the issues [#23](https://github.com/github-tools/github-release-notes/issues/23)
- [**enhancement**] Add an "ignore label" flag [#19](https://github.com/github-tools/github-release-notes/issues/19)
- [**enhancement**] Add the chance to rebuild the history of release notes [#12](https://github.com/github-tools/github-release-notes/issues/12)
#### Framework Enhancements:
- [#20](https://github.com/github-tools/github-release-notes/issues/20) Specify which tag to build
- [#18](https://github.com/github-tools/github-release-notes/issues/18) Create global version of the module
- [#16](https://github.com/github-tools/github-release-notes/issues/16) Update the documentation
- [#14](https://github.com/github-tools/github-release-notes/issues/14) Add the chance to override the latest release body
- [#13](https://github.com/github-tools/github-release-notes/issues/13) Check the network
- [#11](https://github.com/github-tools/github-release-notes/issues/11) Add tests
- [#10](https://github.com/github-tools/github-release-notes/issues/10) Use the issues as data source
- [#9](https://github.com/github-tools/github-release-notes/issues/9) Get the information from the local git config
- [#7](https://github.com/github-tools/github-release-notes/issues/7) Add the possibility to create a CHANGELOG file
#### Bug Fixes:
---
- [#15](https://github.com/github-tools/github-release-notes/issues/15) Manage the scenario where there is only one tag
## 0.5.0 (26/09/2016)
---
- [**enhancement**] Specify which tag to build [#20](https://github.com/github-tools/github-release-notes/issues/20)
- [**enhancement**] Create global version of the module [#18](https://github.com/github-tools/github-release-notes/issues/18)
- [**enhancement**] Update the documentation [#16](https://github.com/github-tools/github-release-notes/issues/16)
- [**bug**] Manage the scenario where there is only one tag [#15](https://github.com/github-tools/github-release-notes/issues/15)
- [**enhancement**] Add the chance to override the latest release body [#14](https://github.com/github-tools/github-release-notes/issues/14)
- [**enhancement**] Check the network [#13](https://github.com/github-tools/github-release-notes/issues/13)
- [**enhancement**] Add tests [#11](https://github.com/github-tools/github-release-notes/issues/11)
- [**enhancement**] Use the issues as data source [#10](https://github.com/github-tools/github-release-notes/issues/10)
- [**enhancement**] Get the information from the local git config [#9](https://github.com/github-tools/github-release-notes/issues/9)
- [**enhancement**] Add the possibility to create a CHANGELOG file [#7](https://github.com/github-tools/github-release-notes/issues/7)
## v0.6.1 (14/03/2017)
#### Framework Enhancements:
- [#39](https://github.com/github-tools/github-release-notes/issues/39) Use different files type for configuration
---
#### Bug Fixes:
## 0.4.0 (03/03/2016)
- [#43](https://github.com/github-tools/github-release-notes/issues/43) Error when there is only one tag
- [**enhancement**] Include various types of commit messages [#5](https://github.com/github-tools/github-release-notes/issues/5)
---
## v0.6.2 (14/03/2017)
#### Bug Fixes:
---
- [#45](https://github.com/github-tools/github-release-notes/issues/45) Remove unused option user.name
## 0.3.3 (26/01/2016)
---
*No changelog for this release.*
## v0.6.3 (14/03/2017)
#### Bug Fixes:
- [#48](https://github.com/github-tools/github-release-notes/issues/48) Fix multiple repo information
---
---
## 0.3.2 (07/12/2015)
## v0.4.0 (14/03/2017)
*No changelog for this release.*
#### Framework Enhancements:
- [#5](https://github.com/github-tools/github-release-notes/issues/5) Include various types of commit messages
---
---
## v0.6.0 (14/03/2017)
## 0.3.1 (07/12/2015)
#### Framework Enhancements:
*No changelog for this release.*
- [#32](https://github.com/github-tools/github-release-notes/issues/32) Unwrap github-api promises
- [#26](https://github.com/github-tools/github-release-notes/issues/26) Use external config file
- [#23](https://github.com/github-tools/github-release-notes/issues/23) Introduce templates for the issues
- [#19](https://github.com/github-tools/github-release-notes/issues/19) Add an "ignore label" flag
- [#12](https://github.com/github-tools/github-release-notes/issues/12) Add the chance to rebuild the history of release notes
#### Bug Fixes:
- [#29](https://github.com/github-tools/github-release-notes/issues/29) Remove escaping character on regex
- [#24](https://github.com/github-tools/github-release-notes/issues/24) The changelog action doesn't compile latest release
---
---
## 0.3.0 (19/11/2015)
## v0.2.2 (10/03/2017)
*No changelog for this release.*
---
## v0.3.0 (10/03/2017)
*No changelog for this release.*
---
---
## 0.2.2 (18/11/2015)
## v0.3.1 (10/03/2017)
*No changelog for this release.*
---
## v0.3.2 (10/03/2017)
*No changelog for this release.*
---
---
## 0.2.1 (18/11/2015)
## v0.2.1 (26/09/2016)
*No changelog for this release.*
---
## v0.2.0 (26/09/2016)
---
#### Framework Enhancements:
## 0.2.0 (15/11/2015)
- [#3](https://github.com/github-tools/github-release-notes/issues/3) Cleanse option
- [**enhancement**] Cleanse option [#3](https://github.com/github-tools/github-release-notes/issues/3)
---
---
## v0.1.0 (12/11/2015)
_No changelog for this release._
---
{
"name": "github-release-notes",
"version": "0.7.2",
"version": "0.8.0",
"description": "Node module to publish release notes based on commits between the last two tags.",

@@ -43,4 +43,5 @@ "main": "./github-release-notes.js",

"github-api": "^3.0.0",
"require-yaml": "0.0.1",
"object-assign-deep": "0.0.4"
"minimist": "^1.2.0",
"object-assign-deep": "0.0.4",
"require-yaml": "0.0.1"
},

@@ -47,0 +48,0 @@ "devDependencies": {

@@ -26,6 +26,8 @@ 'use strict';

dateZero: new Date(0),
generate: false,
override: false,
ignoreLabels: false, // || array of labels
ignoreIssuesWith: false, // || array of labels
template: templateConfig
template: templateConfig,
groupBy: false
};

@@ -238,24 +240,2 @@

*
* @param {Object[]} releases A list of release Objects
*
* @return {Array} The list of the dates
*/
function getReleaseDates(gren, releases) {
return [].concat(releases).map(function(release) {
return {
id: release.id,
name: release.name,
tag_name: release.tag_name,
date: release.created_at,
body: release.body || null
};
});
}
/**
* Get all releases
*
* @since 0.5.0
* @private
*
* @param {GithubReleaseNotes} gren The gren object

@@ -281,21 +261,4 @@ *

/**
* Get the latest releases
* Return the templated commit message
*
* @since 0.5.0
* @private
*
* @param {GithubReleaseNotes} gren The gren object
*
* @return {Promise} The promise which resolves the tag name of the release
*/
function getLastTwoReleases(gren) {
return getListReleases(gren)
.then(function(releases) {
return releases.slice(0, 2);
});
}
/**
* Return a string with a - to be a bulvar list (used for a mapping)
*
* @since 0.1.0

@@ -325,4 +288,4 @@ * @private

function templateLabels(gren, issue) {
if (!issue.labels.length) {
issue.labels.push({name: 'closed'});
if (!issue.labels.length && gren.options.template.noLabel) {
issue.labels.push({name: gren.options.template.noLabel});
}

@@ -342,19 +305,20 @@

/**
* Generate the MD template a block
* Generate the releases bodies from a release Objects Array
*
* @since 0.5.0
* @since 0.8.0
* @private
*
* @param {Object} block ({name: 'v1.2.3', body: []})
* @param {GithubReleaseNotes} gren
* @param {Array} releases The release Objects Array coming from GitHub
*
* @return {string}
*/
function templateBlock(gren, block) {
var date = new Date(block.date);
var releaseTemplate = template.generate({
release: block.name,
date: utils.formatDate(date)
}, gren.options.template.release);
return releaseTemplate + '\n\n' + block.body;
function templateReleases(gren, releases) {
return releases.map(function(release) {
return template.generate({
release: release.name,
date: utils.formatDate(new Date(release.published_at)),
body: release.body
}, gren.options.template.release);
}).join(gren.options.template.releaseSeparator);
}

@@ -382,3 +346,3 @@

/**
* Generate the Changelog MD template
* Generate the Changelog issues body template
*

@@ -392,21 +356,29 @@ * @since 0.5.0

*/
function templateChangelog(gren, blocks) {
return '# Changelog\n\n' +
blocks
.map(templateBlock.bind(null, gren))
.join('\n\n --- \n\n');
function templateIssueBody(body, rangeBody) {
return (body.length ? body.join('\n') : rangeBody || '*No changelog for this release.*') + '\n';
}
/**
* Generate the Changelog issues body template
* Generates the template for the groups
*
* @since 0.5.0
* @since 0.8.0
* @private
*
* @param {Object[]} blocks
* @param {GithubReleaseNotes} gren
* @param {Object} groups The groups to template e.g.
* {
* 'bugs': [{...}, {...}, {...}]
* }
*
* @return {string}
*/
function templateIssueBody(body, rangeBody) {
return (body.length ? body.join('\n') : rangeBody || '*No changelog for this release.*') + '\n';
function templateGroups(gren, groups) {
return Object.keys(groups).map(function(group) {
var heading = template.generate({
heading: group
}, gren.options.template.group);
var body = groups[group].join('\n');
return heading + '\n' + body;
});
}

@@ -511,14 +483,14 @@

releaseRanges
.map(function(range) {
return getCommitsBetweenTwo(gren, range[1].date, range[0].date)
.then(function(commits) {
return {
id: range[0].id,
name: gren.options.prefix + range[0].name,
release: range[0].name,
date: range[0].date,
body: generateCommitsBody(gren, commits) + '\n'
};
});
})
.map(function(range) {
return getCommitsBetweenTwo(gren, range[1].date, range[0].date)
.then(function(commits) {
return {
id: range[0].id,
name: gren.options.prefix + range[0].name,
release: range[0].name,
published_at: range[0].date,
body: generateCommitsBody(gren, commits) + '\n'
};
});
})
);

@@ -579,2 +551,86 @@ }

/**
* Group the issues based on their first label
*
* @since 0.8.0
* @private
*
* @param {GithubReleaseNotes} gren
* @param {Array} issues
*
* @return {string}
*/
function groupByLabel(gren, issues) {
var groups = [];
issues.forEach(function(issue) {
if (!issue.labels.length && gren.options.template.noLabel) {
issue.labels.push({name: gren.options.template.noLabel});
}
var labelName = issue.labels[0].name;
if (!groups[labelName]) {
groups[labelName] = [];
}
groups[labelName].push(templateIssue(gren, issue));
});
return templateGroups(gren, utils.sortObject(groups));
}
/**
* Create groups of issues based on labels
*
* @since 0.8.0
* @private
*
* @param {GithubReleaseNotes} gren
* @param {Array} issues The array of all the issues.
*
* @return {Array}
*/
function groupBy(gren, issues) {
var groupBy = gren.options.groupBy;
if (!groupBy) {
return issues.map(templateIssue.bind(null, gren));
}
if (groupBy === 'label') {
return groupByLabel(gren, issues);
}
if (typeof groupBy !== 'object') {
throw chalk.red('The option for groupBy is invalid, please check the documentation');
}
var allLabels = Object.keys(groupBy).reduce(function(carry, group) {
return carry.concat(groupBy[group]);
}, []);
var groups = Object.keys(groupBy).reduce(function(carry, group) {
var groupIssues = issues.filter(function(issue) {
if (!issue.labels.length && gren.options.template.noLabel) {
issue.labels.push({name: gren.options.template.noLabel});
}
return issue.labels.some(function(label) {
var isOtherLabel = groupBy[group].indexOf('...') !== -1 && allLabels.indexOf(label.name) === -1;
return groupBy[group].indexOf(label.name) !== -1 || isOtherLabel;
});
}).map(templateIssue.bind(null, gren));
if (groupIssues.length) {
carry[group] = groupIssues;
}
return carry;
}, {});
return templateGroups(gren, groups);
}
/**
* Get the blocks of issues based on release dates

@@ -597,12 +653,12 @@ *

.map(function(range) {
var body = (!range[0].body || gren.options.override) &&
issues.filter(function(issue) {
return utils.isInRange(
Date.parse(issue.closed_at),
Date.parse(range[1].date),
Date.parse(range[0].date)
);
})
.map(templateIssue.bind(null, gren));
var filteredIssues = issues.filter(function(issue) {
return utils.isInRange(
Date.parse(issue.closed_at),
Date.parse(range[1].date),
Date.parse(range[0].date)
);
});
var body = (!range[0].body || gren.options.override) && groupBy(gren, filteredIssues);
return {

@@ -612,3 +668,3 @@ id: range[0].id,

name: gren.options.prefix + range[0].name,
date: range[0].date,
published_at: range[0].date,
body: templateIssueBody(body, range[0].body)

@@ -667,14 +723,11 @@ };

/**
* Generate a CHANGELOG.md file based on Time and issues
* Generate release blocks based on issues or commit messages
* depending on the option.
*
* @since 0.5.0
* @private
* @param {GithubReleaseNotes} gren
*
* @return {Promise[]}
* @return {Promise} Resolving the release blocks
*/
function generateReleaseDatesChangelogBody(gren) {
var releaseActions = {
history: getListReleases,
latest: getLastTwoReleases
};
function getReleaseBlocks(gren) {
var loaded;
var dataSource = {

@@ -685,14 +738,18 @@ issues: getIssueBlocks,

return releaseActions[gren.options.timeWrap](gren)
return getListReleases(gren)
.then(function(releases) {
if (releases.length === 0) {
throw chalk.red('There are no releases! Run gren to generate release notes');
}
return getLastTags(gren, releases.length ? releases : false);
})
.then(function(tags) {
loaded = utils.task(gren, 'Getting the tag dates ranges');
var releaseRanges = createReleaseRanges(gren, getReleaseDates(gren, releases));
return Promise.all(getTagDates(gren, tags));
})
.then(function(releaseDates) {
loaded();
return dataSource[gren.options.dataSource](gren, releaseRanges);
})
.then(function(blocks) {
return templateChangelog(gren, blocks);
return dataSource[gren.options.dataSource](
gren,
createReleaseRanges(gren, releaseDates)
);
});

@@ -702,40 +759,36 @@ }

/**
* Create the CHANGELOG.md file
* Check if the changelog file exists
*
* @since 0.5.0
* @since 0.8.0
* @private
*
* @param {string} body
* @param {GithubReleaseNotes} gren
*
* @return {boolean}
* @return {Promise}
*/
function createChangelog(gren, body) {
function checkChangelogFile(gren) {
var filePath = process.cwd() + '/' + gren.options.changelogFilename;
function createFile(fileBody) {
fs.writeFileSync(filePath, fileBody);
process.stdout.write('\n' + chalk.green('The changelog file has been saved!\n'));
return Promise.resolve();
if (fs.existsSync(filePath) && !gren.options.override) {
Promise.reject(chalk.red('Looks like there is already a changelog, to override it use --override'));
}
if (!fs.existsSync(filePath)) {
return createFile(body);
}
return Promise.resolve();
}
var data = fs.readFileSync(filePath, 'utf-8');
var newReleaseName = body.match(/(##\s[\w\s.]+)/)[0];
/**
* Create the changelog file
*
* @since 0.8.0
* @private
*
* @param {GithubReleaseNotes} gren
* @param {string} body The body of the file
*/
function createChangelog(gren, body) {
var filePath = process.cwd() + '/' + gren.options.changelogFilename;
if (data.match(newReleaseName)) {
if (gren.options.force) {
return createFile(body + '\n\n --- \n\n' + data.replace(/^(#\s?\w*\n\n)/g, ''));
} else if (gren.options.override) {
return createFile(body);
}
fs.writeFileSync(filePath, gren.options.template.changelogTitle + body);
return Promise.reject('This release is already in the changelog\n');
}
return createFile(body + '\n --- \n' + data.replace(/^(#\s?\w*\n\n)/g, ''));
console.log(chalk.green('\nChangelog created!'));
}

@@ -843,26 +896,5 @@

var loaded;
var gren = this;
var dataSource = {
issues: getIssueBlocks,
commits: getCommitBlocks
};
return getListReleases(this)
.then(function(releases) {
return getLastTags(gren, releases.length ? releases : false);
})
.then(function(tags) {
loaded = utils.task(gren, 'Getting the tag dates ranges');
return Promise.all(getTagDates(gren, tags));
})
.then(function(releaseDates) {
loaded();
return dataSource[gren.options.dataSource](
gren,
createReleaseRanges(gren, releaseDates)
);
})
return getReleaseBlocks(this)
.then(function(blocks) {

@@ -876,5 +908,7 @@ return blocks.reduce(function(carry, block) {

/**
* Generate the Changelog
* Generate the Changelog based on the github releases, or
* from fresh generated releases.
*
* @since 0.5.0
*
* @public

@@ -889,5 +923,19 @@ *

return generateReleaseDatesChangelogBody(this)
.then(function(changelogBody) {
return createChangelog(gren, changelogBody);
return checkChangelogFile(this)
.then(function() {
if (gren.options.generate) {
return getReleaseBlocks(gren);
}
return getListReleases(gren);
})
.then(function(releases) {
if (releases.length === 0) {
throw chalk.red('There are no releases, use --generate to create release notes, or run the release command.');
}
return Promise.resolve(releases);
})
.then(function(releases) {
createChangelog(gren, templateReleases(gren, releases));
});

@@ -894,0 +942,0 @@ };

@@ -5,3 +5,7 @@ {

"label": "[**{{label}}**]",
"release": "## {{release}} {{date}}"
"noLabel": "closed",
"group": "\n#### {{heading}}\n",
"changelogTitle": "# Changelog\n\n",
"release": "## {{release}} ({{date}})\n{{body}}",
"releaseSeparator": "\n---\n\n"
}

@@ -5,5 +5,25 @@ 'use strict';

var fs = require('fs');
var minimist = require('minimist');
require('require-yaml');
/**
* Sort an object by its keys
*
* @since 0.8.0
* @public
*
* @param {Object} object
* @return {Object}
*/
function sortObject(object) {
return Object.keys(object)
.sort()
.reduce(function(result, key) {
result[key] = object[key];
return result;
}, {});
}
/**
* Print a task name in a custom format

@@ -115,13 +135,3 @@ *

function getBashOptions(args) {
var settings = {};
for (var i = 2; i < args.length; i++) {
var paramArray = args[i].split('=');
var key = paramArray[0].replace('--', '');
var value = paramArray[1];
settings[dashToCamelCase(key)] = value || true;
}
return settings;
return minimist(args.slice(2));
}

@@ -218,2 +228,3 @@

module.exports = {
sortObject: sortObject,
printTask: printTask,

@@ -227,3 +238,4 @@ task: task,

formatDate: formatDate,
getConfigFromFile: getConfigFromFile
getConfigFromFile: getConfigFromFile,
noop: function() {}
};

@@ -6,39 +6,40 @@ 'use strict';

exports['utils'] = {
'Should return the string of the formatted date': function (test) {
test.expect(1);
'Should return the string of the formatted date': function (test) {
test.expect(1);
test.deepEqual(utils.formatDate(new Date(0)), '01/01/1970', 'Given a date object.');
test.done();
},
'Should return the options in a key/value format': function (test) {
test.expect(1);
test.deepEqual(utils.formatDate(new Date(0)), '01/01/1970', 'Given a date object.');
test.done();
},
'Should return the options in a key/value format': function (test) {
test.expect(1);
let bashOptions = utils.getBashOptions([null, null, '--key=value', '--key2=value2']);
let bashOptions = utils.getBashOptions([null, null, '--key=value', '--key2=value2']);
test.deepEqual(JSON.stringify(bashOptions), JSON.stringify({
key: 'value',
key2: 'value2'
}), 'Given an array of node arguments.');
test.done();
},
'Should return a camelCase string': function (test) {
test.expect(2);
test.deepEqual(JSON.stringify(bashOptions), JSON.stringify({
_: [],
key: 'value',
key2: 'value2'
}), 'Given an array of node arguments.');
test.done();
},
'Should return a camelCase string': function (test) {
test.expect(2);
test.deepEqual(utils.dashToCamelCase('this-is-a-string'), 'thisIsAString', 'Given a string with dashes.');
test.deepEqual(utils.dashToCamelCase('tHIs-Is-a-sTriNg'), 'thisIsAString', 'Given a string with random capital letters');
test.done();
},
'Should return if a number is in between a range': function (test) {
test.expect(7);
test.deepEqual(utils.dashToCamelCase('this-is-a-string'), 'thisIsAString', 'Given a string with dashes.');
test.deepEqual(utils.dashToCamelCase('tHIs-Is-a-sTriNg'), 'thisIsAString', 'Given a string with random capital letters');
test.done();
},
'Should return if a number is in between a range': function (test) {
test.expect(7);
test.deepEqual(utils.isInRange(2, 1, 3), true, 'Given a number in range');
test.deepEqual(utils.isInRange(1, 2, 3), false, 'Given a number below range');
test.deepEqual(utils.isInRange(4, 1, 3), false, 'Given a number above range');
test.deepEqual(utils.isInRange(-1, 1, 3), false, 'Given a number above range, negative');
test.deepEqual(utils.isInRange(-1, -3, 0), true, 'Given a number in range, negative');
test.deepEqual(utils.isInRange(2, 2, 5), true, 'Given same number as first range value');
test.deepEqual(utils.isInRange(5, 2, 5), false, 'Given same number as last range value');
test.deepEqual(utils.isInRange(2, 1, 3), true, 'Given a number in range');
test.deepEqual(utils.isInRange(1, 2, 3), false, 'Given a number below range');
test.deepEqual(utils.isInRange(4, 1, 3), false, 'Given a number above range');
test.deepEqual(utils.isInRange(-1, 1, 3), false, 'Given a number above range, negative');
test.deepEqual(utils.isInRange(-1, -3, 0), true, 'Given a number in range, negative');
test.deepEqual(utils.isInRange(2, 2, 5), true, 'Given same number as first range value');
test.deepEqual(utils.isInRange(5, 2, 5), false, 'Given same number as last range value');
test.done();
}
test.done();
}
};
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc