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

htmx-router

Package Overview
Dependencies
Maintainers
0
Versions
41
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

htmx-router - npm Package Compare versions

Comparing version 1.0.0-alpha.4 to 1.0.0-alpha.5

bin/util/response.d.ts

2

bin/cli/config.js
import { readFile } from "fs/promises";
export async function ReadConfig() {
return JSON.parse(await readFile(process.argv[2] || "./htmx-router.json", "utf-8"));
return JSON.parse(await readFile(process.argv[2] || "./htmx-config.json", "utf-8"));
}

@@ -17,4 +17,4 @@ #!/usr/bin/env node

import { GenericContext, RouteTree } from "htmx-router/bin/router";
import { RegisterDynamic } from "htmx-router/bin/util/dynamic";
import { GetClientEntryURL } from 'htmx-router/bin/client/entry';
import { DynamicReference } from "htmx-router/bin/util/dynamic";
import { GetMountUrl } from 'htmx-router/bin/client/mount';

@@ -36,14 +36,8 @@ import { GetSheetUrl } from 'htmx-router/bin/util/css';

export function Dynamic<T extends Record<string, string>>(props: {
params: T,
loader: (params: T, ctx: GenericContext) => Promise<JSX.Element>
params?: T,
loader: (ctx: GenericContext, params?: T) => Promise<JSX.Element>
children?: JSX.Element
}): JSX.Element {
const path = RegisterDynamic(props.loader);
const query = new URLSearchParams();
for (const key in props.params) query.set(key, props.params[key]);
const url = path + query.toString();
return <div
hx-get={url}
hx-get={DynamicReference(props.loader, props.params)}
hx-trigger="load"

@@ -50,0 +44,0 @@ hx-swap="outerHTML transition:true"

@@ -21,3 +21,3 @@ /**

+ `// hash: ${hash}\n`
+ BuildClientServer(imported)),
+ BuildClientServer(config.adapter, imported)),
writeFile(CutString(config.source, ".", -1)[0] + ".manifest.tsx", BuildClientManifest(config.adapter, imported))

@@ -48,3 +48,9 @@ ]);

}
function BuildClientServer(imported) {
function SafeScript(type, script) {
switch (type) {
case "react": return `<script dangerouslySetInnerHTML={{__html: ${script}}}></script>`;
default: return `<script>${script}</script>`;
}
}
function BuildClientServer(type, imported) {
const names = new Array();

@@ -63,3 +69,3 @@ for (const imp of imported) {

+ `\t\t<div className={island}>{ssr}</div>\n`
+ "\t\t<script>{`Router.mountAboveWith('${name}', ${data})`}</script>\n"
+ `\t\t${SafeScript(type, "`Router.mountAboveWith('${name}', ${data})`")}\n`
+ "\t</>);\n"

@@ -66,0 +72,0 @@ + "}\n"

@@ -6,2 +6,5 @@ import { QuickHash } from "../util/hash.js";

const theme = {
get: () => {
return (localStorage.getItem("theme") || theme.infer());
},
infer: () => {

@@ -14,8 +17,6 @@ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

apply: () => {
const current = localStorage.getItem("theme") || theme.infer();
document.documentElement.setAttribute('data-theme', current);
document.documentElement.setAttribute('data-theme', theme.get());
},
toggle: () => {
const current = localStorage.getItem("theme") || theme.infer();
if (current === "dark")
if (theme.get() === "dark")
localStorage.setItem("theme", "light");

@@ -25,2 +26,3 @@ else

theme.apply();
return localStorage.getItem("theme");
}

@@ -55,2 +57,41 @@ };

global.htmx.onLoad(Mount);
// Track the number of active requests
let activeRequests = 0;
const updateLoadingAttribute = () => {
if (activeRequests > 0)
document.body.setAttribute('data-loading', 'true');
else
document.body.removeAttribute('data-loading');
};
const originalXHROpen = XMLHttpRequest.prototype.open;
const originalXHRSend = XMLHttpRequest.prototype.send;
// @ts-ignore
XMLHttpRequest.prototype.open = function (...args) {
this.addEventListener('loadstart', () => {
activeRequests++;
updateLoadingAttribute();
});
this.addEventListener('loadend', () => {
activeRequests--;
updateLoadingAttribute();
});
originalXHROpen.apply(this, args);
};
XMLHttpRequest.prototype.send = function (...args) {
originalXHRSend.apply(this, args);
};
// Override fetch
const originalFetch = window.fetch;
window.fetch = async (...args) => {
activeRequests++;
updateLoadingAttribute();
try {
const response = await originalFetch(...args);
return response;
}
finally {
activeRequests--;
updateLoadingAttribute();
}
};
return {

@@ -57,0 +98,0 @@ mountAboveWith: RequestMount,

import { RouteModule, CatchFunction, RenderFunction } from './types.js';
import { RouteContext, GenericContext } from "./router.js";
import { createRequestHandler } from './request/index.js';
import { MetaDescriptor, RenderMetaDescriptor, ShellOptions, ApplyMetaDescriptorDefaults, LdJsonObject, OpenGraph, OpenGraphImage, OpenGraphVideo, OpenGraphAudio, InferShellOptions } from './util/shell.js';
import { redirect, refresh, revalidate, text, json } from './util/response.js';
import { Cookies, CookieOptions } from "./util/cookies.js";
import { EventSourceConnection } from "./util/event-source.js";
import { DynamicReference } from './util/dynamic.js';
import { StyleClass } from './util/css.js';
import { Endpoint } from './util/endpoint.js';
import { redirect, text, json, refresh } from './response.js';
export { CatchFunction, CookieOptions, Cookies, createRequestHandler, Endpoint, EventSourceConnection, GenericContext, RenderFunction, RouteContext, RouteModule, StyleClass, redirect, text, json, refresh };
export { createRequestHandler, CatchFunction, RenderFunction, RouteContext, RouteModule, GenericContext, Cookies, CookieOptions, Endpoint, DynamicReference, StyleClass, EventSourceConnection, redirect, refresh, revalidate, text, json, MetaDescriptor, RenderMetaDescriptor, ShellOptions, ApplyMetaDescriptorDefaults, LdJsonObject, OpenGraph, OpenGraphImage, OpenGraphVideo, OpenGraphAudio, InferShellOptions };
import { RouteContext, GenericContext } from "./router.js";
import { createRequestHandler } from './request/index.js';
import { RenderMetaDescriptor, ApplyMetaDescriptorDefaults } from './util/shell.js';
import { redirect, refresh, revalidate, text, json } from './util/response.js';
import { Cookies } from "./util/cookies.js";
import { EventSourceConnection } from "./util/event-source.js";
import { DynamicReference } from './util/dynamic.js';
import { StyleClass } from './util/css.js';
import { Endpoint } from './util/endpoint.js';
import { redirect, text, json, refresh } from './response.js';
export { Cookies, createRequestHandler, Endpoint, EventSourceConnection, GenericContext, RouteContext, StyleClass, redirect, text, json, refresh };
export { createRequestHandler, RouteContext, GenericContext,
// Request helpers
Cookies, Endpoint, DynamicReference,
// CSS Helper
StyleClass,
// SSE helper
EventSourceConnection,
// Response helpers
redirect, refresh, revalidate, text, json, RenderMetaDescriptor, ApplyMetaDescriptorDefaults };

@@ -30,4 +30,12 @@ import { GenericContext } from '../router.js';

response = new Response("No Route Found", { status: 404, statusText: "Not Found", headers: ctx.headers });
// Override with context headers
if (response.headers !== ctx.headers) {
for (const [key, value] of ctx.headers) {
if (ctx.headers.has(key))
continue;
response.headers.set(key, value);
}
}
// Merge cookie changes
const headers = Object.fromEntries(ctx.headers);
const headers = Object.fromEntries(response.headers);
const cookies = ctx.cookie.export();

@@ -38,15 +46,3 @@ if (cookies.length > 0) {

}
// Merge context headers
if (response.headers !== ctx.headers) {
for (const [key, value] of response.headers) {
if (!headers[key]) {
headers[key] = value;
continue;
}
if (!Array.isArray(headers[key]))
headers[key] = [headers[key]];
headers[key].push(value);
}
}
return { response, headers };
}

@@ -1,4 +0,9 @@

export declare function redirect(url: string, init?: ResponseInit): Response;
export declare function text(text: string, init?: ResponseInit): Response;
export declare function json(data: unknown, init?: ResponseInit): Response;
export declare function refresh(init?: ResponseInit): Response;
export declare function redirect(url: string, init?: ResponseInit & {
client?: boolean;
}): Response;
export declare function revalidate(init?: ResponseInit): Response;
export declare function refresh(init?: ResponseInit & {
client?: boolean;
}): Response;

@@ -1,10 +0,1 @@

export function redirect(url, init) {
init ||= {};
init.statusText ||= "Temporary Redirect";
init.status = 307;
const res = new Response("", init);
res.headers.set("X-Caught", "true");
res.headers.set("Location", url);
return res;
}
export function text(text, init) {

@@ -16,2 +7,3 @@ init ||= {};

res.headers.set("Content-Type", "text/plain");
res.headers.set("X-Caught", "true");
return res;

@@ -25,4 +17,23 @@ }

res.headers.set("Content-Type", "application/json");
res.headers.set("X-Caught", "true");
return res;
}
export function redirect(url, init) {
init ||= {};
init.statusText ||= "Temporary Redirect";
init.status = 307;
const res = new Response("", init);
if (!init?.client)
res.headers.set("Location", url);
res.headers.set("HX-Location", url); // use hx-boost if applicable
return res;
}
export function revalidate(init) {
init ||= {};
init.statusText ||= "ok";
init.status = 200;
const res = new Response("", init);
res.headers.set("HX-Location", "");
return res;
}
export function refresh(init) {

@@ -33,4 +44,6 @@ init ||= {};

const res = new Response("", init);
if (!init?.client)
res.headers.set("Refresh", "0"); // fallback
res.headers.set("HX-Refresh", "true");
return res;
}

@@ -156,3 +156,3 @@ import * as endpoint from './util/endpoint.js';

return res;
this.unwrap(ctx, res);
return this.unwrap(ctx, res);
}

@@ -159,0 +159,0 @@ return res;

@@ -5,5 +5,5 @@ /**

import { GenericContext } from "../router.js";
export declare function RegisterDynamic<T>(load: Loader<T>): string;
type Loader<T> = (params: T, ctx: GenericContext) => Promise<JSX.Element>;
export declare function DynamicReference<T extends Record<string, string>>(loader: Loader<T>, params?: T): string;
type Loader<T> = (ctx: GenericContext, params: T) => Promise<JSX.Element>;
export declare function _resolve(fragments: string[], ctx: GenericContext): Promise<Response | null>;
export {};

@@ -7,3 +7,3 @@ /**

const index = new Map();
export function RegisterDynamic(load) {
function Register(load) {
const existing = index.get(load);

@@ -15,6 +15,17 @@ if (existing)

registry.set(name, load);
const url = `/_/dynamic/${name}?`;
const url = `/_/dynamic/${name}`;
index.set(load, url);
return url;
}
export function DynamicReference(loader, params) {
let url = Register(loader);
if (params) {
const query = new URLSearchParams();
if (params)
for (const key in params)
query.set(key, params[key]);
url += "?" + query.toString();
}
return url;
}
export async function _resolve(fragments, ctx) {

@@ -30,3 +41,3 @@ if (!fragments[2])

ctx.headers.set("X-Partial", "true");
return ctx.render(await endpoint(props, ctx));
return ctx.render(await endpoint(ctx, props));
}

@@ -1,32 +0,120 @@

/**
* These types are just helpers which could be useful
* But the goal is to add a feature in the future to help will shells merging meta data
* Currently I want more experience using the slug-shell pattern before I build it out
*/
export type ShellOptions<D = {}> = D & MetaDescriptor;
export declare function ApplyMetaDescriptorDefaults(options: ShellOptions, defaults: Readonly<Partial<ShellOptions>>): void;
export type InferShellOptions<F> = F extends (jsx: any, options: infer U) => any ? U : never;
export type MetaDescriptor = {
charSet: "utf-8";
} | {
title: string;
} | {
name: string;
content: string;
} | {
property: string;
content: string;
} | {
httpEquiv: string;
content: string;
} | {
"script:ld+json": LdJsonObject;
} | {
tagName: "meta" | "link";
[name: string]: string;
} | {
[name: string]: unknown;
title?: string;
description?: string;
meta?: Record<string, string>;
og?: OpenGraph<string>;
jsonLD?: LdJsonObject[];
};
export declare function RenderMetaDescriptor<T>(options: ShellOptions<T>): string;
export type LdJsonObject = {
[Key in string]?: LdJsonValue | undefined;
};
export type LdJsonArray = LdJsonValue[] | readonly LdJsonValue[];
export type LdJsonPrimitive = string | number | boolean | null;
export type LdJsonValue = LdJsonPrimitive | LdJsonObject | LdJsonArray;
type LdJsonArray = LdJsonValue[] | readonly LdJsonValue[];
type LdJsonPrimitive = string | number | boolean | null;
type LdJsonValue = LdJsonPrimitive | LdJsonObject | LdJsonArray;
export type OpenGraphType = "website" | "article" | "book" | "profile" | "music.song" | "music.album" | "music.playlist" | "music.radio_station" | "video.movie" | "video.episode" | "video.tv_show" | "video.other" | string;
export type OpenGraph<T extends OpenGraphType = string> = {
type?: T;
title?: string;
description?: string;
determiner?: string;
url?: string;
secure_url?: string;
locale?: string | {
base: string;
alternative: string[];
};
image?: OpenGraphImage[];
video?: OpenGraphVideo[];
audio?: OpenGraphAudio[];
} & (T extends "music.song" ? OpenGraphSong : T extends "music.album" ? OpenGraphAlbum : T extends "music.playlist" ? OpenGraphPlaylist : T extends "music.radio_station" ? OpenGraphRadioStation : T extends "video.movie" ? OpenGraphMovie : T extends "video.episode" ? OpenGraphEpisode : T extends "video.tv_show" ? OpenGraphTvShow : T extends "video.other" ? OpenGraphVideoOther : T extends "article" ? OpenGraphArticle : T extends "book" ? OpenGraphBook : T extends "profile" ? OpenGraphProfile : {});
export type OpenGraphImage = {
url: string;
secure_url?: string;
type?: string;
width?: number;
height?: number;
alt?: string;
};
export type OpenGraphVideo = {
url: string;
type?: string;
secure_url?: string;
width?: number;
height?: number;
alt?: string;
};
export type OpenGraphAudio = {
url: string;
type?: string;
secure_url?: string;
};
type OpenGraphSong = {
duration?: number;
album?: Array<string | {
url: string;
disc?: number;
track?: number;
}>;
musician?: string[];
};
type OpenGraphAlbum = {
songs?: Array<string | {
url: string;
disc?: number;
track?: number;
}>;
musician?: string[];
release_date?: Date;
};
type OpenGraphPlaylist = {
songs?: Array<string | {
url: string;
disc?: number;
track?: number;
}>;
creator?: string[];
};
type OpenGraphRadioStation = {
creator?: string[];
};
type OpenGraphMovie = {
actors?: Array<string | {
url: string;
role: string;
}>;
directors?: string[];
writers?: string[];
duration?: number;
release_date?: Date;
tag: string[];
};
type OpenGraphEpisode = OpenGraphMovie & {
series?: string;
};
type OpenGraphTvShow = OpenGraphMovie;
type OpenGraphVideoOther = OpenGraphMovie;
type OpenGraphArticle = {
published_time?: Date;
modified_time?: Date;
expiration_time?: Date;
authors?: string[];
section?: string;
tag?: string;
};
type OpenGraphBook = {
authors?: string[];
isbn?: string;
release_date?: Date;
tag?: string;
};
type OpenGraphProfile = {
first_name?: string;
last_name?: string;
username?: string;
gender?: "male" | "female";
};
export {};

@@ -1,8 +0,251 @@

/**
* These types are just helpers which could be useful
* But the goal is to add a feature in the future to help will shells merging meta data
* Currently I want more experience using the slug-shell pattern before I build it out
*/
export {};
// export type ShellOptions = { meta?: Array<MetaDescriptor> } | undefined;
// export type ShellProps<T> = T & ShellOptions;
export function ApplyMetaDescriptorDefaults(options, defaults) {
if (defaults.title && !options.title)
options.title = defaults.title;
if (defaults.description && !options.description)
options.description = defaults.description;
if (defaults.meta && !options.meta)
options.meta = defaults.meta;
if (defaults.og && !options.og)
options.og = defaults.og;
if (defaults.jsonLD && !options.jsonLD)
options.jsonLD = defaults.jsonLD;
}
export function RenderMetaDescriptor(options) {
let out = "";
if (options.title)
out += `<title>${EscapeHTML(options.title)}</title>`;
if (options.description)
out += `<meta name="description" content="${EscapeHTML(options.description)}">\n`;
if (options.meta)
for (const key in options.meta) {
out += `<meta name="${EscapeHTML(key)}" content="${EscapeHTML(options.meta[key])}">\n`;
}
if (options.jsonLD)
for (const json of options.jsonLD) {
out += `<script>${EscapeHTML(JSON.stringify(json))}</script>\n`;
}
// Auto apply og:title + og:description if not present
if (options.title && !options.og?.title)
out += `<meta property="og:title" content="${EscapeHTML(options.title)}">\n`;
if (options.description && !options.og?.description)
out += `<meta property="og:description" content="${EscapeHTML(options.description)}">\n`;
// Apply open graphs
if (options.og)
out += RenderOpenGraph(options.og);
return out;
}
function RenderOpenGraph(og) {
// Manually encoding everything rather than using a loop to ensure they are in the correct order
// And to ensure extra values can't leak in creating unsafe og tags
const type = og.type || "website";
let out = RenderProperty("og:type", type);
if (og.title)
out += RenderProperty("og:title", og.title);
if (og.description)
out += RenderProperty("og:description", og.description);
if (og.determiner)
out += RenderProperty("og:determiner", og.determiner);
if (og.url)
out += RenderProperty("og:url", og.url);
if (og.secure_url)
out += RenderProperty("og:secure_url", og.secure_url);
if (og.locale) {
if (typeof og.locale === "string")
out += RenderProperty("og:locale", og.locale);
else {
out += RenderProperty("og:locale", og.locale.base);
for (const l of og.locale.alternative)
out += RenderProperty("og:locale:alternative", l);
}
}
if (og.image)
for (const img of og.image) {
out += RenderProperty("og:image", img.url);
if (img.secure_url)
out += RenderProperty("og:image:secure_url", img.secure_url);
if (img.type)
out += RenderProperty("og:image:type", img.type);
if (img.width)
out += RenderProperty("og:image:width", img.width.toString());
if (img.height)
out += RenderProperty("og:image:height", img.height.toString());
if (img.alt)
out += RenderProperty("og:image:alt", img.alt);
}
if (og.video)
for (const vid of og.video) {
out += RenderProperty("og:video", vid.url);
if (vid.secure_url)
out += RenderProperty("og:video:secure_url", vid.secure_url);
if (vid.type)
out += RenderProperty("og:video:type", vid.type);
if (vid.width)
out += RenderProperty("og:video:width", vid.width.toString());
if (vid.height)
out += RenderProperty("og:video:height", vid.height.toString());
if (vid.alt)
out += RenderProperty("og:video:alt", vid.alt);
}
if (og.audio)
for (const audio of og.audio) {
out += RenderProperty("og:audio", audio.url);
if (audio.secure_url)
out += RenderProperty("og:audio:secure_url", audio.secure_url);
if (audio.type)
out += RenderProperty("og:audio:type", audio.type);
}
return out + RenderOpenGraphExtras(og);
}
function RenderProperty(name, value) {
return `<meta property="${name}" content="${EscapeHTML(value)}">\n`;
}
function RenderOpenGraphExtras(og) {
let out = "";
if (og.type === "music.song") {
const g = og;
if (g.duration)
out += RenderProperty("og:music:duration", g.duration.toString());
if (g.album)
for (const album of g.album) {
if (typeof album === "string")
out += RenderProperty("og:music:album", album);
else {
out += RenderProperty("og:music:album", album.url);
if (album.disc)
out += RenderProperty("og:music:album:disc", album.disc.toString());
if (album.track)
out += RenderProperty("og:music:album:track", album.track.toString());
}
}
if (g.musician)
for (const profile of g.musician)
out += RenderProperty("og:music:musician", profile);
return out;
}
if (og.type === "music.album") {
const g = og;
if (g.songs)
for (const song of g.songs) {
if (typeof song === "string")
out += RenderProperty("og:music:song", song);
else {
out += RenderProperty("og:music:song", song.url);
if (song.disc)
out += RenderProperty("og:music:song:disc", song.disc.toString());
if (song.track)
out += RenderProperty("og:music:song:track", song.track.toString());
}
}
if (g.musician)
for (const profile of g.musician)
out += RenderProperty("og:music:musician", profile);
if (g.release_date)
out += RenderProperty("og:music:release_date", g.release_date.toISOString());
return out;
}
if (og.type === "music.playlist") {
const g = og;
if (g.songs)
for (const song of g.songs) {
if (typeof song === "string")
out += RenderProperty("og:music:song", song);
else {
out += RenderProperty("og:music:song", song.url);
if (song.disc)
out += RenderProperty("og:music:song:disc", song.disc.toString());
if (song.track)
out += RenderProperty("og:music:song:track", song.track.toString());
}
}
if (g.creator)
for (const profile of g.creator)
out += RenderProperty("og:music:creator", profile);
return out;
}
if (og.type === "music.radio_station") {
const g = og;
if (g.creator)
for (const profile of g.creator)
out += RenderProperty("og:music:creator", profile);
return out;
}
if (og.type === "video.movie" || og.type === "video.episode" || og.type === "video.tv_show" || og.type === "video.other") {
const g = og;
if (g.actors)
for (const actor of g.actors) {
if (typeof actor === "string")
out += RenderProperty("og:video:actor", actor);
else {
out += RenderProperty("og:video:actor", actor.url);
out += RenderProperty("og:video:actor:role", actor.role);
}
}
if (g.directors)
for (const profile of g.directors)
out += RenderProperty("og:video:director", profile);
if (g.writers)
for (const profile of g.writers)
out += RenderProperty("og:video:writer", profile);
if (g.duration)
out += RenderProperty("og:video:duration", g.duration.toString());
if (g.release_date)
out += RenderProperty("og:video:release_date", g.release_date.toISOString());
if (g.tag)
for (const tag of g.tag)
out += RenderProperty("og:video:tag", tag);
if (g.series)
out += RenderProperty("og:video:series", g.series);
}
if (og.type === "article") {
const g = og;
if (g.published_time)
out += RenderProperty("og:article:published_time", g.published_time.toISOString());
if (g.modified_time)
out += RenderProperty("og:article:modified_time", g.modified_time.toISOString());
if (g.expiration_time)
out += RenderProperty("og:article:expiration_time", g.expiration_time.toISOString());
if (g.authors)
for (const profile of g.authors)
out += RenderProperty("og:article:author", profile);
if (g.section)
out += RenderProperty("og:article:section", g.section);
if (g.tag)
for (const tag of g.tag)
out += RenderProperty("og:video:tag", tag);
}
if (og.type === "book") {
const g = og;
if (g.authors)
for (const profile of g.authors)
out += RenderProperty("og:article:author", profile);
if (g.isbn)
out += RenderProperty("og:book:isbn", g.isbn);
if (g.release_date)
out += RenderProperty("og:book:release_date", g.release_date.toISOString());
if (g.tag)
for (const tag of g.tag)
out += RenderProperty("og:video:tag", tag);
}
if (og.type === "profile") {
const g = og;
if (g.first_name)
out += RenderProperty("og:profile:first_name", g.first_name);
if (g.last_name)
out += RenderProperty("og:profile:last_name", g.last_name);
if (g.username)
out += RenderProperty("og:profile:username", g.username);
if (g.gender)
out += RenderProperty("og:profile:gender", g.gender);
}
return "";
}
const escapeTo = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
"\"": "&quot;",
"'": "&#39;",
};
function EscapeHTML(str) {
return str.replace(/[&<>"']/g, (match) => escapeTo[match] || match);
}
{
"name": "htmx-router",
"version": "1.0.0-alpha.4",
"version": "1.0.0-alpha.5",
"description": "A simple SSR framework with dynamic+client islands",

@@ -5,0 +5,0 @@ "keywords": ["htmx", "router", ""],

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