Socket
Socket
Sign inDemoInstall

eslint

Package Overview
Dependencies
89
Maintainers
4
Versions
356
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 9.0.0-alpha.2 to 9.0.0-beta.0

26

lib/api.js

@@ -12,3 +12,4 @@ /**

const { ESLint } = require("./eslint/eslint");
const { ESLint, shouldUseFlatConfig } = require("./eslint/eslint");
const { LegacyESLint } = require("./eslint/legacy-eslint");
const { Linter } = require("./linter");

@@ -19,2 +20,24 @@ const { RuleTester } = require("./rule-tester");

//-----------------------------------------------------------------------------
// Functions
//-----------------------------------------------------------------------------
/**
* Loads the correct ESLint constructor given the options.
* @param {Object} [options] The options object
* @param {boolean} [options.useFlatConfig] Whether or not to use a flat config
* @returns {Promise<ESLint|LegacyESLint>} The ESLint constructor
*/
async function loadESLint({ useFlatConfig } = {}) {
/*
* Note: The v8.x version of this function also accepted a `cwd` option, but
* it is not used in this implementation so we silently ignore it.
*/
const shouldESLintUseFlatConfig = useFlatConfig ?? (await shouldUseFlatConfig());
return shouldESLintUseFlatConfig ? ESLint : LegacyESLint;
}
//-----------------------------------------------------------------------------
// Exports

@@ -25,2 +48,3 @@ //-----------------------------------------------------------------------------

Linter,
loadESLint,
ESLint,

@@ -27,0 +51,0 @@ RuleTester,

@@ -569,2 +569,8 @@ /**

/**
* The type of configuration used by this class.
* @type {string}
*/
static configType = "flat";
/**
* Creates a new instance of the main ESLint API.

@@ -571,0 +577,0 @@ * @param {ESLintOptions} options The options for this instance.

@@ -442,2 +442,8 @@ /**

/**
* The type of configuration used by this class.
* @type {string}
*/
static configType = "eslintrc";
/**
* Creates a new instance of the main ESLint API.

@@ -444,0 +450,0 @@ * @param {LegacyESLintOptions} options The options for this instance.

2

lib/linter/index.js
"use strict";
const { Linter } = require("./linter");
const interpolate = require("./interpolate");
const { interpolate } = require("./interpolate");
const SourceCodeFixer = require("./source-code-fixer");

@@ -6,0 +6,0 @@

@@ -12,3 +12,18 @@ /**

module.exports = (text, data) => {
/**
* Returns a global expression matching placeholders in messages.
* @returns {RegExp} Global regular expression matching placeholders
*/
function getPlaceholderMatcher() {
return /\{\{([^{}]+?)\}\}/gu;
}
/**
* Replaces {{ placeholders }} in the message with the provided data.
* Does not replace placeholders not available in the data.
* @param {string} text Original message with potential placeholders
* @param {Record<string, string>} data Map of placeholder name to its value
* @returns {string} Message with replaced placeholders
*/
function interpolate(text, data) {
if (!data) {

@@ -18,4 +33,6 @@ return text;

const matcher = getPlaceholderMatcher();
// Substitution content for any {{ }} markers.
return text.replace(/\{\{([^{}]+?)\}\}/gu, (fullMatch, termWithWhitespace) => {
return text.replace(matcher, (fullMatch, termWithWhitespace) => {
const term = termWithWhitespace.trim();

@@ -30,2 +47,7 @@

});
}
module.exports = {
getPlaceholderMatcher,
interpolate
};

@@ -14,3 +14,3 @@ /**

const ruleFixer = require("./rule-fixer");
const interpolate = require("./interpolate");
const { interpolate } = require("./interpolate");

@@ -17,0 +17,0 @@ //------------------------------------------------------------------------------

@@ -20,3 +20,4 @@ /**

{ getRuleOptionsSchema } = require("../config/flat-config-helpers"),
{ Linter, SourceCodeFixer, interpolate } = require("../linter"),
{ Linter, SourceCodeFixer } = require("../linter"),
{ interpolate, getPlaceholderMatcher } = require("../linter/interpolate"),
stringify = require("json-stable-stringify-without-jsonify");

@@ -308,2 +309,35 @@

/**
* Extracts names of {{ placeholders }} from the reported message.
* @param {string} message Reported message
* @returns {string[]} Array of placeholder names
*/
function getMessagePlaceholders(message) {
const matcher = getPlaceholderMatcher();
return Array.from(message.matchAll(matcher), ([, name]) => name.trim());
}
/**
* Returns the placeholders in the reported messages but
* only includes the placeholders available in the raw message and not in the provided data.
* @param {string} message The reported message
* @param {string} raw The raw message specified in the rule meta.messages
* @param {undefined|Record<unknown, unknown>} data The passed
* @returns {string[]} Missing placeholder names
*/
function getUnsubstitutedMessagePlaceholders(message, raw, data = {}) {
const unsubstituted = getMessagePlaceholders(message);
if (unsubstituted.length === 0) {
return [];
}
// Remove false positives by only counting placeholders in the raw message, which were not provided in the data matcher or added with a data property
const known = getMessagePlaceholders(raw);
const provided = Object.keys(data);
return unsubstituted.filter(name => known.includes(name) && !provided.includes(name));
}
const metaSchemaDescription = `

@@ -650,3 +684,7 @@ \t- If the rule has options, set \`meta.schema\` to an array or non-empty object to enable options validation.

if (item.filename) {
if (hasOwnProperty(item, "only")) {
assert.ok(typeof item.only === "boolean", "Optional test case property 'only' must be a boolean");
}
if (hasOwnProperty(item, "filename")) {
assert.ok(typeof item.filename === "string", "Optional test case property 'filename' must be a string");
filename = item.filename;

@@ -966,2 +1004,3 @@ }

assertMessageMatches(message.message, error);
assert.ok(message.suggestions === void 0, `Error at index ${i} has suggestions. Please convert the test error into an object and specify 'suggestions' property on it to test suggestions.`);
} else if (typeof error === "object" && error !== null) {

@@ -999,2 +1038,14 @@

);
const unsubstitutedPlaceholders = getUnsubstitutedMessagePlaceholders(
message.message,
rule.meta.messages[message.messageId],
error.data
);
assert.ok(
unsubstitutedPlaceholders.length === 0,
`The reported message has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(", ")}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? "values" : "value"} via the 'data' property in the context.report() call.`
);
if (hasOwnProperty(error, "data")) {

@@ -1016,9 +1067,6 @@

}
} else {
assert.fail("Test error must specify either a 'messageId' or 'message'.");
}
assert.ok(
hasOwnProperty(error, "data") ? hasOwnProperty(error, "messageId") : true,
"Error must specify 'messageId' if 'data' is used."
);
if (error.type) {

@@ -1044,72 +1092,94 @@ assert.strictEqual(message.nodeType, error.type, `Error type should be ${error.type}, found ${message.nodeType}`);

assert.ok(!message.suggestions || hasOwnProperty(error, "suggestions"), `Error at index ${i} has suggestions. Please specify 'suggestions' property on the test error object.`);
if (hasOwnProperty(error, "suggestions")) {
// Support asserting there are no suggestions
if (!error.suggestions || (Array.isArray(error.suggestions) && error.suggestions.length === 0)) {
if (Array.isArray(message.suggestions) && message.suggestions.length > 0) {
assert.fail(`Error should have no suggestions on error with message: "${message.message}"`);
}
} else {
assert.strictEqual(Array.isArray(message.suggestions), true, `Error should have an array of suggestions. Instead received "${message.suggestions}" on error with message: "${message.message}"`);
assert.strictEqual(message.suggestions.length, error.suggestions.length, `Error should have ${error.suggestions.length} suggestions. Instead found ${message.suggestions.length} suggestions`);
const expectsSuggestions = Array.isArray(error.suggestions) ? error.suggestions.length > 0 : Boolean(error.suggestions);
const hasSuggestions = message.suggestions !== void 0;
error.suggestions.forEach((expectedSuggestion, index) => {
assert.ok(
typeof expectedSuggestion === "object" && expectedSuggestion !== null,
"Test suggestion in 'suggestions' array must be an object."
);
Object.keys(expectedSuggestion).forEach(propertyName => {
assert.ok(
suggestionObjectParameters.has(propertyName),
`Invalid suggestion property name '${propertyName}'. Expected one of ${friendlySuggestionObjectParameterList}.`
);
});
if (!hasSuggestions && expectsSuggestions) {
assert.ok(!error.suggestions, `Error should have suggestions on error with message: "${message.message}"`);
} else if (hasSuggestions) {
assert.ok(expectsSuggestions, `Error should have no suggestions on error with message: "${message.message}"`);
if (typeof error.suggestions === "number") {
assert.strictEqual(message.suggestions.length, error.suggestions, `Error should have ${error.suggestions} suggestions. Instead found ${message.suggestions.length} suggestions`);
} else if (Array.isArray(error.suggestions)) {
assert.strictEqual(message.suggestions.length, error.suggestions.length, `Error should have ${error.suggestions.length} suggestions. Instead found ${message.suggestions.length} suggestions`);
const actualSuggestion = message.suggestions[index];
const suggestionPrefix = `Error Suggestion at index ${index} :`;
if (hasOwnProperty(expectedSuggestion, "desc")) {
error.suggestions.forEach((expectedSuggestion, index) => {
assert.ok(
!hasOwnProperty(expectedSuggestion, "data"),
`${suggestionPrefix} Test should not specify both 'desc' and 'data'.`
typeof expectedSuggestion === "object" && expectedSuggestion !== null,
"Test suggestion in 'suggestions' array must be an object."
);
assert.strictEqual(
actualSuggestion.desc,
expectedSuggestion.desc,
`${suggestionPrefix} desc should be "${expectedSuggestion.desc}" but got "${actualSuggestion.desc}" instead.`
);
}
Object.keys(expectedSuggestion).forEach(propertyName => {
assert.ok(
suggestionObjectParameters.has(propertyName),
`Invalid suggestion property name '${propertyName}'. Expected one of ${friendlySuggestionObjectParameterList}.`
);
});
if (hasOwnProperty(expectedSuggestion, "messageId")) {
assert.ok(
ruleHasMetaMessages,
`${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.`
);
assert.ok(
hasOwnProperty(rule.meta.messages, expectedSuggestion.messageId),
`${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.`
);
assert.strictEqual(
actualSuggestion.messageId,
expectedSuggestion.messageId,
`${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.`
);
if (hasOwnProperty(expectedSuggestion, "data")) {
const unformattedMetaMessage = rule.meta.messages[expectedSuggestion.messageId];
const rehydratedDesc = interpolate(unformattedMetaMessage, expectedSuggestion.data);
const actualSuggestion = message.suggestions[index];
const suggestionPrefix = `Error Suggestion at index ${index}:`;
if (hasOwnProperty(expectedSuggestion, "desc")) {
assert.ok(
!hasOwnProperty(expectedSuggestion, "data"),
`${suggestionPrefix} Test should not specify both 'desc' and 'data'.`
);
assert.ok(
!hasOwnProperty(expectedSuggestion, "messageId"),
`${suggestionPrefix} Test should not specify both 'desc' and 'messageId'.`
);
assert.strictEqual(
actualSuggestion.desc,
rehydratedDesc,
`${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".`
expectedSuggestion.desc,
`${suggestionPrefix} desc should be "${expectedSuggestion.desc}" but got "${actualSuggestion.desc}" instead.`
);
} else if (hasOwnProperty(expectedSuggestion, "messageId")) {
assert.ok(
ruleHasMetaMessages,
`${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.`
);
assert.ok(
hasOwnProperty(rule.meta.messages, expectedSuggestion.messageId),
`${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.`
);
assert.strictEqual(
actualSuggestion.messageId,
expectedSuggestion.messageId,
`${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.`
);
const unsubstitutedPlaceholders = getUnsubstitutedMessagePlaceholders(
actualSuggestion.desc,
rule.meta.messages[expectedSuggestion.messageId],
expectedSuggestion.data
);
assert.ok(
unsubstitutedPlaceholders.length === 0,
`The message of the suggestion has ${unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map(name => `'${name}'`).join(", ")}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`}. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? "values" : "value"} via the 'data' property for the suggestion in the context.report() call.`
);
if (hasOwnProperty(expectedSuggestion, "data")) {
const unformattedMetaMessage = rule.meta.messages[expectedSuggestion.messageId];
const rehydratedDesc = interpolate(unformattedMetaMessage, expectedSuggestion.data);
assert.strictEqual(
actualSuggestion.desc,
rehydratedDesc,
`${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".`
);
}
} else if (hasOwnProperty(expectedSuggestion, "data")) {
assert.fail(
`${suggestionPrefix} Test must specify 'messageId' if 'data' is used.`
);
} else {
assert.fail(
`${suggestionPrefix} Test must specify either 'messageId' or 'desc'.`
);
}
} else {
assert.ok(
!hasOwnProperty(expectedSuggestion, "data"),
`${suggestionPrefix} Test must specify 'messageId' if 'data' is used.`
);
}
if (hasOwnProperty(expectedSuggestion, "output")) {
assert.ok(hasOwnProperty(expectedSuggestion, "output"), `${suggestionPrefix} The "output" property is required.`);
const codeWithAppliedSuggestion = SourceCodeFixer.applyFixes(item.code, [actualSuggestion]).output;

@@ -1119,3 +1189,3 @@

const errorMessageInSuggestion =
linter.verify(codeWithAppliedSuggestion, result.configs, result.filename).find(m => m.fatal);
linter.verify(codeWithAppliedSuggestion, result.configs, result.filename).find(m => m.fatal);

@@ -1130,4 +1200,7 @@ assert(!errorMessageInSuggestion, [

assert.strictEqual(codeWithAppliedSuggestion, expectedSuggestion.output, `Expected the applied suggestion fix to match the test suggestion output for suggestion at index: ${index} on error with message: "${message.message}"`);
}
});
assert.notStrictEqual(expectedSuggestion.output, item.code, `The output of a suggestion should differ from the original source code for suggestion at index: ${index} on error with message: "${message.message}"`);
});
} else {
assert.fail("Test error object property 'suggestions' should be an array or a number");
}
}

@@ -1152,2 +1225,3 @@ }

assert.strictEqual(result.output, item.output, "Output is incorrect.");
assert.notStrictEqual(item.code, item.output, "Test property 'output' matches 'code'. If no autofix is expected, then omit the 'output' property or set it to null.");
}

@@ -1154,0 +1228,0 @@ } else {

@@ -95,3 +95,3 @@ /**

ignoreRestSiblings: false,
caughtErrors: "none"
caughtErrors: "all"
};

@@ -98,0 +98,0 @@

@@ -104,3 +104,3 @@ /**

type: "boolean",
default: false
default: true
}

@@ -118,3 +118,3 @@ },

const sourceCode = context.sourceCode;
const enforceForClassMembers = context.options[0] && context.options[0].enforceForClassMembers;
const enforceForClassMembers = context.options[0]?.enforceForClassMembers ?? true;

@@ -121,0 +121,0 @@ /**

@@ -24,5 +24,13 @@ /**

function isNaNIdentifier(node) {
return Boolean(node) && (
astUtils.isSpecificId(node, "NaN") ||
astUtils.isSpecificMemberAccess(node, "Number", "NaN")
if (!node) {
return false;
}
const nodeToCheck = node.type === "SequenceExpression"
? node.expressions.at(-1)
: node;
return (
astUtils.isSpecificId(nodeToCheck, "NaN") ||
astUtils.isSpecificMemberAccess(nodeToCheck, "Number", "NaN")
);

@@ -38,2 +46,3 @@ }

meta: {
hasSuggestions: true,
type: "problem",

@@ -68,3 +77,5 @@

caseNaN: "'case NaN' can never match. Use Number.isNaN before the switch.",
indexOfNaN: "Array prototype method '{{ methodName }}' cannot find NaN."
indexOfNaN: "Array prototype method '{{ methodName }}' cannot find NaN.",
replaceWithIsNaN: "Replace with Number.isNaN.",
replaceWithCastingAndIsNaN: "Replace with Number.isNaN cast to a Number."
}

@@ -77,4 +88,33 @@ },

const enforceForIndexOf = context.options[0] && context.options[0].enforceForIndexOf;
const sourceCode = context.sourceCode;
const fixableOperators = new Set(["==", "===", "!=", "!=="]);
const castableOperators = new Set(["==", "!="]);
/**
* Get a fixer for a binary expression that compares to NaN.
* @param {ASTNode} node The node to fix.
* @param {function(string): string} wrapValue A function that wraps the compared value with a fix.
* @returns {function(Fixer): Fix} The fixer function.
*/
function getBinaryExpressionFixer(node, wrapValue) {
return fixer => {
const comparedValue = isNaNIdentifier(node.left) ? node.right : node.left;
const shouldWrap = comparedValue.type === "SequenceExpression";
const shouldNegate = node.operator[0] === "!";
const negation = shouldNegate ? "!" : "";
let comparedValueText = sourceCode.getText(comparedValue);
if (shouldWrap) {
comparedValueText = `(${comparedValueText})`;
}
const fixedValue = wrapValue(comparedValueText);
return fixer.replaceText(node, `${negation}${fixedValue}`);
};
}
/**
* Checks the given `BinaryExpression` node for `foo === NaN` and other comparisons.

@@ -89,3 +129,28 @@ * @param {ASTNode} node The node to check.

) {
context.report({ node, messageId: "comparisonWithNaN" });
const suggestedFixes = [];
const NaNNode = isNaNIdentifier(node.left) ? node.left : node.right;
const isSequenceExpression = NaNNode.type === "SequenceExpression";
const isFixable = fixableOperators.has(node.operator) && !isSequenceExpression;
const isCastable = castableOperators.has(node.operator);
if (isFixable) {
suggestedFixes.push({
messageId: "replaceWithIsNaN",
fix: getBinaryExpressionFixer(node, value => `Number.isNaN(${value})`)
});
if (isCastable) {
suggestedFixes.push({
messageId: "replaceWithCastingAndIsNaN",
fix: getBinaryExpressionFixer(node, value => `Number.isNaN(Number(${value}))`)
});
}
}
context.report({
node,
messageId: "comparisonWithNaN",
suggest: suggestedFixes
});
}

@@ -92,0 +157,0 @@ }

{
"name": "eslint",
"version": "9.0.0-alpha.2",
"version": "9.0.0-beta.0",
"author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>",

@@ -68,4 +68,4 @@ "description": "An AST-based pattern checker for JavaScript.",

"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^3.0.0",
"@eslint/js": "9.0.0-alpha.2",
"@eslint/eslintrc": "^3.0.1",
"@eslint/js": "9.0.0-beta.0",
"@humanwhocodes/config-array": "^0.11.14",

@@ -80,8 +80,8 @@ "@humanwhocodes/module-importer": "^1.0.1",

"eslint-scope": "^8.0.0",
"eslint-visitor-keys": "^3.4.3",
"espree": "^10.0.0",
"eslint-visitor-keys": "^4.0.0",
"espree": "^10.0.1",
"esquery": "^1.4.2",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^6.0.1",
"file-entry-cache": "^8.0.0",
"find-up": "^5.0.0",

@@ -141,3 +141,3 @@ "glob-parent": "^6.0.2",

"markdownlint": "^0.33.0",
"markdownlint-cli": "^0.38.0",
"markdownlint-cli": "^0.39.0",
"marked": "^4.0.8",

@@ -161,3 +161,3 @@ "memfs": "^3.0.1",

"semver": "^7.5.3",
"shelljs": "^0.8.2",
"shelljs": "^0.8.5",
"sinon": "^11.0.0",

@@ -164,0 +164,0 @@ "vite-plugin-commonjs": "^0.10.0",

@@ -258,7 +258,2 @@ [![npm version](https://img.shields.io/npm/v/eslint.svg)](https://www.npmjs.com/package/eslint)

</td><td align="center" valign="top" width="11%">
<a href="https://github.com/ota-meshi">
<img src="https://github.com/ota-meshi.png?s=75" width="75" height="75" alt="Yosuke Ota's Avatar"><br />
Yosuke Ota
</a>
</td><td align="center" valign="top" width="11%">
<a href="https://github.com/Tanujkanti4441">

@@ -303,3 +298,3 @@ <img src="https://github.com/Tanujkanti4441.png?s=75" width="75" height="75" alt="Tanuj Kanti's Avatar"><br />

<p><a href="https://www.jetbrains.com/"><img src="https://images.opencollective.com/jetbrains/eb04ddc/logo.png" alt="JetBrains" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301?v=4" alt="American Express" height="64"></a> <a href="https://www.workleap.com"><img src="https://avatars.githubusercontent.com/u/53535748?u=d1e55d7661d724bf2281c1bfd33cb8f99fe2465f&v=4" alt="Workleap" height="64"></a></p><h3>Bronze Sponsors</h3>
<p><a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/d5592fe/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://transloadit.com/"><img src="https://avatars.githubusercontent.com/u/125754?v=4" alt="Transloadit" height="32"></a> <a href="https://www.ignitionapp.com"><img src="https://avatars.githubusercontent.com/u/5753491?v=4" alt="Ignition" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104?v=4" alt="Nx" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774?v=4" alt="HeroCoders" height="32"></a></p>
<p><a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/d5592fe/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://transloadit.com/"><img src="https://avatars.githubusercontent.com/u/125754?v=4" alt="Transloadit" height="32"></a> <a href="https://www.ignitionapp.com"><img src="https://avatars.githubusercontent.com/u/5753491?v=4" alt="Ignition" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104?v=4" alt="Nx" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774?v=4" alt="HeroCoders" height="32"></a> <a href="https://usenextbase.com"><img src="https://avatars.githubusercontent.com/u/145838380?v=4" alt="Nextbase Starter Kit" height="32"></a></p>
<!--sponsorsend-->

@@ -306,0 +301,0 @@

SocketSocket SOC 2 Logo

Product

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

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc