New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@netflix/x-element

Package Overview
Dependencies
Maintainers
10
Versions
28
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@netflix/x-element - npm Package Compare versions

Comparing version 1.0.0-rc.55 to 1.0.0-rc.56

129

demo/performance/index.js

@@ -35,64 +35,89 @@ import XElement from '../../x-element.js';

};
const tick = async () => {
await new Promise(resolve => requestAnimationFrame(resolve));
};
const run = async (output, constructor) => {
const run = async (output, constructor, ...tests) => {
output.textContent = '';
const { render, html } = constructor.templateEngine;
const injectCount = 100000;
const initialCount = 1000000;
const updateCount = 1000000;
const slop = 1000;
const injectCount = 10000;
const initialCount = 100000;
const updateCount = 100000;
// Test inject performance.
await new Promise(resolve => setTimeout(resolve, 0));
let injectSum = 0;
const injectProperties = { attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' };
const injectContainer = document.createElement('div');
for (let iii = 0; iii < injectCount; iii++) {
const strings = getStrings();
const values = getValues(injectProperties);
const t0 = performance.now();
render(injectContainer, html(strings, ...values));
const t1 = performance.now();
injectSum += t1 - t0;
if (tests.includes('inject')) {
// Test inject performance.
await tick();
let injectSum = 0;
const injectProperties = { attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' };
const injectContainer = document.createElement('div');
for (let iii = 0; iii < injectCount + slop; iii++) {
const strings = getStrings();
const values = getValues(injectProperties);
const t0 = performance.now();
render(injectContainer, html(strings, ...values));
const t1 = performance.now();
if (iii >= slop) {
injectSum += t1 - t0;
}
if (iii % 100 === 0) {
await tick();
}
}
const injectAverage = `${(injectSum / injectCount * 1000).toFixed(1).padStart(6)} µs`;
output.textContent += `${output.textContent ? '\n' : ''}inject: ${injectAverage} (tested ${injectCount.toLocaleString()} times)`;
}
const injectAverage = `${(injectSum / injectCount * 1000).toFixed(1).padStart(6)} µs`;
output.textContent += `${output.textContent ? '\n' : ''}inject: ${injectAverage} (tested ${injectCount.toLocaleString()} times)`;
// Test initial performance.
await new Promise(resolve => setTimeout(resolve, 0));
let initialSum = 0;
const initialStrings = getStrings();
const initialProperties = { attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' };
const initialValues = getValues(initialProperties);
const initialContainer = document.createElement('div');
for (let iii = 0; iii < initialCount; iii++) {
const t0 = performance.now();
render(initialContainer, html(initialStrings, ...initialValues));
const t1 = performance.now();
initialSum += t1 - t0;
if (tests.includes('initial')) {
// Test initial performance.
await new Promise(resolve => setTimeout(resolve, 0));
let initialSum = 0;
const initialStrings = getStrings();
const initialProperties = { attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' };
const initialValues = getValues(initialProperties);
const initialContainer = document.createElement('div');
for (let iii = 0; iii < initialCount + slop; iii++) {
const t0 = performance.now();
render(initialContainer, html(initialStrings, ...initialValues));
const t1 = performance.now();
if (iii >= slop) {
initialSum += t1 - t0;
}
if (iii % 1000 === 0) {
await tick();
}
}
const initialAverage = `${(initialSum / initialCount * 1000).toFixed(1).padStart(4)} µs`;
output.textContent += `${output.textContent ? '\n' : ''}initial: ${initialAverage} (tested ${initialCount.toLocaleString()} times)`;
}
const initialAverage = `${(initialSum / initialCount * 1000).toFixed(1).padStart(4)} µs`;
output.textContent += `${output.textContent ? '\n' : ''}initial: ${initialAverage} (tested ${initialCount.toLocaleString()} times)`;
// Test update performance.
await new Promise(resolve => setTimeout(resolve, 0));
let updateSum = 0;
const updateStrings = getStrings();
const updateProperties = [
{ attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' },
{ attr: '456', id: 'bar', hidden: false, title: 'test', content1: 'ZZZ', content2: 'BBB' },
];
const updateContainer = document.createElement('div');
for (let iii = 0; iii < updateCount; iii++) {
const values = getValues(updateProperties[iii % 2]);
const t0 = performance.now();
render(updateContainer, html(updateStrings, ...values));
const t1 = performance.now();
updateSum += t1 - t0;
if (tests.includes('update')) {
// Test update performance.
await new Promise(resolve => setTimeout(resolve, 0));
let updateSum = 0;
const updateStrings = getStrings();
const updateProperties = [
{ attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' },
{ attr: '456', id: 'bar', hidden: false, title: 'test', content1: 'ZZZ', content2: 'BBB' },
];
const updateContainer = document.createElement('div');
for (let iii = 0; iii < updateCount + slop; iii++) {
const values = getValues(updateProperties[iii % 2]);
const t0 = performance.now();
render(updateContainer, html(updateStrings, ...values));
const t1 = performance.now();
if (iii >= slop) {
updateSum += t1 - t0;
}
if (iii % 1000 === 0) {
await tick();
}
}
const updateAverage = `${(updateSum / updateCount * 1000).toFixed(1).padStart(5)} µs`;
output.textContent += `${output.textContent ? '\n' : ''}update: ${updateAverage} (tested ${updateCount.toLocaleString()} times)`;
}
const updateAverage = `${(updateSum / updateCount * 1000).toFixed(1).padStart(5)} µs`;
output.textContent += `${output.textContent ? '\n' : ''}update: ${updateAverage} (tested ${updateCount.toLocaleString()} times)`;
};
await run(document.getElementById('default'), XElement);
await run(document.getElementById('lit-html'), LitHtmlElement);
await run(document.getElementById('uhtml'), UhtmlElement);
await run(document.getElementById('default'), XElement, 'inject', 'initial', 'update');
await run(document.getElementById('lit-html'), LitHtmlElement, 'inject', 'initial', 'update');
await run(document.getElementById('uhtml'), UhtmlElement, 'inject', 'initial', 'update');

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

"description": "Custom Element base class.",
"version": "1.0.0-rc.55",
"version": "1.0.0-rc.56",
"license": "SEE LICENSE IN LICENSE",

@@ -8,0 +8,0 @@ "repository": "https://github.com/Netflix/x-element",

@@ -216,2 +216,15 @@ import XElement from '../x-element.js';

it('resets compute validity on initialization to catch upgrade edge cases with internal, computed properties', () => {
const el = document.createElement('test-element');
el.setAttribute('a', '1');
el.setAttribute('b', '2');
assert(el.a === undefined);
assert(el.b === undefined);
assert(Number.isNaN(el.internal.c));
document.body.append(el);
assert(el.a === 1);
assert(el.b === 2);
assert(el.internal.c === 3);
});
it('cannot be written to from host', () => {

@@ -218,0 +231,0 @@ const el = document.createElement('test-element');

@@ -22,36 +22,2 @@ import XElement from '../x-element.js';

it('refuses to interpolate within a style tag', () => {
const getTemplate = ({ color }) => {
return html`
<style id="target">
#target {
background-color: ${color};
}
</style>
`;
};
const container = document.createElement('div');
document.body.append(container);
render(container, getTemplate({ color: 'url(evil-url)' }));
const textContent = container.querySelector('#target').textContent;
assert(textContent === '/* x-element: Interpolation is not allowed here. */', textContent);
container.remove();
});
it('refuses to interpolate within a script tag', () => {
const getTemplate = ({ message }) => {
return html`
<script id="target">
console.log('${message}');
</script>
`;
};
const container = document.createElement('div');
document.body.append(container);
render(container, getTemplate({ message: '\' + prompt(\'evil\') + \'' }));
const textContent = container.querySelector('#target').textContent;
assert(textContent === '/* x-element: Interpolation is not allowed here. */', textContent);
container.remove();
});
it('renders interpolated content without parsing', () => {

@@ -728,5 +694,43 @@ const userContent = '<a href="https://evil.com">Click Me!</a>';

describe('templating', () => {
it.skip('throws when attempting to interpolate within a style tag', () => {});
it('throws when attempting to interpolate within a style tag', () => {
const getTemplate = ({ color }) => {
return html`
<style id="target">
#target {
background-color: ${color};
}
</style>
`;
};
const container = document.createElement('div');
document.body.append(container);
let error;
try {
render(container, getTemplate({ color: 'url(evil-url)' }));
} catch (e) {
error = e;
}
assert(error?.message === `Interpolation of "style" tags is not allowed.`, error.message);
container.remove();
});
it.skip('throws when attempting to interpolate within a script tag', () => {});
it('throws when attempting to interpolate within a script tag', () => {
const getTemplate = ({ message }) => {
return html`
<script id="target">
console.log('${message}');
</script>
`;
};
const container = document.createElement('div');
document.body.append(container);
let error;
try {
render(container, getTemplate({ message: '\' + prompt(\'evil\') + \'' }));
} catch (e) {
error = e;
}
assert(error?.message === `Interpolation of "script" tags is not allowed.`, error.message);
container.remove();
});

@@ -743,3 +747,3 @@ it('throws for unquoted attributes', () => {

}
assert(error?.message === `Found invalid template near " not-ok=".`, error.message);
assert(error?.message === `Found invalid template string "<div id="target" not-ok=" at " not-ok=".`, error.message);
container.remove();

@@ -758,3 +762,3 @@ });

}
assert(error?.message === `Found invalid template near " not-ok='".`, error.message);
assert(error?.message === `Found invalid template string "<div id="target" not-ok='" at " not-ok='".`, error.message);
container.remove();

@@ -773,3 +777,3 @@ });

}
assert(error?.message === `Found invalid template near " .notOk=".`, error.message);
assert(error?.message === `Found invalid template string "<div id="target" .notOk=" at " .notOk=".`, error.message);
container.remove();

@@ -788,3 +792,3 @@ });

}
assert(error?.message === `Found invalid template near " .notOk='".`, error.message);
assert(error?.message === `Found invalid template string "<div id="target" .notOk='" at " .notOk='".`, error.message);
container.remove();

@@ -813,4 +817,4 @@ });

describe('ifDefined', () => {
it('throws if used on a "boolean-attribute"', () => {
const expected = 'The ifDefined update must be used on an attribute, not on a boolean-attribute.';
it('throws if used on a "boolean"', () => {
const expected = 'The ifDefined update must be used on an attribute, not on a boolean attribute.';
const getTemplate = ({ maybe }) => {

@@ -868,4 +872,4 @@ return html`<div id="target" ?maybe="${ifDefined(maybe)}"></div>`;

it('throws if used with "text-content"', () => {
const expected = 'The ifDefined update must be used on an attribute, not on text-content.';
it('throws if used with "text"', () => {
const expected = 'The ifDefined update must be used on an attribute, not on text content.';
const getTemplate = ({ maybe }) => {

@@ -889,4 +893,4 @@ return html`<textarea id="target">${ifDefined(maybe)}</textarea>`;

describe('nullish', () => {
it('throws if used on a "boolean-attribute"', () => {
const expected = 'The nullish update must be used on an attribute, not on a boolean-attribute.';
it('throws if used on a "boolean"', () => {
const expected = 'The nullish update must be used on an attribute, not on a boolean attribute.';
const getTemplate = ({ maybe }) => {

@@ -944,4 +948,4 @@ return html`<div id="target" ?maybe="${nullish(maybe)}"></div>`;

it('throws if used with "text-content"', () => {
const expected = 'The nullish update must be used on an attribute, not on text-content.';
it('throws if used with "text"', () => {
const expected = 'The nullish update must be used on an attribute, not on text content.';
const getTemplate = ({ maybe }) => {

@@ -983,4 +987,4 @@ return html`<textarea id="target">${nullish(maybe)}</textarea>`;

it('throws if used on a "boolean-attribute"', () => {
const expected = 'The live update must be used on a property, not on a boolean-attribute.';
it('throws if used on a "boolean"', () => {
const expected = 'The live update must be used on a property, not on a boolean attribute.';
const getTemplate = ({ maybe }) => {

@@ -1020,4 +1024,4 @@ return html`<div id="target" ?maybe="${live(maybe)}"></div>`;

it('throws if used with "text-content"', () => {
const expected = 'The live update must be used on a property, not on text-content.';
it('throws if used with "text"', () => {
const expected = 'The live update must be used on a property, not on text content.';
const getTemplate = ({ maybe }) => {

@@ -1059,4 +1063,4 @@ return html`<textarea id="target">${live(maybe)}</textarea>`;

it('throws if used on a "boolean-attribute"', () => {
const expected = 'The unsafeHTML update must be used on content, not on a boolean-attribute.';
it('throws if used on a "boolean"', () => {
const expected = 'The unsafeHTML update must be used on content, not on a boolean attribute.';
const getTemplate = ({ maybe }) => {

@@ -1096,4 +1100,4 @@ return html`<div id="target" ?maybe="${unsafeHTML(maybe)}"></div>`;

it('throws if used with "text-content"', () => {
const expected = 'The unsafeHTML update must be used on content, not on text-content.';
it('throws if used with "text"', () => {
const expected = 'The unsafeHTML update must be used on content, not on text content.';
const getTemplate = ({ maybe }) => {

@@ -1155,4 +1159,4 @@ return html`<textarea id="target">${unsafeHTML(maybe)}</textarea>`;

it('throws if used on a "boolean-attribute"', () => {
const expected = 'The unsafeSVG update must be used on content, not on a boolean-attribute.';
it('throws if used on a "boolean"', () => {
const expected = 'The unsafeSVG update must be used on content, not on a boolean attribute.';
const getTemplate = ({ maybe }) => {

@@ -1192,4 +1196,4 @@ return html`<div id="target" ?maybe="${unsafeSVG(maybe)}"></div>`;

it('throws if used with "text-content"', () => {
const expected = 'The unsafeSVG update must be used on content, not on text-content.';
it('throws if used with "text"', () => {
const expected = 'The unsafeSVG update must be used on content, not on text content.';
const getTemplate = ({ maybe }) => {

@@ -1287,4 +1291,4 @@ return html`<textarea id="target">${unsafeSVG(maybe)}</textarea>`;

it('throws if used on a "boolean-attribute"', () => {
const expected = 'The map update must be used on content, not on a boolean-attribute.';
it('throws if used on a "boolean"', () => {
const expected = 'The map update must be used on content, not on a boolean attribute.';
const getTemplate = ({ maybe }) => {

@@ -1324,4 +1328,4 @@ return html`<div id="target" ?maybe="${map(maybe, () => {}, () => {})}"></div>`;

it('throws if used with "text-content"', () => {
const expected = 'The map update must be used on content, not on text-content.';
it('throws if used with "text"', () => {
const expected = 'The map update must be used on content, not on text content.';
const getTemplate = ({ maybe }) => {

@@ -1460,4 +1464,4 @@ return html`<textarea id="target">${map(maybe, () => {}, () => {})}</textarea>`;

it('throws if used on a "boolean-attribute"', () => {
const expected = 'The repeat update must be used on content, not on a boolean-attribute.';
it('throws if used on a "boolean"', () => {
const expected = 'The repeat update must be used on content, not on a boolean attribute.';
const getTemplate = ({ maybe }) => {

@@ -1497,4 +1501,4 @@ return html`<div id="target" ?maybe="${repeat(maybe, () => {})}"></div>`;

it('throws if used with "text-content"', () => {
const expected = 'The repeat update must be used on content, not on text-content.';
it('throws if used with "text"', () => {
const expected = 'The repeat update must be used on content, not on text content.';
const getTemplate = ({ maybe }) => {

@@ -1501,0 +1505,0 @@ return html`<textarea id="target">${repeat(maybe, () => {})}</textarea>`;

@@ -649,3 +649,3 @@ /** Base element class for creating custom elements. */

const hostInfo = XElement.#hosts.get(host);
const { initialized, invalidProperties } = hostInfo;
const { computeMap, initialized, invalidProperties } = hostInfo;
if (initialized === false) {

@@ -665,2 +665,5 @@ XElement.#upgradeOwnProperties(host);

invalidProperties.add(property);
if (property.compute) {
computeMap.get(property).valid = false;
}
}

@@ -1067,7 +1070,14 @@ hostInfo.initialized = true;

static #updaters = new WeakMap();
static #ATTRIBUTE_PREFIXES = {
attribute: 'x-element-attribute-',
boolean: 'x-element-boolean-',
property: 'x-element-property-',
};
static #CONTENT_PREFIX = 'x-element-content-';
static #CONTENT_REGEX = new RegExp(`${Template.#CONTENT_PREFIX}(\\d+)`);
static #OPEN = /<[a-z][a-z0-9-]*(?=\s)/g;
static #STEP = /\s+[a-z][a-z0-9-]*(?=[\s>])|\s+[a-z][a-zA-Z0-9-]*="[^"]*"/y;
static #CLOSE = />/g;
static #ATTRIBUTE = /\s+(\??([a-z][a-zA-Z0-9-]*))="$/y;
static #PROPERTY = /\s+\.([a-z][a-zA-Z0-9_]*)="$/y;
static #CLOSE = />/g;

@@ -1085,7 +1095,6 @@ #type = null;

if (!this.#analysis) {
let html = '';
const htmlStrings = [];
const state = { inside: false, index: 0 };
for (let iii = 0; iii < this.#strings.length; iii++) {
const string = this.#strings[iii];
html += string;
let string = this.#strings[iii];
Template.#exhaustString(string, state);

@@ -1099,6 +1108,6 @@ if (state.inside) {

// We found a match like this: html`<div ?hidden="${!!value}"></div>`.
html = html.slice(0, -3 - name.length) + `x-element-boolean-attribute-$${iii}="${name}`;
string = string.slice(0, -3 - name.length) + `${Template.#ATTRIBUTE_PREFIXES.boolean}${iii}="${name}`;
} else {
// We found a match like this: html`<div title="${value}"></div>`.
html = html.slice(0, -2 - name.length) + `x-element-attribute-$${iii}="${name}`;
string = string.slice(0, -2 - name.length) + `${Template.#ATTRIBUTE_PREFIXES.attribute}${iii}="${name}`;
}

@@ -1112,6 +1121,6 @@ state.index = 1; // Accounts for an expected quote character next.

const name = propertyMatch[1];
html = html.slice(0, -3 - name.length) + `x-element-property-$${iii}="${name}`;
string = string.slice(0, -3 - name.length) + `${Template.#ATTRIBUTE_PREFIXES.property}${iii}="${name}`;
state.index = 1; // Accounts for an expected quote character next.
} else {
throw new Error(`Found invalid template near "${string.slice(state.index)}".`);
throw new Error(`Found invalid template string "${string}" at "${string.slice(state.index)}".`);
}

@@ -1121,9 +1130,10 @@ }

// Assume it's a match like this: html`<div>${value}</div>`.
html += `<!--x-element-content-$${iii}-->`;
string += `<!--${Template.#CONTENT_PREFIX}${iii}-->`;
state.index = 0; // No characters to account for. Reset to zero.
}
htmlStrings[iii] = string;
}
if (this.#type === 'svg') {
html = `<svg xmlns="http://www.w3.org/2000/svg">${html}</svg>`;
}
const html = this.#type === 'svg'
? `<svg xmlns="http://www.w3.org/2000/svg">${htmlStrings.join('')}</svg>`
: htmlStrings.join('');
const element = document.createElement('template');

@@ -1265,31 +1275,35 @@ element.innerHTML = html;

if (node.nodeType === Node.ELEMENT_NODE) {
for (const attribute of [...node.attributes]) {
const attributeMatch = attribute.name.match(/^x-element-attribute-\$(\d+)$/);
const booleanAttributeMatch = !attributeMatch ? attribute.name.match(/^x-element-boolean-attribute-\$(\d+)$/) : null;
const propertyMatch = !attributeMatch && !booleanAttributeMatch ? attribute.name.match(/^x-element-property-\$(\d+)$/) : null;
if (attributeMatch) {
node.removeAttribute(attributeMatch[0]);
items.push({ path, key: attributeMatch[1], type: 'attribute', name: attribute.value });
} else if (booleanAttributeMatch) {
node.removeAttribute(booleanAttributeMatch[0]);
items.push({ path, key: booleanAttributeMatch[1], type: 'boolean-attribute', name: attribute.value });
} else if (propertyMatch) {
node.removeAttribute(propertyMatch[0]);
items.push({ path, key: propertyMatch[1], type: 'property', name: attribute.value });
const attributesToRemove = new Set();
for (const attribute of node.attributes) {
const name = attribute.name;
const type = name.startsWith(Template.#ATTRIBUTE_PREFIXES.attribute)
? 'attribute'
: name.startsWith(Template.#ATTRIBUTE_PREFIXES.boolean)
? 'boolean'
: name.startsWith(Template.#ATTRIBUTE_PREFIXES.property)
? 'property'
: null;
if (type) {
const prefix = Template.#ATTRIBUTE_PREFIXES[type];
const key = name.slice(prefix.length);
const value = attribute.value;
items.push({ path, key, type, name: value });
attributesToRemove.add(name);
}
}
for (const attribute of attributesToRemove) {
node.removeAttribute(attribute);
}
// Special case to handle elements which only allow text content (no comments).
if (node.localName.match(/^plaintext|script|style|textarea|title$/)) {
const contentMatch = node.textContent.match(/x-element-content-\$(\d+)/);
const localName = node.localName;
if ((localName === 'style' || localName === 'script') && Template.#CONTENT_REGEX.exec(node.textContent)) {
throw new Error(`Interpolation of "${localName}" tags is not allowed.`);
} else if (localName === 'plaintext' || localName === 'textarea' || localName === 'title') {
const contentMatch = Template.#CONTENT_REGEX.exec(node.textContent);
if (contentMatch) {
if (node.localName.match(/^plaintext|textarea|title$/)) {
node.textContent = '';
items.push({ path, key: contentMatch[1], type: 'text-content' });
} else {
node.textContent = '/* x-element: Interpolation is not allowed here. */';
}
items.push({ path, key: contentMatch[1], type: 'text' });
}
}
} else if (node.nodeType === Node.COMMENT_NODE) {
const contentMatch = node.textContent.match(/x-element-content-\$(\d+)/);
const contentMatch = Template.#CONTENT_REGEX.exec(node.textContent);
if (contentMatch) {

@@ -1324,3 +1338,3 @@ node.textContent = '';

case 'attribute':
case 'boolean-attribute':
case 'boolean':
case 'property': {

@@ -1336,3 +1350,3 @@ nextItems.push({ key: item.key, type: item.type, name: item.name, node });

}
case 'text-content': {
case 'text': {
const nextItem = { key: item.key, type: item.type, node };

@@ -1358,6 +1372,6 @@ nextItems.push(nextItem);

break;
case 'boolean-attribute':
case 'boolean':
updater
? updater(type, lastValue, { node, name })
: Template.#booleanAttribute(type, values[key], lastValue, { node, name });
: Template.#boolean(type, values[key], lastValue, { node, name });
break;

@@ -1374,6 +1388,6 @@ case 'property':

break;
case 'text-content':
case 'text':
updater
? updater(type, lastValue, { node })
: Template.#textContent(type, values[key], lastValue, { node });
: Template.#text(type, values[key], lastValue, { node });
break;

@@ -1390,3 +1404,3 @@ }

static #booleanAttribute(type, value, lastValue, { node, name }) {
static #boolean(type, value, lastValue, { node, name }) {
if (value !== lastValue) {

@@ -1405,3 +1419,3 @@ value

static #textContent(type, value, lastValue, { node }) {
static #text(type, value, lastValue, { node }) {
if (value !== lastValue) {

@@ -1646,7 +1660,14 @@ node.textContent = value;

static #getTypeText(type) {
return type === 'attribute'
? `an ${type}`
: type === 'boolean-attribute' || type === 'property'
? `a ${type}`
: `${type}`;
switch (type) {
case 'attribute':
return 'an attribute';
case 'boolean':
return 'a boolean attribute';
case 'property':
return 'a property';
case 'content':
return 'content';
case 'text':
return 'text content';
}
}

@@ -1653,0 +1674,0 @@ }

Sorry, the diff of this file is not supported yet

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