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

@gltf-transform/functions

Package Overview
Dependencies
Maintainers
1
Versions
144
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@gltf-transform/functions - npm Package Compare versions

Comparing version 3.7.5 to 3.8.0

3

dist/meshopt.d.ts
import type { Transform } from '@gltf-transform/core';
export interface MeshoptOptions {
import { QuantizeOptions } from './quantize.js';
export interface MeshoptOptions extends Omit<QuantizeOptions, 'pattern' | 'patternTargets'> {
encoder: unknown;

@@ -4,0 +5,0 @@ level?: 'medium' | 'high';

@@ -9,2 +9,4 @@ import { Transform } from '@gltf-transform/core';

keepAttributes?: boolean;
/** Whether to keep single-color textures that can be converted to material factors. */
keepSolidTextures?: boolean;
}

@@ -11,0 +13,0 @@ /**

@@ -6,2 +6,4 @@ import { Transform } from '@gltf-transform/core';

pattern?: RegExp;
/** Pattern (regex) used to filter morph target semantics for quantization. Default: `options.pattern`. */
patternTargets?: RegExp;
/** Bounds for quantization grid. */

@@ -24,3 +26,3 @@ quantizationVolume?: 'mesh' | 'scene';

}
export declare const QUANTIZE_DEFAULTS: Required<QuantizeOptions>;
export declare const QUANTIZE_DEFAULTS: Required<Omit<QuantizeOptions, 'patternTargets'>>;
/**

@@ -27,0 +29,0 @@ * References:

import { Texture, Transform, vec2 } from '@gltf-transform/core';
import { TextureResizeFilter } from './texture-resize.js';
type Format = (typeof FORMATS)[number];
declare const FORMATS: readonly ["jpeg", "png", "webp", "avif"];
type Format = (typeof TEXTURE_COMPRESS_SUPPORTED_FORMATS)[number];
export declare const TEXTURE_COMPRESS_SUPPORTED_FORMATS: readonly ["jpeg", "png", "webp", "avif"];
export interface TextureCompressOptions {

@@ -27,5 +27,12 @@ /** Instance of the Sharp encoder, which must be installed from the

pattern?: RegExp | null;
/** Pattern matching the format(s) to be compressed or converted. */
/**
* Pattern matching the format(s) to be compressed or converted. Some examples
* of formats include "jpeg" and "png".
*/
formats?: RegExp | null;
/** Pattern matching the material texture slot(s) to be compressed or converted. */
/**
* Pattern matching the material texture slot(s) to be compressed or converted.
* Some examples of slot names include "baseColorTexture", "occlusionTexture",
* "metallicRoughnessTexture", and "normalTexture".
*/
slots?: RegExp | null;

@@ -32,0 +39,0 @@ /** Quality, 1-100. Default: auto. */

{
"name": "@gltf-transform/functions",
"version": "3.7.5",
"version": "3.8.0",
"repository": "github:donmccurdy/glTF-Transform",

@@ -39,4 +39,4 @@ "homepage": "https://gltf-transform.dev/functions.html",

"dependencies": {
"@gltf-transform/core": "^3.7.5",
"@gltf-transform/extensions": "^3.7.5",
"@gltf-transform/core": "^3.8.0",
"@gltf-transform/extensions": "^3.8.0",
"ktx-parse": "^0.6.0",

@@ -57,3 +57,3 @@ "ndarray": "^1.0.19",

},
"gitHead": "bf05150bcaa3df7add9c86ba8ffaffae3fdd94a7"
"gitHead": "94ea01f4c998c85cb25d56a057f7326d36a81786"
}

@@ -5,6 +5,6 @@ import type { Document, Transform } from '@gltf-transform/core';

import { reorder } from './reorder.js';
import { quantize } from './quantize.js';
import { QUANTIZE_DEFAULTS, QuantizeOptions, quantize } from './quantize.js';
import { createTransform } from './utils.js';
export interface MeshoptOptions {
export interface MeshoptOptions extends Omit<QuantizeOptions, 'pattern' | 'patternTargets'> {
encoder: unknown;

@@ -14,3 +14,6 @@ level?: 'medium' | 'high';

export const MESHOPT_DEFAULTS: Required<Omit<MeshoptOptions, 'encoder'>> = { level: 'high' };
export const MESHOPT_DEFAULTS: Required<Omit<MeshoptOptions, 'encoder'>> = {
level: 'high',
...QUANTIZE_DEFAULTS,
};

@@ -53,2 +56,20 @@ const NAME = 'meshopt';

return createTransform(NAME, async (document: Document): Promise<void> => {
let pattern: RegExp;
let patternTargets: RegExp;
let quantizeNormal = options.quantizeNormal;
// IMPORTANT: Vertex attributes should be quantized in 'high' mode IFF they are
// _not_ filtered in 'packages/extensions/src/ext-meshopt-compression/encoder.ts'.
// Note that normals and tangents use octahedral filters, but _morph_ normals
// and tangents do not.
// See: https://github.com/donmccurdy/glTF-Transform/issues/1142
if (options.level === 'medium') {
pattern = /.*/;
patternTargets = /.*/;
} else {
pattern = /^(POSITION|TEXCOORD|JOINTS|WEIGHTS)(_\d+)?$/;
patternTargets = /^(POSITION|TEXCOORD|JOINTS|WEIGHTS|NORMAL|TANGENT)(_\d+)?$/;
quantizeNormal = Math.min(quantizeNormal, 8); // See meshopt::getMeshoptFilter.
}
await document.transform(

@@ -60,10 +81,7 @@ reorder({

quantize({
// IMPORTANT: Vertex attributes should be quantized in 'high' mode IFF they are
// _not_ filtered in 'packages/extensions/src/ext-meshopt-compression/encoder.ts'.
pattern: options.level === 'medium' ? /.*/ : /^(POSITION|TEXCOORD|JOINTS|WEIGHTS)(_\d+)?$/,
quantizePosition: 14,
quantizeTexcoord: 12,
quantizeColor: 8,
quantizeNormal: 8,
})
...options,
pattern,
patternTargets,
quantizeNormal,
}),
);

@@ -70,0 +88,0 @@

import {
AnimationChannel,
ColorUtils,
Document,
ExtensionProperty,
Graph,
ILogger,
Material,
Node,
Primitive,
PrimitiveTarget,
Property,
PropertyType,
Root,
Transform,
Node,
Scene,
ExtensionProperty,
Material,
Primitive,
PrimitiveTarget,
Texture,
TextureInfo,
Transform,
vec3,
vec4,
} from '@gltf-transform/core';
import { mul as mulVec3 } from 'gl-matrix/vec3';
import { add, create, len, mul, scale, sub } from 'gl-matrix/vec4';
import { NdArray } from 'ndarray';
import { getPixels } from 'ndarray-pixels';
import { getTextureColorSpace } from './get-texture-color-space.js';
import { listTextureInfoByMaterial } from './list-texture-info.js';
import { listTextureSlots } from './list-texture-slots.js';
import { createTransform } from './utils.js';
import { listTextureInfoByMaterial } from './list-texture-info.js';
const NAME = 'prune';
const EPS = 3 / 255;
export interface PruneOptions {

@@ -30,2 +42,4 @@ /** List of {@link PropertyType} identifiers to be de-duplicated.*/

keepAttributes?: boolean;
/** Whether to keep single-color textures that can be converted to material factors. */
keepSolidTextures?: boolean;
}

@@ -48,2 +62,3 @@ const PRUNE_DEFAULTS: Required<PruneOptions> = {

keepAttributes: true,
keepSolidTextures: true,
};

@@ -75,8 +90,8 @@

return createTransform(NAME, (doc: Document): void => {
const logger = doc.getLogger();
const root = doc.getRoot();
const graph = doc.getGraph();
return createTransform(NAME, async (document: Document): Promise<void> => {
const logger = document.getLogger();
const root = document.getRoot();
const graph = document.getGraph();
const disposed: Record<string, number> = {};
const counter = new DisposeCounter();

@@ -90,18 +105,42 @@ // Prune top-down, so that low-level properties like accessors can be removed if the

if (mesh.listPrimitives().length > 0) continue;
mesh.dispose();
markDisposed(mesh);
counter.dispose(mesh);
}
}
if (propertyTypes.has(PropertyType.NODE) && !options.keepLeaves) root.listScenes().forEach(nodeTreeShake);
if (propertyTypes.has(PropertyType.NODE)) root.listNodes().forEach(treeShake);
if (propertyTypes.has(PropertyType.SKIN)) root.listSkins().forEach(treeShake);
if (propertyTypes.has(PropertyType.MESH)) root.listMeshes().forEach(treeShake);
if (propertyTypes.has(PropertyType.CAMERA)) root.listCameras().forEach(treeShake);
if (propertyTypes.has(PropertyType.NODE)) {
if (!options.keepLeaves) {
for (const scene of root.listScenes()) {
nodeTreeShake(graph, scene, counter);
}
}
for (const node of root.listNodes()) {
treeShake(node, counter);
}
}
if (propertyTypes.has(PropertyType.SKIN)) {
for (const skin of root.listSkins()) {
treeShake(skin, counter);
}
}
if (propertyTypes.has(PropertyType.MESH)) {
for (const mesh of root.listMeshes()) {
treeShake(mesh, counter);
}
}
if (propertyTypes.has(PropertyType.CAMERA)) {
for (const camera of root.listCameras()) {
treeShake(camera, counter);
}
}
if (propertyTypes.has(PropertyType.PRIMITIVE)) {
indirectTreeShake(graph, PropertyType.PRIMITIVE);
indirectTreeShake(graph, PropertyType.PRIMITIVE, counter);
}
if (propertyTypes.has(PropertyType.PRIMITIVE_TARGET)) {
indirectTreeShake(graph, PropertyType.PRIMITIVE_TARGET);
indirectTreeShake(graph, PropertyType.PRIMITIVE_TARGET, counter);
}

@@ -115,3 +154,3 @@

const material = prim.getMaterial();
const required = listRequiredSemantics(doc, material);
const required = listRequiredSemantics(document, material);
const unused = listUnusedSemantics(prim, required);

@@ -140,4 +179,3 @@ pruneAttributes(prim, unused);

if (!channel.getTargetNode()) {
channel.dispose();
markDisposed(channel);
counter.dispose(channel);
}

@@ -147,6 +185,6 @@ }

const samplers = anim.listSamplers();
treeShake(anim);
samplers.forEach(treeShake);
treeShake(anim, counter);
samplers.forEach((sampler) => treeShake(sampler, counter));
} else {
anim.listSamplers().forEach(treeShake);
anim.listSamplers().forEach((sampler) => treeShake(sampler, counter));
}

@@ -156,7 +194,21 @@ }

if (propertyTypes.has(PropertyType.MATERIAL)) root.listMaterials().forEach(treeShake);
if (propertyTypes.has(PropertyType.TEXTURE)) root.listTextures().forEach(treeShake);
if (propertyTypes.has(PropertyType.ACCESSOR)) root.listAccessors().forEach(treeShake);
if (propertyTypes.has(PropertyType.BUFFER)) root.listBuffers().forEach(treeShake);
if (propertyTypes.has(PropertyType.MATERIAL)) {
root.listMaterials().forEach((material) => treeShake(material, counter));
}
if (propertyTypes.has(PropertyType.TEXTURE)) {
root.listTextures().forEach((texture) => treeShake(texture, counter));
if (!options.keepSolidTextures) {
await pruneSolidTextures(document, counter);
}
}
if (propertyTypes.has(PropertyType.ACCESSOR)) {
root.listAccessors().forEach((accessor) => treeShake(accessor, counter));
}
if (propertyTypes.has(PropertyType.BUFFER)) {
root.listBuffers().forEach((buffer) => treeShake(buffer, counter));
}
// TODO(bug): This process does not identify unused ExtensionProperty instances. That could

@@ -167,5 +219,6 @@ // be a future enhancement, either tracking unlinked properties as if they were connected

if (Object.keys(disposed).length) {
const str = Object.keys(disposed)
.map((t) => `${t} (${disposed[t]})`)
if (!counter.empty()) {
const str = counter
.entries()
.map(([type, count]) => `${type} (${count})`)
.join(', ');

@@ -178,60 +231,83 @@ logger.info(`${NAME}: Removed types... ${str}`);

logger.debug(`${NAME}: Complete.`);
});
}
//
/**********************************************************************************************
* Utility for disposing properties and reporting statistics afterward.
*/
/** Disposes of the given property if it is unused. */
function treeShake(prop: Property): void {
// Consider a property unused if it has no references from another property, excluding
// types Root and AnimationChannel.
const parents = prop.listParents().filter((p) => !(p instanceof Root || p instanceof AnimationChannel));
if (!parents.length) {
prop.dispose();
markDisposed(prop);
}
}
class DisposeCounter {
public readonly disposed: Record<string, number> = {};
/**
* For property types the Root does not maintain references to, we'll need to search the
* graph. It's possible that objects may have been constructed without any outbound links,
* but since they're not on the graph they don't need to be tree-shaken.
*/
function indirectTreeShake(graph: Graph<Property>, propertyType: string): void {
graph
.listEdges()
.map((edge) => edge.getParent())
.filter((parent) => parent.propertyType === propertyType)
.forEach(treeShake);
}
empty(): boolean {
for (const key in this.disposed) return false;
return true;
}
/** Iteratively prunes leaf Nodes without contents. */
function nodeTreeShake(prop: Node | Scene): void {
prop.listChildren().forEach(nodeTreeShake);
entries(): [string, number][] {
return Object.entries(this.disposed);
}
if (prop instanceof Scene) return;
/** Records properties disposed by type. */
dispose(prop: Property): void {
this.disposed[prop.propertyType] = this.disposed[prop.propertyType] || 0;
this.disposed[prop.propertyType]++;
prop.dispose();
}
}
const isUsed = graph.listParentEdges(prop).some((e) => {
const ptype = e.getParent().propertyType;
return ptype !== PropertyType.ROOT && ptype !== PropertyType.SCENE && ptype !== PropertyType.NODE;
});
const isEmpty = graph.listChildren(prop).length === 0;
if (isEmpty && !isUsed) {
prop.dispose();
markDisposed(prop);
}
}
/**********************************************************************************************
* Helper functions for the {@link prune} transform.
*
* IMPORTANT: These functions were previously declared in function scope, but
* broke in the CommonJS build due to a buggy Babel transform. See:
* https://github.com/donmccurdy/glTF-Transform/issues/1140
*/
function pruneAttributes(prim: Primitive | PrimitiveTarget, unused: string[]) {
for (const semantic of unused) {
prim.setAttribute(semantic, null);
}
/** Disposes of the given property if it is unused. */
function treeShake(prop: Property, counter: DisposeCounter): void {
// Consider a property unused if it has no references from another property, excluding
// types Root and AnimationChannel.
const parents = prop.listParents().filter((p) => !(p instanceof Root || p instanceof AnimationChannel));
if (!parents.length) {
counter.dispose(prop);
}
}
/**
* For property types the Root does not maintain references to, we'll need to search the
* graph. It's possible that objects may have been constructed without any outbound links,
* but since they're not on the graph they don't need to be tree-shaken.
*/
function indirectTreeShake(graph: Graph<Property>, propertyType: string, counter: DisposeCounter): void {
for (const edge of graph.listEdges()) {
const parent = edge.getParent();
if (parent.propertyType === propertyType) {
treeShake(parent, counter);
}
}
}
/** Records properties disposed by type. */
function markDisposed(prop: Property): void {
disposed[prop.propertyType] = disposed[prop.propertyType] || 0;
disposed[prop.propertyType]++;
}
/** Iteratively prunes leaf Nodes without contents. */
function nodeTreeShake(graph: Graph<Property>, prop: Node | Scene, counter: DisposeCounter): void {
prop.listChildren().forEach((child) => nodeTreeShake(graph, child, counter));
if (prop instanceof Scene) return;
const isUsed = graph.listParentEdges(prop).some((e) => {
const ptype = e.getParent().propertyType;
return ptype !== PropertyType.ROOT && ptype !== PropertyType.SCENE && ptype !== PropertyType.NODE;
});
const isEmpty = graph.listChildren(prop).length === 0;
if (isEmpty && !isUsed) {
counter.dispose(prop);
}
}
function pruneAttributes(prim: Primitive | PrimitiveTarget, unused: string[]) {
for (const semantic of unused) {
prim.setAttribute(semantic, null);
}
}
/**

@@ -261,3 +337,3 @@ * Lists vertex attribute semantics that are unused when rendering a given primitive.

material: Material | ExtensionProperty | null,
semantics = new Set<string>()
semantics = new Set<string>(),
): Set<string> {

@@ -348,1 +424,104 @@ if (!material) return semantics;

}
/**********************************************************************************************
* Prune solid (single-color) textures.
*/
async function pruneSolidTextures(document: Document, counter: DisposeCounter): Promise<void> {
const root = document.getRoot();
const graph = document.getGraph();
const logger = document.getLogger();
const textures = root.listTextures();
const pending = textures.map(async (texture) => {
const factor = await getTextureFactor(texture);
if (!factor) return;
if (getTextureColorSpace(texture) === 'srgb') {
ColorUtils.convertSRGBToLinear(factor, factor);
}
const name = texture.getName() || texture.getURI();
const size = texture.getSize()?.join('x');
const slots = listTextureSlots(texture);
for (const edge of graph.listParentEdges(texture)) {
const parent = edge.getParent();
if (parent !== root && applyMaterialFactor(parent as Material, factor, edge.getName(), logger)) {
edge.dispose();
}
}
if (texture.listParents().length === 1) {
counter.dispose(texture);
logger.debug(`${NAME}: Removed solid-color texture "${name}" (${size}px ${slots.join(', ')})`);
}
});
await Promise.all(pending);
}
function applyMaterialFactor(
material: Material | ExtensionProperty,
factor: vec4,
slot: string,
logger: ILogger,
): boolean {
if (material instanceof Material) {
switch (slot) {
case 'baseColorTexture':
material.setBaseColorFactor(mul(factor, factor, material.getBaseColorFactor()) as vec4);
return true;
case 'emissiveTexture':
material.setEmissiveFactor(
mulVec3([0, 0, 0], factor.slice(0, 3) as vec3, material.getEmissiveFactor()) as vec3,
);
return true;
case 'occlusionTexture':
return Math.abs(factor[0] - 1) <= EPS;
case 'metallicRoughnessTexture':
material.setRoughnessFactor(factor[1] * material.getRoughnessFactor());
material.setMetallicFactor(factor[2] * material.getMetallicFactor());
return true;
case 'normalTexture':
return len(sub(create(), factor, [0.5, 0.5, 1, 1])) <= EPS;
}
}
logger.warn(`${NAME}: Detected single-color ${slot} texture. Pruning ${slot} not yet supported.`);
return false;
}
async function getTextureFactor(texture: Texture): Promise<vec4 | null> {
const pixels = await maybeGetPixels(texture);
if (!pixels) return null;
const min: vec4 = [Infinity, Infinity, Infinity, Infinity];
const max: vec4 = [-Infinity, -Infinity, -Infinity, -Infinity];
const target: vec4 = [0, 0, 0, 0];
const [width, height] = pixels.shape;
for (let i = 0; i < width; i++) {
for (let j = 0; j < height; j++) {
for (let k = 0; k < 4; k++) {
min[k] = Math.min(min[k], pixels.get(i, j, k));
max[k] = Math.max(max[k], pixels.get(i, j, k));
}
}
if (len(sub(target, max, min)) / 255 > EPS) {
return null;
}
}
return scale(target, add(target, max, min), 0.5 / 255) as vec4;
}
async function maybeGetPixels(texture: Texture): Promise<NdArray<Uint8Array> | null> {
try {
return await getPixels(texture.getImage()!, texture.getMimeType());
} catch (e) {
return null;
}
}

@@ -45,2 +45,4 @@ import {

pattern?: RegExp;
/** Pattern (regex) used to filter morph target semantics for quantization. Default: `options.pattern`. */
patternTargets?: RegExp;
/** Bounds for quantization grid. */

@@ -64,3 +66,3 @@ quantizationVolume?: 'mesh' | 'scene';

export const QUANTIZE_DEFAULTS: Required<QuantizeOptions> = {
export const QUANTIZE_DEFAULTS: Required<Omit<QuantizeOptions, 'patternTargets'>> = {
pattern: /.*/,

@@ -94,2 +96,4 @@ quantizationVolume: 'mesh',

options.patternTargets = options.patternTargets || options.pattern;
return createTransform(NAME, async (doc: Document): Promise<void> => {

@@ -128,3 +132,3 @@ const logger = doc.getLogger();

prune({ propertyTypes: [PropertyType.ACCESSOR, PropertyType.SKIN, PropertyType.MATERIAL] }),
dedup({ propertyTypes: [PropertyType.ACCESSOR, PropertyType.MATERIAL, PropertyType.SKIN] })
dedup({ propertyTypes: [PropertyType.ACCESSOR, PropertyType.MATERIAL, PropertyType.SKIN] }),
);

@@ -140,10 +144,13 @@

nodeTransform: VectorTransform<vec3>,
options: Required<QuantizeOptions>
options: Required<QuantizeOptions>,
): void {
const isTarget = prim instanceof PrimitiveTarget;
const logger = doc.getLogger();
for (const semantic of prim.listSemantics()) {
if (!options.pattern.test(semantic)) continue;
if (!isTarget && !options.pattern.test(semantic)) continue;
if (isTarget && !options.patternTargets.test(semantic)) continue;
const srcAttribute = prim.getAttribute(semantic)!;
const { bits, ctor } = getQuantizationSettings(semantic, srcAttribute, logger, options);

@@ -201,3 +208,3 @@

(max[1] - min[1]) / 2,
(max[2] - min[2]) / 2
(max[2] - min[2]) / 2,
);

@@ -303,3 +310,3 @@

instanceScale ? (instanceScale.getElement(i, s) as vec3) : S_IDENTITY,
instanceMatrix
instanceMatrix,
);

@@ -357,2 +364,3 @@

const hi = 2 * quantBits - storageBits;
const range = [signBits > 0 ? -1 : 0, 1] as vec2;

@@ -362,4 +370,7 @@ for (let i = 0, di = 0, el: number[] = []; i < attribute.getCount(); i++) {

for (let j = 0; j < el.length; j++) {
// Clamp to range.
let value = clamp(el[j], range);
// Map [0.0 ... 1.0] to [0 ... scale].
let value = Math.round(Math.abs(el[j]) * scale);
value = Math.round(Math.abs(value) * scale);

@@ -382,3 +393,3 @@ // Replicate msb to missing lsb.

logger: ILogger,
options: Required<QuantizeOptions>
options: Required<QuantizeOptions>,
): { bits: number; ctor?: TypedArrayConstructor } {

@@ -509,1 +520,5 @@ const min = attribute.getMinNormalized([]);

}
function clamp(value: number, range: vec2): number {
return Math.min(Math.max(value, range[0]), range[1]);
}

@@ -14,4 +14,4 @@ import { BufferUtils, Document, ImageUtils, Texture, TextureChannel, Transform, vec2 } from '@gltf-transform/core';

type Format = (typeof FORMATS)[number];
const FORMATS = ['jpeg', 'png', 'webp', 'avif'] as const;
type Format = (typeof TEXTURE_COMPRESS_SUPPORTED_FORMATS)[number];
export const TEXTURE_COMPRESS_SUPPORTED_FORMATS = ['jpeg', 'png', 'webp', 'avif'] as const;
const SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/avif'];

@@ -41,5 +41,12 @@

pattern?: RegExp | null;
/** Pattern matching the format(s) to be compressed or converted. */
/**
* Pattern matching the format(s) to be compressed or converted. Some examples
* of formats include "jpeg" and "png".
*/
formats?: RegExp | null;
/** Pattern matching the material texture slot(s) to be compressed or converted. */
/**
* Pattern matching the material texture slot(s) to be compressed or converted.
* Some examples of slot names include "baseColorTexture", "occlusionTexture",
* "metallicRoughnessTexture", and "normalTexture".
*/
slots?: RegExp | null;

@@ -337,3 +344,3 @@

const format = mimeType.split('/').pop() as Format | undefined;
if (!format || !FORMATS.includes(format)) {
if (!format || !TEXTURE_COMPRESS_SUPPORTED_FORMATS.includes(format)) {
throw new Error(`Unknown MIME type "${mimeType}".`);

@@ -340,0 +347,0 @@ }

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

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