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

@podium/layout

Package Overview
Dependencies
Maintainers
6
Versions
252
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@podium/layout - npm Package Compare versions

Comparing version 5.0.11 to 5.1.0

types/layout.d.ts

18

CHANGELOG.md

@@ -0,1 +1,19 @@

# [5.1.0](https://github.com/podium-lib/layout/compare/v5.0.11...v5.1.0) (2024-02-13)
### Bug Fixes
* .gitignore ([10eacc1](https://github.com/podium-lib/layout/commit/10eacc132c0b8eb892117beeebfe2485300d5225))
* add types folder to package ([78e0725](https://github.com/podium-lib/layout/commit/78e07253d6c9a453c23b2880d313c3ecf832a671))
* fix @types/readable-stream missing type in constructor ([a6dc40a](https://github.com/podium-lib/layout/commit/a6dc40a564c57bde6928ac0897251bd14675d232))
* keep original type for middleware incoming.url ([1dc150a](https://github.com/podium-lib/layout/commit/1dc150afb539046ec9bc498b42b2fe6ebc5c4498))
* no ts in tests ([1395258](https://github.com/podium-lib/layout/commit/13952586e44846aaf84988fb6e683ad2b46197ae))
* remove old type file ([26d6c0e](https://github.com/podium-lib/layout/commit/26d6c0e3caa0ebec2d23df5dcc615a32507d05a2))
### Features
* created jsdoc, and setup type generation ([61ae410](https://github.com/podium-lib/layout/commit/61ae41043d9d029e26439c1dcd682f6dc34ed180))
* type generation script ([8662636](https://github.com/podium-lib/layout/commit/86626364df8a1a897a39450185b4dc0a82c2e850))
## [5.0.11](https://github.com/podium-lib/layout/compare/v5.0.10...v5.0.11) (2024-02-05)

@@ -2,0 +20,0 @@

584

lib/layout.js

@@ -8,5 +8,9 @@ /* eslint-disable consistent-return */

HttpIncoming,
// @ts-ignore
template,
// @ts-ignore
isFunction,
// @ts-ignore
pathnameBuilder,
// @ts-ignore
uriIsRelative,

@@ -28,11 +32,196 @@ AssetCss,

const currentDirectory = dirname(fileURLToPath(import.meta.url));
const pkgJson = fs.readFileSync(join(currentDirectory, '../package.json'), 'utf-8');
const pkgJson = fs.readFileSync(
join(currentDirectory, '../package.json'),
'utf-8',
);
const pkg = JSON.parse(pkgJson);
const _pathname = Symbol('_pathname');
const _sanitize = Symbol('_sanitize');
const _addCssAsset = Symbol('_addCssAsset');
const _addJsAsset = Symbol('_addJsAsset');
/**
* @typedef {(...args: any) => void} LogFunction
* @typedef {{ trace: LogFunction, debug: LogFunction, info: LogFunction, warn: LogFunction, error: LogFunction, fatal: LogFunction }} AbsLogger
*
* @typedef {Object} LayoutOptions
* @property {string} name - (required) layout name
* @property {string} pathname - (required) layout pathname
* @property {Console | AbsLogger} [logger] - A logger to use when provided. Can be the console object if console logging is desired but can also be any Log4j compatible logging object as well. Nothing is logged if no logger is provided. (default null)
* @property {LayoutContext} [context] - Options to be passed on to the internal `@podium/context` constructor. See that module for details. (default null)
* @property {LayoutClientOptions} [client] - Options to be passed on to the internal `@podium/client` constructor. See that module for details. (default null)
* @property {import("@podium/proxy").default.PodiumProxyOptions} [proxy] - Options to be passed on to the internal `@podium/proxy` constructor. See that module for details. (default null)
*
*
* @typedef {Object} LayoutContext
* @property {{enabled: boolean}} [debug] - Config object passed on to the debug parser. (default { enabled: false })
* @property {Object} [locale] - Config object passed on to the locale parser.
* @property {Object} [deviceType] - Config object passed on to the device type parser.
* @property {Object} [mountOrigin] - Config object passed on to the mount origin parser.
* @property {Object} [mountPathname] - Config object passed on to the mount pathname parser.
* @property {Object} [publicPathname] - Config object passed on to the public pathname parser.
*
* @typedef {Object} LayoutClientOptions
* @property {number} [retries=4] - Number of times the client should retry settling a version number conflict before terminating. (default 4)
* @property {number} [timeout=1000] - Default value, in milliseconds, for how long a request should wait before the connection is terminated. (default 1000)
* @property {number} [maxAge=Infinity] - Default value, in milliseconds, for how long manifests should be cached. (default Infinity)
*
* @typedef {{ as?: string | false | null, crossorigin?: string | null | boolean, disabled?: boolean | '' | null, hreflang?: string | false | null, title?: string | false | null, media?: string | false | null, rel?: string | false | null, type?: string | false | null, value: string | false | null, data?: Array<{ key: string; value: string }>, strategy?: "beforeInteractive" | "afterInteractive" | "lazy", scope?: "content" | "fallback" | "all", [key: string]: any }} AssetCssLike
* @typedef {{ value: string | null, crossorigin?: string | null | boolean, type?: string | null | false, integrity?: string | null | false, referrerpolicy?: string | null | false, nomodule?: boolean | null | '', async?: boolean | null | '', defer?: boolean | null | '', data?: Array<{ key: string; value: string }>, strategy?: "beforeInteractive" | "afterInteractive" | "lazy", scope?: "content" | "fallback" | "all", [key: string]: any }} AssetJsLike
*/
export default class PodiumLayout {
/**
* Podium document template. A custom document template is set by using the .view() method in the podlet and layout modules.
*
* @see https://podium-lib.io/docs/api/document
*
* @example ```js
* const layout = new Layout({ ... });
* layout.view(myDocumentTemplate);
* ```
*/
#view = template;
export default class PodiumLayout {
/**
* Name that the layout identifies itself by (set in the constructor).
* This value must be in camelCase.
*
* @see https://podium-lib.io/docs/api/layout/#name
*
* @example ```js
* const layout = new Layout({
* name: 'myLayoutName',
* pathname: '/foo',
* });
* ```
*/
name = '';
/**
* The Pathname to where the layout is mounted in an HTTP server.
* It is important that this value matches the entry point of the route where content is served in the HTTP server
* since this value is used to mount the proxy and inform podlets (through the Podium context) where they are mounted and where the proxy is mounted.
* If the layout is mounted at the server "root", set the pathname to /:
*
* @see https://podium-lib.io/docs/api/layout/#pathname
*/
#pathname = '';
/**
* A logger. The abstract logger "Abslog" is used to make it possible to provide different kinds of loggers.
* The logger can be provided via the 'logger' constructor argument.
*
* @see https://www.npmjs.com/package/abslog
*
* @example ```js
* const layout = new Layout({ logger: console, ... });
* layout.log.trace('trace log to the console')
* layout.log.debug('debug log to the console')
* layout.log.info('info log to the console')
* layout.log.warn('warn log to the console')
* layout.log.error('error log to the console')
* layout.log.fatal('fatal log to the console')
* ```
*
* @type {AbsLogger}
*/
log;
/**
* An instance of the `@podium/proxy` module
* @see https://github.com/podium-lib/proxy
*
* @type{import("@podium/proxy").default}
*/
httpProxy;
/**
* Options to be passed on to the context parsers. See the `@podium/context` module for details.
*
* @example ```js
* const layout = new Layout({
* name: 'myLayout',
* pathname: '/foo',
* context: {
* debug: {
* enabled: true,
* },
* },
* });
* ```
*
* @see https://podium-lib.io/docs/api/layout/#context
* @see https://podium-lib.io/docs/layout/context
* @see https://podium-lib.io/docs/podlet/context
*
* @type {LayoutContext}
*/
context;
/**
* Property that holds information about a Cascading Style Sheet related to a layout.
*
* @see https://podium-lib.io/docs/api/assets#assetcss
* @see https://podium-lib.io/docs/api/assets
*
* @example ```js
* const podlet = layout.client.register({
* name: 'myPodlet',
* uri: 'http://localhost:7100/manifest.json',
* });
*
* app.get(layout.pathname(), async (req, res, next) => {
* const incoming = res.locals.podium;
*
* const response = await podlet.fetch(incoming);
*
* console.log(incoming.css) // array with the layouts and podlets AssetCSS objects *
*
* [ ... ]
* });
* ```
*
* @type {AssetCss[]}
*/
cssRoute = [];
/**
* Property that holds information about a layout's Javascript client side assets.
*
* @see https://podium-lib.io/docs/api/assets#assetjs
* @see https://podium-lib.io/docs/api/assets
*
* @example ```js
* const podlet = layout.client.register({
* name: 'myPodlet',
* uri: 'http://localhost:7100/manifest.json',
* });
*
* app.get(layout.pathname(), async (req, res, next) => {
* const incoming = res.locals.podium;
*
* const response = await podlet.fetch(incoming);
*
* console.log(response.js) // array with the podlets AssetJS objects
*
* [ ... ]
* });
* ```
*
* @type {AssetJs[]}
*/
jsRoute = [];
/**
* Metrics client stream object that can be used to consume metrics out of a Podium layout.
* @see https://www.npmjs.com/package/@metrics/client for detailed documentation
*
* @example
* ```js
* const layout = new Layout(...);
* layout.metrics.pipe(...);
* // or
* layout.metrics.on('data', chunk => { ... });
* ```
*
* @type {import("@metrics/client")}
*/
metrics = new Metrics();
constructor({

@@ -45,3 +234,3 @@ name = '',

proxy = {},
} = {}) {
}) {
if (schema.name(name).error)

@@ -57,61 +246,27 @@ throw new Error(

Object.defineProperty(this, 'name', {
value: name,
this.name = name;
this.#pathname = this.#sanitize(pathname);
this.log = abslog(logger);
this.client = new Client({
name: this.name,
logger: this.log,
...client,
});
Object.defineProperty(this, _pathname, {
value: pathname,
this.httpProxy = new Proxy({
pathname: this.#pathname,
logger: this.log,
...proxy,
});
Object.defineProperty(this, 'cssRoute', {
value: [],
this.context = new Context({
name: this.name,
mountPathname: {
pathname: this.#pathname,
},
publicPathname: {
pathname: this.#pathname,
},
logger: this.log,
...context,
});
Object.defineProperty(this, 'jsRoute', {
value: [],
});
Object.defineProperty(this, 'log', {
value: abslog(logger),
});
Object.defineProperty(this, 'client', {
value: new Client(
{ name: this.name, logger: this.log, ...client},
),
enumerable: true,
});
Object.defineProperty(this, 'context', {
enumerable: true,
value: new Context(
{ name: this.name,
mountPathname: {
pathname: this[_pathname],
},
publicPathname: {
pathname: this[_pathname],
},
logger: this.log,
...context},
),
});
Object.defineProperty(this, 'httpProxy', {
enumerable: true,
value: new Proxy(
{ pathname: this[_pathname], logger: this.log, ...proxy},
),
});
Object.defineProperty(this, '_view', {
value: template,
writable: true,
});
Object.defineProperty(this, 'metrics', {
enumerable: true,
value: new Metrics(),
});
// Skip a tick to ensure the metric stream has been consumed

@@ -147,2 +302,3 @@ setImmediate(() => {

this.httpProxy.metrics.pipe(this.metrics);
// @ts-ignore
this.context.metrics.pipe(this.metrics);

@@ -152,3 +308,5 @@ this.client.metrics.pipe(this.metrics);

// Register proxy endpoints
// @ts-ignore
this.client.registry.on('set', (key, item) => {
// @ts-ignore
this.httpProxy.register(key, item.newVal);

@@ -162,24 +320,74 @@ });

[_addCssAsset](options = {}) {
const clonedOptions = JSON.parse(JSON.stringify(options));
clonedOptions.value = this[_sanitize](clonedOptions.value, clonedOptions.prefix)
const args = { prefix: true, ...clonedOptions, pathname: this._pathname };
this.cssRoute.push(new AssetCss(args));
/**
* Takes an AssetCss instance or an object with equivalent properties, converts it to an AssetCss instance if necessary and adds it to the
* cssRoute array.
* @param { AssetCss | AssetCssLike } options
* @returns {void}
*/
#addCssAsset(options) {
const clonedOptions = JSON.parse(JSON.stringify(options));
clonedOptions.value = this.#sanitize(
clonedOptions.value,
clonedOptions.prefix,
);
const args = {
prefix: true,
...clonedOptions,
pathname: this.#pathname,
};
this.cssRoute.push(new AssetCss(args));
}
css(options = {}) {
/**
* Set relative or absolute URLs to Cascading Style Sheets (CSS) assets for the layout.
* When set the values will be internally kept and made available for the document template to include.
* This method can be called multiple times with a single options object to set multiple assets or one can provide an array of options objects to set multiple assets.
* @see https://podium-lib.io/docs/api/layout/#cssoptionsoptions
* @see https://podium-lib.io/docs/api/assets#assetcss
* @see https://podium-lib.io/docs/api/assets
*
* @example ```js
* const app = express();
* const layout = new Layout({
* name: 'myLayout',
* pathname: '/',
* });
*
* app.get('/assets.css', (req, res) => {
* res.status(200).sendFile('./src/js/main.css', err => {});
* });
*
* layout.css({ value: '/assets.css' });
* ```
*
* @param { AssetCss | AssetCss[] | AssetCssLike | AssetCssLike[] } options
* @returns {void}
*/
css(options) {
if (Array.isArray(options)) {
for (const opts of options) {
this[_addCssAsset](opts);
this.#addCssAsset(opts);
}
return;
}
this[_addCssAsset](options);
this.#addCssAsset(options);
}
[_addJsAsset](options = {}) {
/**
* Takes an AssetJs instance or an object with equivalent properties, converts it to an AssetJs instance if necessary and adds it to the
* jsRoute array.
* @param { AssetJs | AssetJsLike } options
* @returns {void}
*/
#addJsAsset(options) {
const clonedOptions = JSON.parse(JSON.stringify(options));
clonedOptions.value = this[_sanitize](clonedOptions.value, clonedOptions.prefix)
clonedOptions.value = this.#sanitize(
clonedOptions.value,
clonedOptions.prefix,
);
const args = { prefix: true, ...clonedOptions, pathname: this._pathname };
const args = {
prefix: true,
...clonedOptions,
pathname: this.#pathname,
};

@@ -201,13 +409,70 @@ // Convert data attribute object structure to array of key value objects

js(options = {}) {
/**
* Set relative or absolute URLs to JavaScript assets for the layout.
* When set, the values will be internally kept and made available for the document template to include.
* This method can be called multiple times with a single options object to set multiple assets or one can provide an array of options objects to set multiple assets.
*
* @see https://podium-lib.io/docs/api/layout/#jsoptionsoptions
* @see https://podium-lib.io/docs/api/assets#assetjs
* @see https://podium-lib.io/docs/api/assets
*
* @example ```js
* const app = express();
* const layout = new Layout({
* name: 'myLayout',
* pathname: '/',
* });
*
* app.get('/assets.js', (req, res) => {
* res.status(200).sendFile('./src/js/main.js', err => {});
* });
*
* layout.js({ value: '/assets.js' });
* ```
*
* @param {AssetJs | AssetJs[] | AssetJsLike | AssetJsLike[] } [options]
* @returns {void}
*/
js(options) {
if (Array.isArray(options)) {
for (const opts of options) {
this[_addJsAsset](opts);
this.#addJsAsset(opts);
}
return;
}
this[_addJsAsset](options);
this.#addJsAsset(options);
}
view(fn = null) {
/**
* Sets the default document template.
* Takes a template function that accepts an instance of HttpIncoming, a content string as well as any additional markup for the document's head section:
* ```js
* (incoming, body, head) => `Return an HTML string here`;
* ```
*
* @see https://podium-lib.io/docs/api/layout/#viewtemplate
* @see https://podium-lib.io/docs/api/document
*
* @example
* A document template can be provided using the layout.view method
* ```js
* layout.view((incoming, body, head) => `<!doctype html>
* <html lang="${incoming.context.locale}">
* <head>
* <meta charset="${incoming.view.encoding}">
* <title>${incoming.view.title}</title>
* ${head}
* </head>
* <body>
* ${body}
* </body>
* </html>`;
* );
* ```
*
* @template {{ [key: string]: unknown }} T
* @param {( incoming: HttpIncoming<T>, fragment: string, ...args: unknown[]) => string} fn
* @returns {void}
*/
view(fn) {
if (!isFunction(fn)) {

@@ -218,9 +483,87 @@ throw new Error(

}
this._view = fn;
this.#view = fn;
}
/**
* Method to render the document template. By default this will render a default document template provided by Podium unless a custom one is set by using the .view method.
* In most HTTP frameworks this method can be ignored in favour of res.podiumSend(). If present, res.podiumSend() has the advantage that it's not necessary to pass in HttpIncoming as the first argument.
* @see https://podium-lib.io/docs/api/layout#renderhttpincoming-fragment-args
*
* @example
* ```js
* layout.view = (incoming, body, head) => {
* return `
* <html>
* <head>${head}</head>
* <body>${body}</body>
* </html>
* `;
* };
*
* app.get(layout.pathname(), (req, res) => {
* const incoming = res.locals.podium;
*
* const head = `<meta ..... />`;
* const body = `<section>my content</section>`;
*
* const document = layout.render(incoming, body, head);
*
* res.send(document);
* });
* ```
*
* @template {{ [key: string]: unknown }} T
* @param {HttpIncoming<T>} incoming - Instance of Podium HttpIncoming object
* @param {string} data - the content as an HTML markup string
* @param {...any} args - additional args depending on the template and what values it accepts
* @returns {string}
*/
render(incoming, data, ...args) {
return this._view(incoming, data, ...args);
return this.#view(incoming, data, ...args);
}
/**
* Method for processing an incoming HTTP request. This method is intended to be used to implement support for multiple HTTP frameworks and in most cases it won't be necessary for layout developers to use this method directly when creating a layout server.
* What it does:
* * Runs context parsers on the incoming request and sets an object with the context at HttpIncoming.context which can be passed on to the client when requesting content from podlets.
* * Mounts a proxy so that each podlet can do transparent proxy requests as needed.
* * Returns a Promise which will resolve with the HttpIncoming object that was passed in.
*
* If the inbound request matches a proxy endpoint the returned Promise will resolve with a HttpIncoming object where the .proxy property is set to true.
*
* @see https://podium-lib.io/docs/api/layout#processhttpincoming
* @see https://podium-lib.io/docs/api/incoming
*
* @example
* ```js
* import { HttpIncoming } from '@podium/utils';
* import Layout from '@podium/layout';
*
* const layout = new Layout({
* name: 'myLayout',
* pathname: '/',
* });
*
* const server = http.createServer(async (req, res) => {
* const incoming = new HttpIncoming(req, res);
*
* try {
* const result = await layout.process(incoming);
* if (result.proxy) return;
*
* res.statusCode = 200;
* res.setHeader('Content-Type', 'application/json');
* res.end(JSON.stringify(result));
* } catch (error) {
* res.statusCode = 500;
* res.setHeader('Content-Type', 'text/plain');
* res.end('Internal server error');
* }
* });
* ```
*
* @param {HttpIncoming} incoming
* @param {{ proxy?: boolean, context?: boolean }} [options]
* @returns {Promise<HttpIncoming>}
*/
async process(incoming, { proxy = true, context = true } = {}) {

@@ -231,2 +574,3 @@ incoming.name = this.name;

// @ts-ignore
if (context) await this.context.process(incoming);

@@ -238,6 +582,25 @@ if (proxy) await this.httpProxy.process(incoming);

/**
* A Connect/Express compatible middleware function which takes care of the various operations needed for a layout to operate correctly. This function is more or less just a wrapper for the .process() method.
* The middleware will create an HttpIncoming object for each request and place it on the response at res.locals.podium.
*
* **Important:** *This middleware must be mounted before defining any routes.*
*
* @see https://podium-lib.io/docs/api/layout#middleware
*
* @example
* ```js
* const app = express();
* app.use(layout.middleware());
* ```
*
* @returns {(req: any, res: any, next: function) => Promise<void>}
*/
middleware() {
return async (req, res, next) => {
const incoming = new HttpIncoming(req, res, res.locals);
incoming.url = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
const incoming = new HttpIncoming(req, res);
// @ts-ignore
incoming.url = `${req.protocol}://${req.get('host')}${
req.originalUrl
}`;

@@ -262,13 +625,60 @@ try {

/**
* A helper method used to retrieve the pathname value that was set in the constructor. This can be handy when defining routes since the pathname set in the constructor must also be the base path for the layout's main content route
* (set in the constructor)
*
* @see https://podium-lib.io/docs/api/layout/#pathname
*
* @example
* The method returns the value of `pathname` as defined in the layout constructor
* ```js
* const layout = new Layout({ pathname: '/foo', ... });
* layout.pathname() // /foo
* ```
*
* @example
* This method is typically used when defining routes to ensure the pathname is prepended to any routes
* ```js
* const layout = new Layout({
* name: 'myLayout',
* pathname: '/foo',
* });
*
* app.get(layout.pathname(), (req, res, next) => {
* [ ... ]
* });
*
* app.get(`${layout.pathname()}/bar`, (req, res, next) => {
* [ ... ]
* });
*
* app.get(`${layout.pathname()}/bar/:id`, (req, res, next) => {
* [ ... ]
* });
* ```
*
* @returns {string}
*/
pathname() {
return this[_pathname];
return this.#pathname;
}
[_sanitize](uri, prefix = false) {
const pathname = prefix ? this.pathname() : '';
/**
* Sanitizes a uri and returns the resulting uri.
* If prefix is true (default false) and the uri is relative, the layout pathname will be prepended to the uri
* @param {string} uri
* @param {boolean} prefix
* @returns {string}
*/
#sanitize(uri, prefix = false) {
const pathname = prefix ? this.#pathname : '';
if (uri) {
return uriIsRelative(uri) ? pathnameBuilder(pathname, uri) : uri;
// @ts-ignore
return uriIsRelative(uri)
? // @ts-ignore
pathnameBuilder(pathname, uri)
: uri;
}
return uri;
}
};
}

28

package.json
{
"name": "@podium/layout",
"version": "5.0.11",
"version": "5.1.0",
"type": "module",

@@ -24,5 +24,5 @@ "description": "Module for composing full page layouts out of page fragments in a micro frontend architecture.",

"CHANGELOG.md",
"layout.d.ts",
"README.md",
"LICENSE",
"types",
"dist",

@@ -32,3 +32,3 @@ "lib"

"main": "./lib/layout.js",
"types": "layout.d.ts",
"types": "./types/layout.d.ts",
"scripts": {

@@ -38,3 +38,4 @@ "lint": "eslint .",

"test": "tap --disable-coverage --allow-empty-coverage",
"test:snapshots": "tap --snapshot --disable-coverage --allow-empty-coverage"
"test:snapshots": "tap --snapshot --disable-coverage --allow-empty-coverage",
"types": "tsc --declaration --emitDeclarationOnly && ./fixup.sh"
},

@@ -49,8 +50,9 @@ "dependencies": {

"abslog": "2.4.0",
"ajv": "8.12.0",
"lodash.merge": "4.6.2",
"objobj": "1.0.0",
"ajv": "8.12.0"
"objobj": "1.0.0"
},
"devDependencies": {
"@podium/podlet": "5.0.3",
"@babel/eslint-parser": "7.23.3",
"@podium/podlet": "5.0.0",
"@podium/test-utils": "2.5.2",

@@ -60,6 +62,7 @@ "@semantic-release/changelog": "6.0.3",

"@semantic-release/git": "10.0.1",
"@semantic-release/github": "9.2.6",
"@semantic-release/npm": "11.0.2",
"@semantic-release/github": "9.2.4",
"@semantic-release/npm": "11.0.1",
"@semantic-release/release-notes-generator": "12.1.0",
"semantic-release": "22.0.12",
"@types/node": "^20.10.5",
"@types/readable-stream": "^4.0.10",
"eslint": "8.54.0",

@@ -70,10 +73,11 @@ "eslint-config-airbnb-base": "15.0.0",

"eslint-plugin-prettier": "5.0.1",
"@babel/eslint-parser": "7.23.3",
"express": "4.18.2",
"hbs": "4.2.0",
"prettier": "3.1.0",
"semantic-release": "22.0.8",
"stoppable": "1.1.0",
"supertest": "6.3.3",
"tap": "18.6.1"
"tap": "18.6.1",
"typescript": "^5.3.3"
}
}
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